From b83ee9990c5a623ce1c495aa5445b35d1348fcbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Wed, 5 Mar 2025 07:10:46 +0000 Subject: [PATCH] feat/refactor (#5) Reviewed-on: https://code.30cm.net/digimon/app-cloudep-permission-server/pulls/5 --- .gitignore | 2 +- .golangci.yaml | 7 +- Makefile | 28 +- build/Dockerfile | 22 +- chart/readme.md | 1 - etc/rbac.conf | 33 + generate/protobuf/permission.proto | 60 +- go.mod | 131 +- go.sum | 412 ++++ internal/config/config.go | 9 +- internal/domain/const.go | 18 - internal/domain/errors.go | 89 - internal/domain/permission.go | 45 - internal/domain/repository/error.go | 66 - internal/domain/repository/member_status.go | 16 - internal/domain/repository/token.go | 31 - internal/domain/usecase/bitmap.go | 16 - internal/domain/usecase/opa.go | 47 - internal/domain/usecase/permission_tree.go | 33 - internal/entity/token.go | 50 - internal/lib/metric/app.go | 30 - .../cancel_one_time_token_logic.go | 47 - .../cancel_token_by_device_i_d_logic.go | 34 + .../cancel_token_by_device_id_logic.go | 45 - .../logic/tokenservice/cancel_token_logic.go | 40 +- .../logic/tokenservice/cancel_tokens_logic.go | 30 +- .../get_user_tokens_by_device_i_d_logic.go | 49 + .../get_user_tokens_by_device_id_logic.go | 59 - .../get_user_tokens_by_u_i_d_logic.go | 50 + .../get_user_tokens_by_uid_logic.go | 59 - .../tokenservice/new_one_time_token_logic.go | 71 - .../logic/tokenservice/new_token_logic.go | 136 +- .../logic/tokenservice/refresh_token_logic.go | 91 +- internal/logic/tokenservice/utils_claims.go | 55 - internal/logic/tokenservice/utils_jwt.go | 105 - .../tokenservice/validation_token_logic.go | 50 +- internal/mock/model/permission_model.go | 115 -- internal/mock/model/permission_model_gen.go | 115 -- internal/mock/model/role_model.go | 130 -- internal/mock/model/role_model_gen.go | 130 -- internal/mock/model/role_permission_model.go | 100 - .../mock/model/role_permission_model_gen.go | 100 - internal/mock/model/user_role_model.go | 115 -- internal/mock/model/user_role_model_gen.go | 115 -- internal/model/permission_model.go | 27 - internal/model/permission_model_gen.go | 157 -- internal/model/role_model.go | 27 - internal/model/role_model_gen.go | 179 -- internal/model/role_permission_model.go | 27 - internal/model/role_permission_model_gen.go | 118 -- internal/model/user_role_model.go | 27 - internal/model/user_role_model_gen.go | 155 -- internal/model/vars.go | 5 - internal/repository/member_status.go | 104 - internal/repository/token.go | 312 --- .../tokenservice/token_service_server.go | 43 +- internal/svc/service_context.go | 38 +- internal/usecase/bitmap.go | 117 -- internal/usecase/bitmap_benchmark_test.go | 69 - internal/usecase/bitmap_test.go | 78 - permission.go | 13 +- {internal => pkg/domain}/entity/claims.go | 0 pkg/domain/entity/permission.go | 22 + pkg/domain/entity/role.go | 38 + pkg/domain/entity/role_permission.go | 15 + pkg/domain/entity/ticket.go | 6 + pkg/domain/entity/token.go | 56 + pkg/domain/entity/token_test.go | 163 ++ pkg/domain/entity/user_role.go | 15 + pkg/domain/error.go | 45 + pkg/domain/permission/status.go | 30 + pkg/domain/permission/type.go | 10 + pkg/domain/rbac/rule.go | 61 + pkg/domain/rbac/rule_test.go | 122 ++ {internal => pkg}/domain/redis.go | 29 +- pkg/domain/repository/casbin_redis_adapter.go | 20 + pkg/domain/repository/permission.go | 46 + pkg/domain/repository/role.go | 41 + pkg/domain/repository/role_permission.go | 20 + pkg/domain/repository/token.go | 50 + pkg/domain/repository/user_role.go | 27 + pkg/domain/token/additional.go | 39 + pkg/domain/token/additional_test.go | 55 + pkg/domain/token/scope.go | 7 + pkg/domain/token/scope_test.go | 28 + pkg/domain/token/type.go | 11 + pkg/domain/token/type_test.go | 28 + pkg/domain/usecase/additional.go | 10 + pkg/domain/usecase/casbin_redis_rbac.go | 20 + pkg/domain/usecase/error.go | 7 + pkg/domain/usecase/permission.go | 44 + pkg/domain/usecase/role.go | 44 + pkg/domain/usecase/role_permission.go | 31 + pkg/domain/usecase/token.go | 92 + pkg/domain/usecase/user_role.go | 21 + pkg/mock/repository/permission.go | 285 +++ pkg/mock/repository/role.go | 201 ++ pkg/mock/repository/role_permission.go | 155 ++ pkg/mock/repository/token.go | 491 +++++ pkg/mock/repository/user_role.go | 187 ++ pkg/repository/casbin_redis_adapter.go | 150 ++ pkg/repository/casbin_redis_adapter_test.go | 196 ++ pkg/repository/error.go | 12 + pkg/repository/permission.go | 176 ++ pkg/repository/permission_test.go | 474 +++++ pkg/repository/role.go | 200 ++ pkg/repository/role_permission.go | 113 ++ pkg/repository/role_permission_test.go | 266 +++ pkg/repository/role_test.go | 542 +++++ pkg/repository/start_mongo_container_test.go | 52 + pkg/repository/token.go | 352 ++++ pkg/repository/token_test.go | 1750 +++++++++++++++++ pkg/repository/user_role.go | 138 ++ pkg/repository/user_role_test.go | 335 ++++ pkg/usecase/additional.go | 35 + pkg/usecase/additional_test.go | 64 + pkg/usecase/casbin_redis_rbac.go | 202 ++ pkg/usecase/casbin_redis_rbac_test.go | 119 ++ pkg/usecase/permiission.go | 98 + pkg/usecase/permission_tree.go | 222 +++ pkg/usecase/permission_tree_test.go | 359 ++++ pkg/usecase/role.go | 234 +++ pkg/usecase/role_permission.go | 119 ++ pkg/usecase/token.go | 527 +++++ pkg/usecase/token_test.go | 612 ++++++ pkg/usecase/user_role.go | 132 ++ 126 files changed, 10881 insertions(+), 3653 deletions(-) delete mode 100644 chart/readme.md create mode 100644 etc/rbac.conf create mode 100644 go.sum delete mode 100644 internal/domain/const.go delete mode 100644 internal/domain/errors.go delete mode 100644 internal/domain/permission.go delete mode 100644 internal/domain/repository/error.go delete mode 100644 internal/domain/repository/member_status.go delete mode 100644 internal/domain/repository/token.go delete mode 100644 internal/domain/usecase/bitmap.go delete mode 100644 internal/domain/usecase/opa.go delete mode 100644 internal/domain/usecase/permission_tree.go delete mode 100644 internal/entity/token.go delete mode 100644 internal/lib/metric/app.go delete mode 100644 internal/logic/tokenservice/cancel_one_time_token_logic.go create mode 100644 internal/logic/tokenservice/cancel_token_by_device_i_d_logic.go delete mode 100644 internal/logic/tokenservice/cancel_token_by_device_id_logic.go create mode 100644 internal/logic/tokenservice/get_user_tokens_by_device_i_d_logic.go delete mode 100644 internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go create mode 100644 internal/logic/tokenservice/get_user_tokens_by_u_i_d_logic.go delete mode 100644 internal/logic/tokenservice/get_user_tokens_by_uid_logic.go delete mode 100644 internal/logic/tokenservice/new_one_time_token_logic.go delete mode 100644 internal/logic/tokenservice/utils_claims.go delete mode 100644 internal/logic/tokenservice/utils_jwt.go delete mode 100644 internal/mock/model/permission_model.go delete mode 100644 internal/mock/model/permission_model_gen.go delete mode 100644 internal/mock/model/role_model.go delete mode 100644 internal/mock/model/role_model_gen.go delete mode 100644 internal/mock/model/role_permission_model.go delete mode 100644 internal/mock/model/role_permission_model_gen.go delete mode 100644 internal/mock/model/user_role_model.go delete mode 100644 internal/mock/model/user_role_model_gen.go delete mode 100755 internal/model/permission_model.go delete mode 100755 internal/model/permission_model_gen.go delete mode 100755 internal/model/role_model.go delete mode 100755 internal/model/role_model_gen.go delete mode 100755 internal/model/role_permission_model.go delete mode 100755 internal/model/role_permission_model_gen.go delete mode 100755 internal/model/user_role_model.go delete mode 100755 internal/model/user_role_model_gen.go delete mode 100644 internal/model/vars.go delete mode 100644 internal/repository/member_status.go delete mode 100644 internal/repository/token.go delete mode 100644 internal/usecase/bitmap.go delete mode 100644 internal/usecase/bitmap_benchmark_test.go delete mode 100644 internal/usecase/bitmap_test.go rename {internal => pkg/domain}/entity/claims.go (100%) create mode 100644 pkg/domain/entity/permission.go create mode 100644 pkg/domain/entity/role.go create mode 100644 pkg/domain/entity/role_permission.go create mode 100644 pkg/domain/entity/ticket.go create mode 100644 pkg/domain/entity/token.go create mode 100644 pkg/domain/entity/token_test.go create mode 100644 pkg/domain/entity/user_role.go create mode 100644 pkg/domain/error.go create mode 100644 pkg/domain/permission/status.go create mode 100644 pkg/domain/permission/type.go create mode 100644 pkg/domain/rbac/rule.go create mode 100644 pkg/domain/rbac/rule_test.go rename {internal => pkg}/domain/redis.go (69%) create mode 100644 pkg/domain/repository/casbin_redis_adapter.go create mode 100644 pkg/domain/repository/permission.go create mode 100644 pkg/domain/repository/role.go create mode 100644 pkg/domain/repository/role_permission.go create mode 100644 pkg/domain/repository/token.go create mode 100644 pkg/domain/repository/user_role.go create mode 100644 pkg/domain/token/additional.go create mode 100644 pkg/domain/token/additional_test.go create mode 100644 pkg/domain/token/scope.go create mode 100644 pkg/domain/token/scope_test.go create mode 100644 pkg/domain/token/type.go create mode 100644 pkg/domain/token/type_test.go create mode 100644 pkg/domain/usecase/additional.go create mode 100644 pkg/domain/usecase/casbin_redis_rbac.go create mode 100644 pkg/domain/usecase/error.go create mode 100644 pkg/domain/usecase/permission.go create mode 100644 pkg/domain/usecase/role.go create mode 100644 pkg/domain/usecase/role_permission.go create mode 100644 pkg/domain/usecase/token.go create mode 100644 pkg/domain/usecase/user_role.go create mode 100644 pkg/mock/repository/permission.go create mode 100644 pkg/mock/repository/role.go create mode 100644 pkg/mock/repository/role_permission.go create mode 100644 pkg/mock/repository/token.go create mode 100644 pkg/mock/repository/user_role.go create mode 100644 pkg/repository/casbin_redis_adapter.go create mode 100644 pkg/repository/casbin_redis_adapter_test.go create mode 100644 pkg/repository/error.go create mode 100644 pkg/repository/permission.go create mode 100644 pkg/repository/permission_test.go create mode 100644 pkg/repository/role.go create mode 100644 pkg/repository/role_permission.go create mode 100644 pkg/repository/role_permission_test.go create mode 100644 pkg/repository/role_test.go create mode 100644 pkg/repository/start_mongo_container_test.go create mode 100644 pkg/repository/token.go create mode 100644 pkg/repository/token_test.go create mode 100644 pkg/repository/user_role.go create mode 100644 pkg/repository/user_role_test.go create mode 100644 pkg/usecase/additional.go create mode 100644 pkg/usecase/additional_test.go create mode 100644 pkg/usecase/casbin_redis_rbac.go create mode 100644 pkg/usecase/casbin_redis_rbac_test.go create mode 100644 pkg/usecase/permiission.go create mode 100644 pkg/usecase/permission_tree.go create mode 100644 pkg/usecase/permission_tree_test.go create mode 100644 pkg/usecase/role.go create mode 100644 pkg/usecase/role_permission.go create mode 100644 pkg/usecase/token.go create mode 100644 pkg/usecase/token_test.go create mode 100644 pkg/usecase/user_role.go diff --git a/.gitignore b/.gitignore index 602c99c..7f24146 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .idea/ -go.sum +old/go.sum account/ gen_result/ etc/permission.yaml diff --git a/.golangci.yaml b/.golangci.yaml index 3f78cd3..a7d6a77 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -117,12 +117,13 @@ issues: - gocognit - contextcheck - exclude-dirs: - - internal/model - - internal/mock +# exclude-dirs: +# - internal/logic exclude-files: - .*_test.go +# - permission_tree.go +# - role_permission.go diff --git a/Makefile b/Makefile index 777c00b..9bba1ad 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="reg.wang/app-cloudep-permission-service" .PHONY: test test: # 進行測試 @@ -42,33 +42,21 @@ run-docker: # 建立 rpc code .PHONY: build-docker build-docker: cp ./build/Dockerfile Dockerfile - docker buildx build -t $(DOCKER_REPO):$(VERSION) --build-arg SSH_PRIVATE_KEY="$(cat ~/.ssh/ed_25519)" . + docker buildx build -t $(DOCKER_REPO):$(VERSION) --build-arg SSH_PRIVATE_KEY="$(cat ~/.ssh/id_ed25519)" . 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 + mockgen -source=./pkg/domain/repository/permission.go -destination=./pkg/mock/repository/permission.go -package=mock + mockgen -source=./pkg/domain/repository/role.go -destination=./pkg/mock/repository/role.go -package=mock + mockgen -source=./pkg/domain/repository/role_permission.go -destination=./pkg/mock/repository/role_permission.go -package=mock + mockgen -source=./pkg/domain/repository/user_role.go -destination=./pkg/mock/repository/user_role.go -package=mock @echo "Generate mock files successfully" + .PHONY: migrate-database migrate-database: migrate -source file://generate/database/mysql -database 'mysql://root:yytt@tcp(127.0.0.1:3306)/digimon_permission' up diff --git a/build/Dockerfile b/build/Dockerfile index 78cd457..99fbad8 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -2,12 +2,11 @@ # BUILDER # ########### -FROM golang:1.22.3 as builder +FROM golang:1.24.0 AS builder ARG VERSION ARG BUILT ARG GIT_COMMIT -ARG SSH_PRV_KEY # private go packages ENV GOPRIVATE=code.30cm.net @@ -17,22 +16,19 @@ COPY . . RUN apt-get update && \ - apt-get install git + apt-get install -y git && \ + mkdir /root/.ssh # Make the root foler for our ssh -RUN mkdir -p /root/.ssh && \ - chmod 0700 /root/.ssh && \ - ssh-keyscan git.30cm.net > /root/.ssh/known_hosts && \ - echo "$SSH_PRV_KEY" > /root/.ssh/id_rsa && \ - chmod 600 /root/.ssh/id_rsa - +RUN --mount=type=secret,id=ssh_key,dst=/root/.ssh/id_rsa \ + ssh-keyscan git.30cm.net >> /root/.ssh/known_hosts RUN --mount=type=ssh go mod download RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ -ldflags "$FLAG" \ - -o service + -o permission ########## ## FINAL # @@ -41,7 +37,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ FROM gcr.io/distroless/static-debian11 WORKDIR /app -COPY --from=builder /app/service /app/service -COPY --from=builder /app/etc/service.yaml /app/etc/service.yaml +COPY --from=builder /app/permission /app/permission +COPY --from=builder /app/etc/permission.yaml /app/etc/permission.yaml EXPOSE 8080 -CMD ["/app/service"] \ No newline at end of file +CMD ["/app/permission"] \ No newline at end of file diff --git a/chart/readme.md b/chart/readme.md deleted file mode 100644 index 33c4f67..0000000 --- a/chart/readme.md +++ /dev/null @@ -1 +0,0 @@ -// TODO 未來要放 helm 的地方 \ No newline at end of file diff --git a/etc/rbac.conf b/etc/rbac.conf new file mode 100644 index 0000000..d038cb0 --- /dev/null +++ b/etc/rbac.conf @@ -0,0 +1,33 @@ +# 表示請求參數 +# role: 角色 +# path: api path +# method: http method +[request_definition] +r = role, path, method + +# 策略的基本參數 +# role: 角色 +# path: api path +# method: http method,可以多個 +# name: 名稱 +[policy_definition] +p = role, path, methods, name + +# 策略配對結果後的應對,只要配對到一個即可成功 +[policy_effect] +e = some(where (p.eft == allow)) + +# 規範角色對應是"用戶"與"角色" +# g=A用戶,管理員 +[role_definition] +g = _, _ + +# 策略配對規則 +# 三個條件必須完全匹配 +# g(r.role, p.role),只要判斷用戶角色(r.role)是否屬於策略角色(p.role),為什麼不寫r.role == p.role,因為role可以有繼承關係所以不能這樣寫 +# keyMatch(r.path, p.path),使用正則表達式分析api path,參考 https://casbin.org/docs/function +# regexMatch(r.method, p.methods),使用正則表達式分析http method,參考 https://casbin.org/docs/function +# 或達到一個條件 +# r.role == admin,用戶 UID 是 GodDog 全開放 +[matchers] +m = g(r.role, p.role) && keyMatch2(r.path, p.path) && regexMatch(r.method, p.methods) || r.role == "GodDog" \ No newline at end of file diff --git a/generate/protobuf/permission.proto b/generate/protobuf/permission.proto index 62f547e..5a57d4f 100644 --- a/generate/protobuf/permission.proto +++ b/generate/protobuf/permission.proto @@ -17,12 +17,20 @@ message AuthorizationReq { string device_id = 2; // scope 表示授權範圍 string scope = 3; + // 角色 + string role = 4; // data 是一個通用的 key-value 結構,用於存儲額外數據 - map data = 4; - // expires 表示過期時間 - int32 expires = 5; + map data = 5; // is_refresh_token 表示是否為刷新令牌 bool is_refresh_token = 6; + // 發送token 的 uid + string uid =7; + // 發送token 的 account + string account = 8; + // expires 表示過期時間(unixnamo utc 時間) + optional int64 expires = 9; + // expires 表示過期時間(unixnamo utc 時間) + optional int64 refreshExpire = 10; } // TokenResp 定義訪問令牌響應的結構 @@ -32,24 +40,14 @@ message TokenResp { // token_type 表示令牌類型 string token_type = 2; // expires_in 表示令牌過期時間 - int32 expires_in = 3; + int64 expires_in = 3; // refresh_token 表示刷新令牌 string refresh_token = 4; } -// CreateOneTimeTokenReq 建立一次性使用的 token, -// 要帶比較長久的 token 來,驗證後才可以 -message CreateOneTimeTokenReq { - string token = 1; -} - -message CreateOneTimeTokenResp { - string one_time_token = 1; -} - // RefreshTokenReq 更新 Token message RefreshTokenReq { - string token = 1; + string token = 1; // refresh token string scope = 2; int64 expires = 3; string device_id = 4; @@ -95,7 +93,7 @@ message Token { // ID 表示令牌的唯一標識符 string id = 1; // client_id 表示客戶端 ID - int32 client_id = 2; + int64 client_id = 2; // uid 表示用戶 ID string uid = 3; // device_id 表示設備 ID @@ -103,13 +101,13 @@ message Token { // access_token 表示訪問令牌 string access_token = 5; // expires_in 表示訪問令牌的過期時間(秒) - int32 expires_in = 6; + int64 expires_in = 6; // access_create_at 表示訪問令牌的創建時間 int64 access_create_at = 7; // refresh_token 表示刷新令牌 string refresh_token = 8; // refresh_expires_in 表示刷新令牌的過期時間(秒) - int32 refresh_expires_in = 9; + int64 refresh_expires_in = 9; // refresh_create_at 表示刷新令牌的創建時間 int64 refresh_create_at = 10; } @@ -123,10 +121,6 @@ message Tokens{ repeated TokenResp token = 1; } -message CancelOneTimeTokenReq { - repeated string token = 1; -} - // 跟 Token 相關的大小事,這次只回應錯誤,以及結果,不統一規範 // 錯誤碼應該在 Biz GW 在做回應,另外我這邊取名字比較通用, // access_token -> token , refresh_token -> one_time_token @@ -141,14 +135,16 @@ service TokenService { rpc ValidationToken(ValidationTokenReq) returns(ValidationTokenResp); // CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出 rpc CancelTokens(DoTokenByUIDReq) returns(OKResp); - // CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token - rpc CancelTokenByDeviceId(DoTokenByDeviceIDReq) returns(OKResp); - // GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens - rpc GetUserTokensByDeviceId(DoTokenByDeviceIDReq) returns(Tokens); - // GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens - rpc GetUserTokensByUid(QueryTokenByUIDReq) returns(Tokens); - // NewOneTimeToken 建立一次性使用,例如:RefreshToken - rpc NewOneTimeToken(CreateOneTimeTokenReq) returns(CreateOneTimeTokenResp); - // CancelOneTimeToken 取消一次性使用 - rpc CancelOneTimeToken(CancelOneTimeTokenReq) returns(OKResp); + // CancelTokenByDeviceID 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token + rpc CancelTokenByDeviceID(DoTokenByDeviceIDReq) returns(OKResp); + // GetUserTokensByDeviceID 取得目前所對應的 DeviceID 所存在的 Tokens + rpc GetUserTokensByDeviceID(DoTokenByDeviceIDReq) returns(Tokens); + // GetUserTokensByUID 取得目前所對應的 UID 所存在的 Tokens + rpc GetUserTokensByUID(QueryTokenByUIDReq) returns(Tokens); } + + +// permission / role / role permission 相關 +service PermissionService{ + +} \ No newline at end of file diff --git a/go.mod b/go.mod index d04251f..08255d4 100644 --- a/go.mod +++ b/go.mod @@ -1,93 +1,136 @@ -module app-cloudep-permission-server +module code.30cm.net/digimon/app-cloudep-permission-server -go 1.22.3 +go 1.23.6 require ( - code.30cm.net/digimon/library-go/errors v1.0.1 - code.30cm.net/digimon/library-go/utils/invited_code v1.0.2 - code.30cm.net/digimon/library-go/validator v1.0.0 - github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/google/uuid v1.6.0 - github.com/stretchr/testify v1.9.0 - github.com/zeromicro/go-zero v1.7.0 - go.uber.org/mock v0.4.0 - google.golang.org/grpc v1.65.0 - google.golang.org/protobuf v1.34.2 + code.30cm.net/digimon/library-go/errs v1.2.14 + code.30cm.net/digimon/library-go/mongo v0.0.9 + github.com/alicebob/miniredis/v2 v2.34.0 + github.com/casbin/casbin/v2 v2.103.0 + github.com/golang-jwt/jwt/v4 v4.5.1 + github.com/segmentio/ksuid v1.0.4 + github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.34.0 + github.com/zeromicro/go-zero v1.8.0 + go.mongodb.org/mongo-driver v1.17.2 + go.uber.org/mock v0.5.0 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.5 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/casbin/govaluate v1.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fatih/color v1.17.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.0 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.4.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/redis/go-redis/v9 v9.6.1 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.10.0 // indirect - go.uber.org/automaxprocs v1.5.3 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/oauth2 v0.20.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -99,5 +142,5 @@ require ( k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..50ee797 --- /dev/null +++ b/go.sum @@ -0,0 +1,412 @@ +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= +code.30cm.net/digimon/library-go/mongo v0.0.9 h1:fPciIE5B85tXpLg8aeVQqKVbLnfpVAk9xbMu7pE2tVw= +code.30cm.net/digimon/library-go/mongo v0.0.9/go.mod h1:KBVKz/Ci5IheI77BgZxPUeKkaGvDy8fV8EDHSCOLIO4= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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= +github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic= +github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +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.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +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= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeromicro/go-zero v1.8.0 h1:4g/8VW+fOyM51HZYPeI3mXIZdEX+Fl6SsdYX2H5PYw4= +github.com/zeromicro/go-zero v1.8.0/go.mod h1:xDBF+/iDzj30zPvu6HNUIbpz1J6+/g3Sx9D/DytJfss= +go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= +go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= +go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= +go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= +go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= +go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= +k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= +k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= +k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= +k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= +k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/config/config.go b/internal/config/config.go index 95a9aab..f83c0c3 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,14 +9,13 @@ import ( type Config struct { zrpc.RpcServerConf + + // Redis Cluster RedisCluster redis.RedisConf - Token struct { + + Token struct { RefreshExpires time.Duration Expired time.Duration Secret string } - // 加上DB結構體 - DB struct { - DsnString string - } } diff --git a/internal/domain/const.go b/internal/domain/const.go deleted file mode 100644 index 82ac1b8..0000000 --- a/internal/domain/const.go +++ /dev/null @@ -1,18 +0,0 @@ -package domain - -type GrantType string - -const ( - PasswordCredentials GrantType = "password" - ClientCredentials GrantType = "client_credentials" - Refreshing GrantType = "refresh_token" -) - -const ( - // DefaultRole 預設role - DefaultRole = "user" -) - -const ( - TokenTypeBearer = "Bearer" -) diff --git a/internal/domain/errors.go b/internal/domain/errors.go deleted file mode 100644 index 14bb3ba..0000000 --- a/internal/domain/errors.go +++ /dev/null @@ -1,89 +0,0 @@ -package domain - -import ( - mts "app-cloudep-permission-server/internal/lib/metric" - - ers "code.30cm.net/digimon/library-go/errors" - "code.30cm.net/digimon/library-go/errors/code" -) - -// 12 represents Scope -// 100 represents Category -// 9 represents Detail error code -// full code 12009 只會有 系統以及錯誤碼,category 是給系統判定用的 -// 目前 Scope 以及分類要系統共用,係向的錯誤各自服務實作就好 - -// token error 方面 -const ( - TokenUnexpectedSigningErrorCode = iota + 1 - TokenValidateErrorCode - TokenClaimErrorCode -) - -const ( - RedisDelErrorCode = iota + 20 - RedisPipLineErrorCode - RedisErrorCode -) - -const ( - PermissionNotFoundCode = iota + 30 - PermissionGetDataErrorCode -) - -// TokenUnexpectedSigningErr 30001 Token 簽名錯誤 -func TokenUnexpectedSigningErr(msg string) *ers.LibError { - mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg) -} - -// TokenTokenValidateErr 30002 Token 驗證錯誤 -func TokenTokenValidateErr(msg string) *ers.LibError { - mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg) -} - -// TokenClaimError 30003 Token 驗證錯誤 -func TokenClaimError(msg string) *ers.LibError { - mts.AppErrorMetrics.AddFailure("token", "token_claim_error") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg) -} - -// RedisDelError 30020 Redis 刪除錯誤 -func RedisDelError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - mts.AppErrorMetrics.AddFailure("redis", "del_error") - - return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg) -} - -// RedisPipLineError 30021 Redis PipLine 錯誤 -func RedisPipLineError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - mts.AppErrorMetrics.AddFailure("redis", "pip_line_error") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg) -} - -// RedisError 30022 Redis 錯誤 -func RedisError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - mts.AppErrorMetrics.AddFailure("redis", "error") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg) -} - -// PermissionNotFoundError 30030 權限錯誤 -func PermissionNotFoundError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - return ers.NewErr(code.CloudEPPermission, code.Forbidden, PermissionNotFoundCode, msg) -} - -// PermissionGetDataError 30031 解析權限時錯誤 -func PermissionGetDataError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - return ers.NewErr(code.CloudEPPermission, code.InvalidFormat, PermissionGetDataErrorCode, msg) -} diff --git a/internal/domain/permission.go b/internal/domain/permission.go deleted file mode 100644 index c4cac51..0000000 --- a/internal/domain/permission.go +++ /dev/null @@ -1,45 +0,0 @@ -package domain - -type PermissionType int8 - -const ( - PermissionTypeBackendUser PermissionType = iota + 1 - PermissionTypeFrontendUser -) - -type PermissionTypeCode string - -const ( - PermissionTypeBackCode PermissionTypeCode = "back" - PermissionTypeFrontCode PermissionTypeCode = "front" -) - -var permissionMap = map[int64]PermissionTypeCode{ - 1: PermissionTypeFrontCode, - 2: PermissionTypeBackCode, -} - -func ToPermissionTypeCode(code int64) (PermissionTypeCode, bool) { - result, ok := permissionMap[code] - if !ok { - return "", false - } - - return result, true -} - -func (t *PermissionTypeCode) ToString() string { - return string(*t) -} - -type PermissionStatus string -type Permissions map[string]PermissionStatus - -const ( - PermissionStatusOpenCode PermissionStatus = "open" - PermissionStatusCloseCode PermissionStatus = "close" -) - -const ( - AdminRoleID = "GodDog!@#" -) diff --git a/internal/domain/repository/error.go b/internal/domain/repository/error.go deleted file mode 100644 index 82bdda6..0000000 --- a/internal/domain/repository/error.go +++ /dev/null @@ -1,66 +0,0 @@ -package repository - -import ( - mts "app-cloudep-permission-server/internal/lib/metric" - - ers "code.30cm.net/digimon/library-go/errors" - "code.30cm.net/digimon/library-go/errors/code" -) - -// token error 方面 -const ( - TokenUnexpectedSigningErrorCode = iota + 1 - TokenValidateErrorCode - TokenClaimErrorCode -) - -const ( - RedisDelErrorCode = iota + 20 - RedisPipLineErrorCode - RedisErrorCode -) - -// TokenUnexpectedSigningErr 30001 Token 簽名錯誤 -func TokenUnexpectedSigningErr(msg string) *ers.LibError { - mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg) -} - -// TokenTokenValidateErr 30002 Token 驗證錯誤 -func TokenTokenValidateErr(msg string) *ers.LibError { - mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg) -} - -// TokenClaimError 30003 Token 驗證錯誤 -func TokenClaimError(msg string) *ers.LibError { - mts.AppErrorMetrics.AddFailure("token", "token_claim_error") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg) -} - -// RedisDelError 30020 Redis 刪除錯誤 -func RedisDelError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - mts.AppErrorMetrics.AddFailure("redis", "del_error") - - return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg) -} - -// RedisPipLineError 30021 Redis PipLine 錯誤 -func RedisPipLineError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - mts.AppErrorMetrics.AddFailure("redis", "pip_line_error") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg) -} - -// RedisError 30022 Redis 錯誤 -func RedisError(msg string) *ers.LibError { - // 看需要建立哪些 Metrics - mts.AppErrorMetrics.AddFailure("redis", "error") - - return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg) -} diff --git a/internal/domain/repository/member_status.go b/internal/domain/repository/member_status.go deleted file mode 100644 index 74de198..0000000 --- a/internal/domain/repository/member_status.go +++ /dev/null @@ -1,16 +0,0 @@ -package repository - -import "context" - -// MemberOnlineStatusRepository 會員上限狀態,使用Bitmap -type MemberOnlineStatusRepository interface { - SetMemberOnline(ctx context.Context, uid string) (bool, error) - SetMemberOffline(ctx context.Context, uid string) (bool, error) - IsMemberOnline(ctx context.Context, uid string) (bool, error) - QueryMemberOnlineList(ctx context.Context, uids []string) ([]MemberOnlineStatusResp, error) -} - -type MemberOnlineStatusResp struct { - UID string - Status bool -} diff --git a/internal/domain/repository/token.go b/internal/domain/repository/token.go deleted file mode 100644 index 0d33421..0000000 --- a/internal/domain/repository/token.go +++ /dev/null @@ -1,31 +0,0 @@ -package repository - -import ( - "app-cloudep-permission-server/internal/entity" - "context" - "time" -) - -// TokenRepository token 的 redis 操作 -type TokenRepository interface { - // Create 建立Token - Create(ctx context.Context, token entity.Token) error - // CreateOneTimeToken 建立臨時 Token - CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, dt time.Duration) error - GetAccessTokenByByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) - GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) - GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) - GetAccessTokenCountByUID(uid string) (int, error) - GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) - GetAccessTokenCountByDeviceID(deviceID string) (int, error) - Delete(ctx context.Context, token entity.Token) error - DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error - DeleteAccessTokenByID(ctx context.Context, ids []string) error - DeleteAccessTokensByUID(ctx context.Context, uid string) error - DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error -} - -type DeviceToken struct { - DeviceID string - TokenID string -} diff --git a/internal/domain/usecase/bitmap.go b/internal/domain/usecase/bitmap.go deleted file mode 100644 index 05379b8..0000000 --- a/internal/domain/usecase/bitmap.go +++ /dev/null @@ -1,16 +0,0 @@ -package usecase - -type BitMapUseCase interface { - // SetTrue 設定該 Bit 狀態為 true - SetTrue(bitPos uint32) - // SetFalse 設定該Bit 狀態為 false - SetFalse(bitPos uint32) - // IsTrue 確認是否為真 - IsTrue(bitPos uint32) bool - // Reset 重設 BitMap - Reset() - // ByteSize 最大 Byte 數 - ByteSize() int - // BitSize 最大 Byte * 8 - BitSize() int -} diff --git a/internal/domain/usecase/opa.go b/internal/domain/usecase/opa.go deleted file mode 100644 index 1a1f3a1..0000000 --- a/internal/domain/usecase/opa.go +++ /dev/null @@ -1,47 +0,0 @@ -package usecase - -import ( - "context" -) - -type OpaUseCase interface { - // CheckRBACPermission 確認有無權限 - CheckRBACPermission(ctx context.Context, req CheckReq) (CheckOPAResp, error) - // LoadPolicy 將 Policy 從其他地方加載到 opa 的 policy 當中 - LoadPolicy(ctx context.Context, input []Policy) error - GetPolicy(ctx context.Context) []map[string]any -} - -type CheckReq struct { - ID string - Roles []string - Path string - Method string -} - -type Grant struct { - ID string - Path string - Method string -} - -type Policy struct { - Methods []string `json:"methods"` - Name string `json:"name"` - Path string `json:"path"` - Role string `json:"role"` -} - -type RuleRequest struct { - Method string `json:"method"` - Path string `json:"path"` - Policies []Policy `json:"policies"` - Roles []string `json:"roles"` -} - -type CheckOPAResp struct { - Allow bool `json:"allow"` - PolicyName string `json:"policy_name"` - PlainCode bool `json:"plain_code"` // 是否為明碼顯示 - Request RuleRequest `json:"request"` -} diff --git a/internal/domain/usecase/permission_tree.go b/internal/domain/usecase/permission_tree.go deleted file mode 100644 index 7b13a5d..0000000 --- a/internal/domain/usecase/permission_tree.go +++ /dev/null @@ -1,33 +0,0 @@ -package usecase - -// // PermissionTreeManager 定義一組操作權限樹的接口 -// // 這個名稱說明它是專門負責管理和操作權限樹的管理器 -// type PermissionTreeManager interface { -// // AddPermission 將一個新的權限節點插入到樹中 -// // key 是父節點的ID,value 是要插入的 Permission 資料 -// // 此方法應該能處理節點是否存在於父節點下的情況 -// AddPermission(parentID int64, permission entity.Permission) error -// // FindPermissionByID 根據權限 ID 查詢樹中的某個節點 -// // 如果節點存在,返回對應的 Permission 資料,否則返回 nil -// FindPermissionByID(permissionID int64) (*Permission, error) -// // GetAllParentPermissionIDs 根據傳入的 permissions 列表 -// // 找出每個權限的完整父節點權限 ID 路徑 -// // 例如,如果 B 的父權限是 A,並且給了 B 權限,則返回 A 和 B 的權限 ID -// GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error) -// // GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態 -// // 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態 -// GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error) -// // GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態 -// // 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態 -// GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions -// } -// -// type Permission struct { -// ID int64 `json:"-"` -// Name string `json:"name"` -// HTTPMethod string `json:"http_method"` -// HTTPPath string `json:"http_path"` -// Parent *Permission `json:"-"` -// Children []*Permission `json:"children"` -// PathIDs []int64 `json:"-"` // full path id -// } diff --git a/internal/entity/token.go b/internal/entity/token.go deleted file mode 100644 index 791395a..0000000 --- a/internal/entity/token.go +++ /dev/null @@ -1,50 +0,0 @@ -package entity - -import "time" - -type Token struct { - ID string `json:"id"` - UID string `json:"uid"` - DeviceID string `json:"device_id"` - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - AccessCreateAt time.Time `json:"access_create_at"` - RefreshToken string `json:"refresh_token"` - RefreshExpiresIn int `json:"refresh_expires_in"` - RefreshCreateAt time.Time `json:"refresh_create_at"` -} - -func (t *Token) AccessTokenExpires() time.Duration { - return time.Duration(t.ExpiresIn) * time.Second -} - -func (t *Token) RefreshTokenExpires() time.Duration { - return time.Duration(t.RefreshExpiresIn) * time.Second -} - -func (t *Token) RefreshTokenExpiresUnix() int64 { - return time.Now().Add(t.RefreshTokenExpires()).Unix() -} - -func (t *Token) IsExpires() bool { - return t.AccessCreateAt.Add(t.AccessTokenExpires()).Before(time.Now()) -} - -func (t *Token) RedisExpiredSec() int64 { - sec := time.Unix(int64(t.ExpiresIn), 0).Sub(time.Now().UTC()) - - return int64(sec.Seconds()) -} - -func (t *Token) RedisRefreshExpiredSec() int64 { - sec := time.Unix(int64(t.RefreshExpiresIn), 0).Sub(time.Now().UTC()) - - return int64(sec.Seconds()) -} - -type UIDToken map[string]int64 - -type Ticket struct { - Data any `json:"data"` - Token Token `json:"token"` -} diff --git a/internal/lib/metric/app.go b/internal/lib/metric/app.go deleted file mode 100644 index 59da7ef..0000000 --- a/internal/lib/metric/app.go +++ /dev/null @@ -1,30 +0,0 @@ -package metric - -import ( - "github.com/zeromicro/go-zero/core/metric" -) - -var AppErrorMetrics = NewAppErrMetrics() - -type appErrMetrics struct { - metric.CounterVec -} - -type Metrics interface { - AddFailure(source, reason string) -} - -// NewAppErrMetrics initiate metrics and register to prometheus -func NewAppErrMetrics() Metrics { - return &appErrMetrics{metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: "ark", - Subsystem: "permission", - Name: "permission_app_error_total", - Help: "App defined failure total.", - Labels: []string{"source", "reason"}, - })} -} - -func (m *appErrMetrics) AddFailure(source, reason string) { - m.Inc(source, reason) -} diff --git a/internal/logic/tokenservice/cancel_one_time_token_logic.go b/internal/logic/tokenservice/cancel_one_time_token_logic.go deleted file mode 100644 index 9bb38bf..0000000 --- a/internal/logic/tokenservice/cancel_one_time_token_logic.go +++ /dev/null @@ -1,47 +0,0 @@ -package tokenservicelogic - -import ( - "context" - - ers "code.30cm.net/digimon/library-go/errors" - - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type CancelOneTimeTokenLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewCancelOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelOneTimeTokenLogic { - return &CancelOneTimeTokenLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -type cancelOneTimeTokenReq struct { - Token []string `json:"token" validate:"required"` -} - -// CancelOneTimeToken 取消一次性使用 -func (l *CancelOneTimeTokenLogic) CancelOneTimeToken(in *permission.CancelOneTimeTokenReq) (*permission.OKResp, error) { - // 驗證所需 - if err := l.svcCtx.Validate.ValidateAll(&cancelOneTimeTokenReq{ - Token: in.GetToken(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - - err := l.svcCtx.TokenRedisRepo.DeleteOneTimeToken(l.ctx, in.GetToken(), nil) - if err != nil { - return nil, err - } - - return &permission.OKResp{}, nil -} diff --git a/internal/logic/tokenservice/cancel_token_by_device_i_d_logic.go b/internal/logic/tokenservice/cancel_token_by_device_i_d_logic.go new file mode 100644 index 0000000..99ef5ad --- /dev/null +++ b/internal/logic/tokenservice/cancel_token_by_device_i_d_logic.go @@ -0,0 +1,34 @@ +package tokenservicelogic + +import ( + "context" + + "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" +) + +type CancelTokenByDeviceIDLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCancelTokenByDeviceIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokenByDeviceIDLogic { + return &CancelTokenByDeviceIDLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// CancelTokenByDeviceID 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token +func (l *CancelTokenByDeviceIDLogic) CancelTokenByDeviceID(in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) { + err := l.svcCtx.TokenUseCase.RevokeTokensByDeviceID(l.ctx, in.GetDeviceId()) + if err != nil { + return nil, err + } + + 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 deleted file mode 100644 index 471eb5e..0000000 --- a/internal/logic/tokenservice/cancel_token_by_device_id_logic.go +++ /dev/null @@ -1,45 +0,0 @@ -package tokenservicelogic - -import ( - "context" - - ers "code.30cm.net/digimon/library-go/errors" - - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type CancelTokenByDeviceIdLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewCancelTokenByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelTokenByDeviceIdLogic { - return &CancelTokenByDeviceIdLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token -func (l *CancelTokenByDeviceIdLogic) CancelTokenByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) { - if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByDeviceIdReq{ - DeviceID: in.GetDeviceId(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - - err := l.svcCtx.TokenRedisRepo.DeleteAccessTokensByDeviceID(l.ctx, in.GetDeviceId()) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.DeleteAccessTokensByDeviceID"), - logx.Field("DeviceID", in.GetDeviceId()), - ).Error(err.Error()) - return nil, err - } - return &permission.OKResp{}, nil -} diff --git a/internal/logic/tokenservice/cancel_token_logic.go b/internal/logic/tokenservice/cancel_token_logic.go index 1bbe893..b7d8cc5 100644 --- a/internal/logic/tokenservice/cancel_token_logic.go +++ b/internal/logic/tokenservice/cancel_token_logic.go @@ -3,10 +3,10 @@ package tokenservicelogic import ( "context" - ers "code.30cm.net/digimon/library-go/errors" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" + "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" ) @@ -25,42 +25,12 @@ func NewCancelTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Cance } } -type cancelTokenReq struct { - Token string `json:"token" validate:"required"` -} - // CancelToken 取消 Token,也包含他裡面的 One Time Toke func (l *CancelTokenLogic) CancelToken(in *permission.CancelTokenReq) (*permission.OKResp, error) { - // 驗證所需 - if err := l.svcCtx.Validate.ValidateAll(&cancelTokenReq{ + err := l.svcCtx.TokenUseCase.RevokeToken(l.ctx, usecase.TokenRequest{ Token: in.GetToken(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - - claims, err := parseClaims(in.GetToken(), l.svcCtx.Config.Token.Secret, false) + }) if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "parseClaims"), - ).Error(err.Error()) - return nil, err - } - - token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByID(l.ctx, claims.ID()) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.GetByAccess"), - logx.Field("claims", claims), - ).Error(err.Error()) - return nil, err - } - - err = l.svcCtx.TokenRedisRepo.Delete(l.ctx, token) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.Delete"), - logx.Field("req", token), - ).Error(err.Error()) return nil, err } diff --git a/internal/logic/tokenservice/cancel_tokens_logic.go b/internal/logic/tokenservice/cancel_tokens_logic.go index d1bfd9d..b42498c 100644 --- a/internal/logic/tokenservice/cancel_tokens_logic.go +++ b/internal/logic/tokenservice/cancel_tokens_logic.go @@ -3,10 +3,10 @@ package tokenservicelogic import ( "context" - ers "code.30cm.net/digimon/library-go/errors" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" + "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" ) @@ -27,26 +27,18 @@ 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 *permission.DoTokenByUIDReq) (*permission.OKResp, error) { + req := usecase.RevokeTokensByUIDRequest{} if in.GetUid() != "" { - err := l.svcCtx.TokenRedisRepo.DeleteAccessTokensByUID(l.ctx, in.GetUid()) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.DeleteAccessTokensByUID"), - logx.Field("uid", in.GetUid()), - ).Error(err.Error()) - return nil, ers.ResourceInsufficient(err.Error()) - } + req.UID = in.GetUid() } if len(in.GetIds()) > 0 { - err := l.svcCtx.TokenRedisRepo.DeleteAccessTokenByID(l.ctx, in.GetIds()) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.DeleteAccessTokenByID"), - logx.Field("ids", in.GetIds()), - ).Error(err.Error()) - return nil, ers.ResourceInsufficient(err.Error()) - } + req.IDs = in.GetIds() + } + + err := l.svcCtx.TokenUseCase.RevokeTokensByUID(l.ctx, req) + if err != nil { + return nil, err } return &permission.OKResp{}, nil diff --git a/internal/logic/tokenservice/get_user_tokens_by_device_i_d_logic.go b/internal/logic/tokenservice/get_user_tokens_by_device_i_d_logic.go new file mode 100644 index 0000000..f8fae3b --- /dev/null +++ b/internal/logic/tokenservice/get_user_tokens_by_device_i_d_logic.go @@ -0,0 +1,49 @@ +package tokenservicelogic + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token" + + "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" +) + +type GetUserTokensByDeviceIDLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUserTokensByDeviceIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByDeviceIDLogic { + return &GetUserTokensByDeviceIDLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetUserTokensByDeviceID 取得目前所對應的 DeviceID 所存在的 Tokens +func (l *GetUserTokensByDeviceIDLogic) GetUserTokensByDeviceID(in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) { + id, err := l.svcCtx.TokenUseCase.GetUserTokensByDeviceID(l.ctx, in.GetDeviceId()) + if err != nil { + return nil, err + } + tokenType := token.Bearer + + result := make([]*permission.TokenResp, 0, len(id)) + for _, v := range id { + result = append(result, &permission.TokenResp{ + AccessToken: v.AccessToken, + ExpiresIn: v.ExpiresIn, + RefreshToken: v.RefreshToken, + TokenType: tokenType.ToString(), + }) + } + + return &permission.Tokens{ + Token: result, + }, 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 deleted file mode 100644 index 2281b21..0000000 --- a/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go +++ /dev/null @@ -1,59 +0,0 @@ -package tokenservicelogic - -import ( - "app-cloudep-permission-server/internal/domain" - "context" - - ers "code.30cm.net/digimon/library-go/errors" - - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetUserTokensByDeviceIdLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewGetUserTokensByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByDeviceIdLogic { - return &GetUserTokensByDeviceIdLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -type getUserTokensByDeviceIdReq struct { - DeviceID string `json:"device_id" validate:"required"` -} - -// GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens -func (l *GetUserTokensByDeviceIdLogic) GetUserTokensByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) { - if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByDeviceIdReq{ - DeviceID: in.GetDeviceId(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - - uidTokens, err := l.svcCtx.TokenRedisRepo.GetAccessTokensByDeviceID(l.ctx, in.GetDeviceId()) - if err != nil { - return nil, err - } - - tokens := make([]*permission.TokenResp, 0, len(uidTokens)) - for _, v := range uidTokens { - tokens = append(tokens, &permission.TokenResp{ - AccessToken: v.AccessToken, - TokenType: domain.TokenTypeBearer, - ExpiresIn: int32(v.ExpiresIn), - RefreshToken: v.RefreshToken, - }) - } - - return &permission.Tokens{ - Token: tokens, - }, nil -} diff --git a/internal/logic/tokenservice/get_user_tokens_by_u_i_d_logic.go b/internal/logic/tokenservice/get_user_tokens_by_u_i_d_logic.go new file mode 100644 index 0000000..3ee5b08 --- /dev/null +++ b/internal/logic/tokenservice/get_user_tokens_by_u_i_d_logic.go @@ -0,0 +1,50 @@ +package tokenservicelogic + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token" + + "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" +) + +type GetUserTokensByUIDLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUserTokensByUIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByUIDLogic { + return &GetUserTokensByUIDLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens +func (l *GetUserTokensByUIDLogic) GetUserTokensByUID(in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) { + uid, err := l.svcCtx.TokenUseCase.GetUserTokensByUID(l.ctx, in.GetUid()) + if err != nil { + return nil, err + } + + tokenType := token.Bearer + + result := make([]*permission.TokenResp, 0, len(uid)) + for _, v := range uid { + result = append(result, &permission.TokenResp{ + AccessToken: v.AccessToken, + ExpiresIn: v.ExpiresIn, + RefreshToken: v.RefreshToken, + TokenType: tokenType.ToString(), + }) + } + + return &permission.Tokens{ + Token: result, + }, 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 deleted file mode 100644 index f0435a9..0000000 --- a/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go +++ /dev/null @@ -1,59 +0,0 @@ -package tokenservicelogic - -import ( - "app-cloudep-permission-server/internal/domain" - "context" - - ers "code.30cm.net/digimon/library-go/errors" - - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetUserTokensByUidLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewGetUserTokensByUidLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTokensByUidLogic { - return &GetUserTokensByUidLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -type getUserTokensByUidReq struct { - UID string `json:"uid" validate:"required"` -} - -// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens -func (l *GetUserTokensByUidLogic) GetUserTokensByUid(in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) { - if err := l.svcCtx.Validate.ValidateAll(&getUserTokensByUidReq{ - UID: in.GetUid(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - - uidTokens, err := l.svcCtx.TokenRedisRepo.GetAccessTokensByUID(l.ctx, in.GetUid()) - if err != nil { - return nil, err - } - - tokens := make([]*permission.TokenResp, 0, len(uidTokens)) - for _, v := range uidTokens { - tokens = append(tokens, &permission.TokenResp{ - AccessToken: v.AccessToken, - TokenType: domain.TokenTypeBearer, - ExpiresIn: int32(v.ExpiresIn), - RefreshToken: v.RefreshToken, - }) - } - - return &permission.Tokens{ - Token: tokens, - }, nil -} diff --git a/internal/logic/tokenservice/new_one_time_token_logic.go b/internal/logic/tokenservice/new_one_time_token_logic.go deleted file mode 100644 index 3480c00..0000000 --- a/internal/logic/tokenservice/new_one_time_token_logic.go +++ /dev/null @@ -1,71 +0,0 @@ -package tokenservicelogic - -import ( - "app-cloudep-permission-server/internal/domain" - "app-cloudep-permission-server/internal/entity" - "context" - "time" - - ers "code.30cm.net/digimon/library-go/errors" - "github.com/google/uuid" - - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type NewOneTimeTokenLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewNewOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewOneTimeTokenLogic { - return &NewOneTimeTokenLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// NewOneTimeToken 建立一次性使用,例如:RefreshToken -func (l *NewOneTimeTokenLogic) NewOneTimeToken(in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) { - // 驗證所需 - if err := l.svcCtx.Validate.ValidateAll(&refreshTokenReq{ - Token: in.GetToken(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - - // 驗證Token - claims, err := parseClaims(in.GetToken(), l.svcCtx.Config.Token.Secret, false) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "parseClaims"), - ).Error(err.Error()) - return nil, err - } - - token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByID(l.ctx, claims.ID()) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.GetByAccess"), - logx.Field("claims", claims), - ).Error(err.Error()) - return nil, err - } - - oneTimeToken := generateRefreshToken(uuid.Must(uuid.NewRandom()).String()) - key := domain.TicketKeyPrefix + oneTimeToken - if err = l.svcCtx.TokenRedisRepo.CreateOneTimeToken(l.ctx, key, entity.Ticket{ - Data: claims, - Token: token, - }, time.Minute); err != nil { - return &permission.CreateOneTimeTokenResp{}, err - } - - return &permission.CreateOneTimeTokenResp{ - OneTimeToken: oneTimeToken, - }, nil -} diff --git a/internal/logic/tokenservice/new_token_logic.go b/internal/logic/tokenservice/new_token_logic.go index 52bd853..71d6f72 100644 --- a/internal/logic/tokenservice/new_token_logic.go +++ b/internal/logic/tokenservice/new_token_logic.go @@ -1,17 +1,13 @@ package tokenservicelogic import ( - "app-cloudep-permission-server/internal/config" - "app-cloudep-permission-server/internal/domain" - "app-cloudep-permission-server/internal/entity" "context" "time" - ers "code.30cm.net/digimon/library-go/errors" - "github.com/google/uuid" - - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" + "code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/internal/svc" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" "github.com/zeromicro/go-zero/core/logx" ) @@ -30,110 +26,42 @@ func NewNewTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewToken } } -// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3 -type authorizationReq struct { - GrantType domain.GrantType `json:"grant_type" validate:"required,oneof=password client_credentials refresh_token"` - DeviceID string `json:"device_id"` - Scope string `json:"scope" validate:"required"` - Data map[string]string `json:"data"` - Expires int `json:"expires"` - IsRefreshToken bool `json:"is_refresh_token"` -} - // NewToken 建立一個新的 Token,例如:AccessToken func (l *NewTokenLogic) NewToken(in *permission.AuthorizationReq) (*permission.TokenResp, error) { - data := authorizationReq{ - GrantType: domain.GrantType(in.GetGrantType()), - Scope: in.GetScope(), - DeviceID: in.GetDeviceId(), - Data: in.GetData(), - Expires: int(in.GetExpires()), - IsRefreshToken: in.GetIsRefreshToken(), - } - // 驗證所需 - if err := l.svcCtx.Validate.ValidateAll(&data); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - token, err := newToken(data, l.svcCtx.Config) - if err != nil { - return nil, err + tokenType := token.Bearer + + var expired, refreshExpired int64 + if in.GetExpires() > 0 { + expired = in.GetExpires() + } else { + expired = time.Now().UTC().Add(l.svcCtx.Config.Token.Expired).UnixNano() } - err = l.svcCtx.TokenRedisRepo.Create(l.ctx, *token) + if in.GetRefreshExpire() > 0 { + refreshExpired = in.GetRefreshExpire() + } else { + refreshExpired = time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshExpires).UnixNano() + } + + t, err := l.svcCtx.TokenUseCase.GenerateAccessToken(l.ctx, usecase.GenerateTokenRequest{ + TokenType: tokenType.ToString(), + DeviceID: in.GetDeviceId(), + Scope: in.GetScope(), + Expires: expired, + RefreshExpires: refreshExpired, + Role: in.GetRole(), + Account: in.GetAccount(), + UID: in.GetUid(), + Data: in.Data, + }) if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.Create"), - logx.Field("token", token), - ).Error(err.Error()) return nil, err } return &permission.TokenResp{ - AccessToken: token.AccessToken, - TokenType: domain.TokenTypeBearer, - ExpiresIn: int32(token.ExpiresIn), - RefreshToken: token.RefreshToken, + AccessToken: t.AccessToken, + RefreshToken: t.RefreshToken, + ExpiresIn: t.ExpiresIn, + TokenType: tokenType.ToString(), }, nil } - -func newToken(authReq authorizationReq, cfg config.Config) (*entity.Token, error) { - // 準備建立 Token 所需 - now := time.Now().UTC() - expires := authReq.Expires - refreshExpires := authReq.Expires - if expires <= 0 { - // 將時間加上 300 秒 - sec := time.Duration(cfg.Token.Expired.Seconds()) * time.Second - newTime := now.Add(sec) - // 獲取 Unix 時間戳 - timestamp := newTime.Unix() - expires = int(timestamp) - refreshExpires = expires - } - - // 如果這是一個 Refresh Token 過期時間要比普通的Token 長 - if authReq.IsRefreshToken { - // 將時間加上 300 秒 - sec := time.Duration(cfg.Token.RefreshExpires.Seconds()) * time.Second - newTime := now.Add(sec) - // 獲取 Unix 時間戳 - timestamp := newTime.Unix() - refreshExpires = int(timestamp) - } - - token := entity.Token{ - ID: uuid.Must(uuid.NewRandom()).String(), - DeviceID: authReq.DeviceID, - ExpiresIn: expires, - RefreshExpiresIn: refreshExpires, - AccessCreateAt: now, - RefreshCreateAt: now, - } - - claims := claims(authReq.Data) - claims.SetRole(domain.DefaultRole) - claims.SetID(token.ID) - claims.SetScope(authReq.Scope) - - token.UID = claims.UID() - - if authReq.DeviceID != "" { - claims.SetDeviceID(authReq.DeviceID) - } - - var err error - token.AccessToken, err = generateAccessTokenFunc(token, claims, cfg.Token.Secret) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "generateAccessTokenFunc"), - logx.Field("claims", claims), - ).Error(err.Error()) - return nil, err - } - - if authReq.IsRefreshToken { - token.RefreshToken = generateRefreshTokenFunc(token.AccessToken) - } - - return &token, nil -} diff --git a/internal/logic/tokenservice/refresh_token_logic.go b/internal/logic/tokenservice/refresh_token_logic.go index 1378989..2b1e87d 100644 --- a/internal/logic/tokenservice/refresh_token_logic.go +++ b/internal/logic/tokenservice/refresh_token_logic.go @@ -1,13 +1,14 @@ package tokenservicelogic import ( - "app-cloudep-permission-server/internal/domain" "context" + "time" - ers "code.30cm.net/digimon/library-go/errors" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" + "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" ) @@ -26,83 +27,33 @@ func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Refr } } -type refreshReq struct { - RefreshToken string `json:"grant_type" validate:"required"` - DeviceID string `json:"device_id" validate:"required"` - Scope string `json:"scope" validate:"required"` -} - // RefreshToken 更新目前的token 以及裡面包含的一次性 Token func (l *RefreshTokenLogic) RefreshToken(in *permission.RefreshTokenReq) (*permission.RefreshTokenResp, error) { - // 驗證所需 - if err := l.svcCtx.Validate.ValidateAll(&refreshReq{ - RefreshToken: in.GetToken(), - Scope: in.GetScope(), - DeviceID: in.GetDeviceId(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) + tokenType := token.Bearer + var expired int64 + if in.GetExpires() > 0 { + expired = in.GetExpires() + } else { + expired = time.Now().UTC().Add(l.svcCtx.Config.Token.Expired).UnixNano() } - // step 1 拿看看有沒有這個 refresh token - token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByByOneTimeToken(l.ctx, in.Token) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.GetByRefresh"), - logx.Field("req", in), - ).Error(err.Error()) - return nil, err - } + refreshExpired := time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshExpires).UnixNano() - // 取得 Data - c, err := parseClaims(token.AccessToken, l.svcCtx.Config.Token.Secret, false) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "parseClaims"), - logx.Field("token", token), - ).Error(err.Error()) - return nil, err - } - - // step 2 建立新 token - nt, err := newToken(authorizationReq{ - GrantType: domain.ClientCredentials, + t, err := l.svcCtx.TokenUseCase.RefreshAccessToken(l.ctx, usecase.RefreshTokenRequest{ + Token: in.GetToken(), // refresh token Scope: in.GetScope(), + Expires: expired, + RefreshExpires: refreshExpired, DeviceID: in.GetDeviceId(), - Data: c, - Expires: int(in.GetExpires()), - IsRefreshToken: true, - }, l.svcCtx.Config) + }) if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "newToken"), - logx.Field("req", in), - ).Error(err.Error()) - return nil, err - } - - // 刪除掉舊的 token - err = l.svcCtx.TokenRedisRepo.Delete(l.ctx, token) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.Delete"), - logx.Field("req", token), - ).Error(err.Error()) - return nil, err - } - - err = l.svcCtx.TokenRedisRepo.Create(l.ctx, *nt) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.Create"), - logx.Field("token", token), - ).Error(err.Error()) return nil, err } return &permission.RefreshTokenResp{ - Token: nt.AccessToken, - OneTimeToken: nt.RefreshToken, - ExpiresIn: int64(nt.ExpiresIn), - TokenType: domain.TokenTypeBearer, + Token: t.AccessToken, + OneTimeToken: t.RefreshToken, + ExpiresIn: t.ExpiresIn, + TokenType: tokenType.ToString(), }, nil } diff --git a/internal/logic/tokenservice/utils_claims.go b/internal/logic/tokenservice/utils_claims.go deleted file mode 100644 index 2d59b66..0000000 --- a/internal/logic/tokenservice/utils_claims.go +++ /dev/null @@ -1,55 +0,0 @@ -package tokenservicelogic - -type claims map[string]string - -func (c claims) SetID(id string) { - c["id"] = id -} - -func (c claims) SetRole(role string) { - c["role"] = role -} - -func (c claims) SetDeviceID(deviceID string) { - c["device_id"] = deviceID -} - -func (c claims) SetScope(scope string) { - c["scope"] = scope -} - -func (c claims) Role() string { - role, ok := c["role"] - if !ok { - return "" - } - - return role -} - -func (c claims) ID() string { - id, ok := c["id"] - if !ok { - return "" - } - - return id -} - -func (c claims) DeviceID() string { - deviceID, ok := c["device_id"] - if !ok { - return "" - } - - return deviceID -} - -func (c claims) UID() string { - uid, ok := c["uid"] - if !ok { - return "" - } - - return uid -} diff --git a/internal/logic/tokenservice/utils_jwt.go b/internal/logic/tokenservice/utils_jwt.go deleted file mode 100644 index 57e8315..0000000 --- a/internal/logic/tokenservice/utils_jwt.go +++ /dev/null @@ -1,105 +0,0 @@ -package tokenservicelogic - -import ( - "app-cloudep-permission-server/internal/domain" - "app-cloudep-permission-server/internal/entity" - "bytes" - "crypto/sha256" - "encoding/hex" - "fmt" - "time" - - "github.com/golang-jwt/jwt/v4" -) - -var generateAccessTokenFunc = generateAccessToken -var generateRefreshTokenFunc = generateRefreshToken - -func generateAccessToken(token entity.Token, data any, sign string) (string, error) { - claim := entity.Claims{ - Data: data, - RegisteredClaims: jwt.RegisteredClaims{ - ID: token.ID, - ExpiresAt: jwt.NewNumericDate(time.Unix(int64(token.ExpiresIn), 0)), - Issuer: "permission", - }, - } - - accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claim). - SignedString([]byte(sign)) - if err != nil { - return "", domain.TokenClaimError(err.Error()) - } - - return accessToken, nil -} - -func generateRefreshToken(accessToken string) string { - buf := bytes.NewBufferString(accessToken) - h := sha256.New() - _, _ = h.Write(buf.Bytes()) - - return hex.EncodeToString(h.Sum(nil)) -} - -func parseToken(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) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, domain.TokenUnexpectedSigningErr(fmt.Sprintf("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(token *jwt.Token) (interface{}, 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{}, domain.TokenTokenValidateErr("token valid error") - } - - return claims, nil -} - -func parseClaims(accessToken string, secret string, validate bool) (claims, error) { - claimMap, err := parseToken(accessToken, secret, validate) - if err != nil { - return claims{}, err - } - - claimsData, ok := claimMap["data"].(map[string]any) - if ok { - return convertMap(claimsData), nil - } - - return claims{}, domain.TokenClaimError("get data from claim map error") -} - -func convertMap(input map[string]interface{}) 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 -} diff --git a/internal/logic/tokenservice/validation_token_logic.go b/internal/logic/tokenservice/validation_token_logic.go index 3717f1c..54edde8 100644 --- a/internal/logic/tokenservice/validation_token_logic.go +++ b/internal/logic/tokenservice/validation_token_logic.go @@ -3,10 +3,9 @@ package tokenservicelogic import ( "context" - ers "code.30cm.net/digimon/library-go/errors" - - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/svc" + "code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/internal/svc" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" "github.com/zeromicro/go-zero/core/logx" ) @@ -25,46 +24,27 @@ func NewValidationTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *V } } -type refreshTokenReq struct { - Token string `json:"token" validate:"required"` -} - // ValidationToken 驗證這個 Token 有沒有效 func (l *ValidationTokenLogic) ValidationToken(in *permission.ValidationTokenReq) (*permission.ValidationTokenResp, error) { - // 驗證所需 - if err := l.svcCtx.Validate.ValidateAll(&refreshTokenReq{ + token, err := l.svcCtx.TokenUseCase.VerifyToken(l.ctx, usecase.TokenRequest{ Token: in.GetToken(), - }); err != nil { - return nil, ers.InvalidFormat(err.Error()) - } - claims, err := parseClaims(in.GetToken(), l.svcCtx.Config.Token.Secret, true) + }) if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "parseClaims"), - ).Info(err.Error()) - return nil, err - } - token, err := l.svcCtx.TokenRedisRepo.GetAccessTokenByID(l.ctx, claims.ID()) - if err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "TokenRedisRepo.GetByAccess"), - logx.Field("claims", claims), - ).Error(err.Error()) return nil, err } return &permission.ValidationTokenResp{ Token: &permission.Token{ - Id: token.ID, - Uid: token.UID, - DeviceId: token.DeviceID, - AccessCreateAt: token.AccessCreateAt.Unix(), - AccessToken: token.AccessToken, - ExpiresIn: int32(token.ExpiresIn), - RefreshToken: token.RefreshToken, - RefreshExpiresIn: int32(token.RefreshExpiresIn), - RefreshCreateAt: token.RefreshCreateAt.Unix(), + Id: token.Token.ID, + ClientId: 1, + DeviceId: token.Token.DeviceID, + AccessToken: token.Token.AccessToken, + ExpiresIn: token.Token.ExpiresIn, + AccessCreateAt: token.Token.AccessCreateAt, + RefreshToken: token.Token.RefreshToken, + RefreshExpiresIn: token.Token.RefreshExpiresIn, + RefreshCreateAt: token.Token.RefreshCreateAt, }, - Data: claims, + Data: token.Data, }, nil } diff --git a/internal/mock/model/permission_model.go b/internal/mock/model/permission_model.go deleted file mode 100644 index 47a7c11..0000000 --- a/internal/mock/model/permission_model.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/permission_model.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/permission_model.go -destination=./internal/mock/model/permission_model.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockPermissionModel is a mock of PermissionModel interface. -type MockPermissionModel struct { - ctrl *gomock.Controller - recorder *MockPermissionModelMockRecorder -} - -// MockPermissionModelMockRecorder is the mock recorder for MockPermissionModel. -type MockPermissionModelMockRecorder struct { - mock *MockPermissionModel -} - -// NewMockPermissionModel creates a new mock instance. -func NewMockPermissionModel(ctrl *gomock.Controller) *MockPermissionModel { - mock := &MockPermissionModel{ctrl: ctrl} - mock.recorder = &MockPermissionModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPermissionModel) EXPECT() *MockPermissionModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockPermissionModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockPermissionModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPermissionModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockPermissionModel) FindOne(ctx context.Context, id int64) (*model.Permission, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.Permission) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockPermissionModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockPermissionModel)(nil).FindOne), ctx, id) -} - -// FindOneByName mocks base method. -func (m *MockPermissionModel) FindOneByName(ctx context.Context, name string) (*model.Permission, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByName", ctx, name) - ret0, _ := ret[0].(*model.Permission) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByName indicates an expected call of FindOneByName. -func (mr *MockPermissionModelMockRecorder) FindOneByName(ctx, name any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByName", reflect.TypeOf((*MockPermissionModel)(nil).FindOneByName), ctx, name) -} - -// Insert mocks base method. -func (m *MockPermissionModel) Insert(ctx context.Context, data *model.Permission) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockPermissionModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockPermissionModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockPermissionModel) Update(ctx context.Context, data *model.Permission) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockPermissionModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPermissionModel)(nil).Update), ctx, data) -} diff --git a/internal/mock/model/permission_model_gen.go b/internal/mock/model/permission_model_gen.go deleted file mode 100644 index fcb4d57..0000000 --- a/internal/mock/model/permission_model_gen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/permission_model_gen.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/permission_model_gen.go -destination=./internal/mock/model/permission_model_gen.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockpermissionModel is a mock of permissionModel interface. -type MockpermissionModel struct { - ctrl *gomock.Controller - recorder *MockpermissionModelMockRecorder -} - -// MockpermissionModelMockRecorder is the mock recorder for MockpermissionModel. -type MockpermissionModelMockRecorder struct { - mock *MockpermissionModel -} - -// NewMockpermissionModel creates a new mock instance. -func NewMockpermissionModel(ctrl *gomock.Controller) *MockpermissionModel { - mock := &MockpermissionModel{ctrl: ctrl} - mock.recorder = &MockpermissionModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockpermissionModel) EXPECT() *MockpermissionModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockpermissionModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockpermissionModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockpermissionModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockpermissionModel) FindOne(ctx context.Context, id int64) (*model.Permission, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.Permission) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockpermissionModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockpermissionModel)(nil).FindOne), ctx, id) -} - -// FindOneByName mocks base method. -func (m *MockpermissionModel) FindOneByName(ctx context.Context, name string) (*model.Permission, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByName", ctx, name) - ret0, _ := ret[0].(*model.Permission) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByName indicates an expected call of FindOneByName. -func (mr *MockpermissionModelMockRecorder) FindOneByName(ctx, name any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByName", reflect.TypeOf((*MockpermissionModel)(nil).FindOneByName), ctx, name) -} - -// Insert mocks base method. -func (m *MockpermissionModel) Insert(ctx context.Context, data *model.Permission) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockpermissionModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockpermissionModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockpermissionModel) Update(ctx context.Context, data *model.Permission) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockpermissionModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockpermissionModel)(nil).Update), ctx, data) -} diff --git a/internal/mock/model/role_model.go b/internal/mock/model/role_model.go deleted file mode 100644 index aeb86cd..0000000 --- a/internal/mock/model/role_model.go +++ /dev/null @@ -1,130 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/role_model.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/role_model.go -destination=./internal/mock/model/role_model.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockRoleModel is a mock of RoleModel interface. -type MockRoleModel struct { - ctrl *gomock.Controller - recorder *MockRoleModelMockRecorder -} - -// MockRoleModelMockRecorder is the mock recorder for MockRoleModel. -type MockRoleModelMockRecorder struct { - mock *MockRoleModel -} - -// NewMockRoleModel creates a new mock instance. -func NewMockRoleModel(ctrl *gomock.Controller) *MockRoleModel { - mock := &MockRoleModel{ctrl: ctrl} - mock.recorder = &MockRoleModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockRoleModel) EXPECT() *MockRoleModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockRoleModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockRoleModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRoleModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockRoleModel) FindOne(ctx context.Context, id int64) (*model.Role, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.Role) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockRoleModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockRoleModel)(nil).FindOne), ctx, id) -} - -// FindOneByDisplayName mocks base method. -func (m *MockRoleModel) FindOneByDisplayName(ctx context.Context, displayName string) (*model.Role, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByDisplayName", ctx, displayName) - ret0, _ := ret[0].(*model.Role) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByDisplayName indicates an expected call of FindOneByDisplayName. -func (mr *MockRoleModelMockRecorder) FindOneByDisplayName(ctx, displayName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByDisplayName", reflect.TypeOf((*MockRoleModel)(nil).FindOneByDisplayName), ctx, displayName) -} - -// FindOneByRoleId mocks base method. -func (m *MockRoleModel) FindOneByRoleId(ctx context.Context, roleId string) (*model.Role, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByRoleId", ctx, roleId) - ret0, _ := ret[0].(*model.Role) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByRoleId indicates an expected call of FindOneByRoleId. -func (mr *MockRoleModelMockRecorder) FindOneByRoleId(ctx, roleId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByRoleId", reflect.TypeOf((*MockRoleModel)(nil).FindOneByRoleId), ctx, roleId) -} - -// Insert mocks base method. -func (m *MockRoleModel) Insert(ctx context.Context, data *model.Role) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockRoleModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockRoleModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockRoleModel) Update(ctx context.Context, data *model.Role) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockRoleModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRoleModel)(nil).Update), ctx, data) -} diff --git a/internal/mock/model/role_model_gen.go b/internal/mock/model/role_model_gen.go deleted file mode 100644 index 75f34a2..0000000 --- a/internal/mock/model/role_model_gen.go +++ /dev/null @@ -1,130 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/role_model_gen.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/role_model_gen.go -destination=./internal/mock/model/role_model_gen.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockroleModel is a mock of roleModel interface. -type MockroleModel struct { - ctrl *gomock.Controller - recorder *MockroleModelMockRecorder -} - -// MockroleModelMockRecorder is the mock recorder for MockroleModel. -type MockroleModelMockRecorder struct { - mock *MockroleModel -} - -// NewMockroleModel creates a new mock instance. -func NewMockroleModel(ctrl *gomock.Controller) *MockroleModel { - mock := &MockroleModel{ctrl: ctrl} - mock.recorder = &MockroleModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockroleModel) EXPECT() *MockroleModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockroleModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockroleModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockroleModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockroleModel) FindOne(ctx context.Context, id int64) (*model.Role, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.Role) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockroleModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockroleModel)(nil).FindOne), ctx, id) -} - -// FindOneByDisplayName mocks base method. -func (m *MockroleModel) FindOneByDisplayName(ctx context.Context, displayName string) (*model.Role, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByDisplayName", ctx, displayName) - ret0, _ := ret[0].(*model.Role) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByDisplayName indicates an expected call of FindOneByDisplayName. -func (mr *MockroleModelMockRecorder) FindOneByDisplayName(ctx, displayName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByDisplayName", reflect.TypeOf((*MockroleModel)(nil).FindOneByDisplayName), ctx, displayName) -} - -// FindOneByRoleId mocks base method. -func (m *MockroleModel) FindOneByRoleId(ctx context.Context, roleId string) (*model.Role, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByRoleId", ctx, roleId) - ret0, _ := ret[0].(*model.Role) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByRoleId indicates an expected call of FindOneByRoleId. -func (mr *MockroleModelMockRecorder) FindOneByRoleId(ctx, roleId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByRoleId", reflect.TypeOf((*MockroleModel)(nil).FindOneByRoleId), ctx, roleId) -} - -// Insert mocks base method. -func (m *MockroleModel) Insert(ctx context.Context, data *model.Role) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockroleModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockroleModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockroleModel) Update(ctx context.Context, data *model.Role) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockroleModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockroleModel)(nil).Update), ctx, data) -} diff --git a/internal/mock/model/role_permission_model.go b/internal/mock/model/role_permission_model.go deleted file mode 100644 index b5301c8..0000000 --- a/internal/mock/model/role_permission_model.go +++ /dev/null @@ -1,100 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/role_permission_model.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/role_permission_model.go -destination=./internal/mock/model/role_permission_model.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockRolePermissionModel is a mock of RolePermissionModel interface. -type MockRolePermissionModel struct { - ctrl *gomock.Controller - recorder *MockRolePermissionModelMockRecorder -} - -// MockRolePermissionModelMockRecorder is the mock recorder for MockRolePermissionModel. -type MockRolePermissionModelMockRecorder struct { - mock *MockRolePermissionModel -} - -// NewMockRolePermissionModel creates a new mock instance. -func NewMockRolePermissionModel(ctrl *gomock.Controller) *MockRolePermissionModel { - mock := &MockRolePermissionModel{ctrl: ctrl} - mock.recorder = &MockRolePermissionModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockRolePermissionModel) EXPECT() *MockRolePermissionModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockRolePermissionModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockRolePermissionModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRolePermissionModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockRolePermissionModel) FindOne(ctx context.Context, id int64) (*model.RolePermission, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.RolePermission) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockRolePermissionModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockRolePermissionModel)(nil).FindOne), ctx, id) -} - -// Insert mocks base method. -func (m *MockRolePermissionModel) Insert(ctx context.Context, data *model.RolePermission) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockRolePermissionModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockRolePermissionModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockRolePermissionModel) Update(ctx context.Context, data *model.RolePermission) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockRolePermissionModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRolePermissionModel)(nil).Update), ctx, data) -} diff --git a/internal/mock/model/role_permission_model_gen.go b/internal/mock/model/role_permission_model_gen.go deleted file mode 100644 index 2c620fd..0000000 --- a/internal/mock/model/role_permission_model_gen.go +++ /dev/null @@ -1,100 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/role_permission_model_gen.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/role_permission_model_gen.go -destination=./internal/mock/model/role_permission_model_gen.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockrolePermissionModel is a mock of rolePermissionModel interface. -type MockrolePermissionModel struct { - ctrl *gomock.Controller - recorder *MockrolePermissionModelMockRecorder -} - -// MockrolePermissionModelMockRecorder is the mock recorder for MockrolePermissionModel. -type MockrolePermissionModelMockRecorder struct { - mock *MockrolePermissionModel -} - -// NewMockrolePermissionModel creates a new mock instance. -func NewMockrolePermissionModel(ctrl *gomock.Controller) *MockrolePermissionModel { - mock := &MockrolePermissionModel{ctrl: ctrl} - mock.recorder = &MockrolePermissionModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockrolePermissionModel) EXPECT() *MockrolePermissionModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockrolePermissionModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockrolePermissionModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockrolePermissionModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockrolePermissionModel) FindOne(ctx context.Context, id int64) (*model.RolePermission, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.RolePermission) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockrolePermissionModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockrolePermissionModel)(nil).FindOne), ctx, id) -} - -// Insert mocks base method. -func (m *MockrolePermissionModel) Insert(ctx context.Context, data *model.RolePermission) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockrolePermissionModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockrolePermissionModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockrolePermissionModel) Update(ctx context.Context, data *model.RolePermission) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockrolePermissionModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockrolePermissionModel)(nil).Update), ctx, data) -} diff --git a/internal/mock/model/user_role_model.go b/internal/mock/model/user_role_model.go deleted file mode 100644 index 194927e..0000000 --- a/internal/mock/model/user_role_model.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/user_role_model.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/user_role_model.go -destination=./internal/mock/model/user_role_model.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockUserRoleModel is a mock of UserRoleModel interface. -type MockUserRoleModel struct { - ctrl *gomock.Controller - recorder *MockUserRoleModelMockRecorder -} - -// MockUserRoleModelMockRecorder is the mock recorder for MockUserRoleModel. -type MockUserRoleModelMockRecorder struct { - mock *MockUserRoleModel -} - -// NewMockUserRoleModel creates a new mock instance. -func NewMockUserRoleModel(ctrl *gomock.Controller) *MockUserRoleModel { - mock := &MockUserRoleModel{ctrl: ctrl} - mock.recorder = &MockUserRoleModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockUserRoleModel) EXPECT() *MockUserRoleModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockUserRoleModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockUserRoleModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUserRoleModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockUserRoleModel) FindOne(ctx context.Context, id int64) (*model.UserRole, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.UserRole) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockUserRoleModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockUserRoleModel)(nil).FindOne), ctx, id) -} - -// FindOneByUid mocks base method. -func (m *MockUserRoleModel) FindOneByUid(ctx context.Context, uid string) (*model.UserRole, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByUid", ctx, uid) - ret0, _ := ret[0].(*model.UserRole) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByUid indicates an expected call of FindOneByUid. -func (mr *MockUserRoleModelMockRecorder) FindOneByUid(ctx, uid any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByUid", reflect.TypeOf((*MockUserRoleModel)(nil).FindOneByUid), ctx, uid) -} - -// Insert mocks base method. -func (m *MockUserRoleModel) Insert(ctx context.Context, data *model.UserRole) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockUserRoleModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockUserRoleModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockUserRoleModel) Update(ctx context.Context, data *model.UserRole) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockUserRoleModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUserRoleModel)(nil).Update), ctx, data) -} diff --git a/internal/mock/model/user_role_model_gen.go b/internal/mock/model/user_role_model_gen.go deleted file mode 100644 index 456c290..0000000 --- a/internal/mock/model/user_role_model_gen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/model/user_role_model_gen.go -// -// Generated by this command: -// -// mockgen -source=./internal/model/user_role_model_gen.go -destination=./internal/mock/model/user_role_model_gen.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - model "app-cloudep-permission-server/internal/model" - context "context" - sql "database/sql" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockuserRoleModel is a mock of userRoleModel interface. -type MockuserRoleModel struct { - ctrl *gomock.Controller - recorder *MockuserRoleModelMockRecorder -} - -// MockuserRoleModelMockRecorder is the mock recorder for MockuserRoleModel. -type MockuserRoleModelMockRecorder struct { - mock *MockuserRoleModel -} - -// NewMockuserRoleModel creates a new mock instance. -func NewMockuserRoleModel(ctrl *gomock.Controller) *MockuserRoleModel { - mock := &MockuserRoleModel{ctrl: ctrl} - mock.recorder = &MockuserRoleModelMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockuserRoleModel) EXPECT() *MockuserRoleModelMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockuserRoleModel) Delete(ctx context.Context, id int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockuserRoleModelMockRecorder) Delete(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockuserRoleModel)(nil).Delete), ctx, id) -} - -// FindOne mocks base method. -func (m *MockuserRoleModel) FindOne(ctx context.Context, id int64) (*model.UserRole, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOne", ctx, id) - ret0, _ := ret[0].(*model.UserRole) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOne indicates an expected call of FindOne. -func (mr *MockuserRoleModelMockRecorder) FindOne(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockuserRoleModel)(nil).FindOne), ctx, id) -} - -// FindOneByUid mocks base method. -func (m *MockuserRoleModel) FindOneByUid(ctx context.Context, uid string) (*model.UserRole, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindOneByUid", ctx, uid) - ret0, _ := ret[0].(*model.UserRole) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindOneByUid indicates an expected call of FindOneByUid. -func (mr *MockuserRoleModelMockRecorder) FindOneByUid(ctx, uid any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByUid", reflect.TypeOf((*MockuserRoleModel)(nil).FindOneByUid), ctx, uid) -} - -// Insert mocks base method. -func (m *MockuserRoleModel) Insert(ctx context.Context, data *model.UserRole) (sql.Result, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Insert", ctx, data) - ret0, _ := ret[0].(sql.Result) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Insert indicates an expected call of Insert. -func (mr *MockuserRoleModelMockRecorder) Insert(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockuserRoleModel)(nil).Insert), ctx, data) -} - -// Update mocks base method. -func (m *MockuserRoleModel) Update(ctx context.Context, data *model.UserRole) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, data) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockuserRoleModelMockRecorder) Update(ctx, data any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockuserRoleModel)(nil).Update), ctx, data) -} diff --git a/internal/model/permission_model.go b/internal/model/permission_model.go deleted file mode 100755 index 719486a..0000000 --- a/internal/model/permission_model.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ PermissionModel = (*customPermissionModel)(nil) - -type ( - // PermissionModel is an interface to be customized, add more methods here, - // and implement the added methods in customPermissionModel. - PermissionModel interface { - permissionModel - } - - customPermissionModel struct { - *defaultPermissionModel - } -) - -// NewPermissionModel returns a model for the database table. -func NewPermissionModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) PermissionModel { - return &customPermissionModel{ - defaultPermissionModel: newPermissionModel(conn, c, opts...), - } -} diff --git a/internal/model/permission_model_gen.go b/internal/model/permission_model_gen.go deleted file mode 100755 index 74bd248..0000000 --- a/internal/model/permission_model_gen.go +++ /dev/null @@ -1,157 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - permissionFieldNames = builder.RawFieldNames(&Permission{}) - permissionRows = strings.Join(permissionFieldNames, ",") - permissionRowsExpectAutoSet = strings.Join(stringx.Remove(permissionFieldNames, "`id`"), ",") - permissionRowsWithPlaceHolder = strings.Join(stringx.Remove(permissionFieldNames, "`id`"), "=?,") + "=?" - - cachePermissionIdPrefix = "cache:permission:id:" - cachePermissionNamePrefix = "cache:permission:name:" -) - -type ( - permissionModel interface { - Insert(ctx context.Context, data *Permission) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*Permission, error) - FindOneByName(ctx context.Context, name string) (*Permission, error) - Update(ctx context.Context, data *Permission) error - Delete(ctx context.Context, id int64) error - } - - defaultPermissionModel struct { - sqlc.CachedConn - table string - } - - Permission struct { - Id int64 `db:"id"` // PK - Parent sql.NullInt64 `db:"parent"` - Name string `db:"name"` - HttpMethod string `db:"http_method"` - HttpPath string `db:"http_path"` - Status int64 `db:"status"` // 狀態 1: 啟用, 2: 關閉 - Type int64 `db:"type"` // 狀態 1: 後台, 2: 前台 - CreateTime int64 `db:"create_time"` // 創建時間 - UpdateTime int64 `db:"update_time"` // 更新時間 - } -) - -func newPermissionModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultPermissionModel { - return &defaultPermissionModel{ - CachedConn: sqlc.NewConn(conn, c, opts...), - table: "`permission`", - } -} - -func (m *defaultPermissionModel) withSession(session sqlx.Session) *defaultPermissionModel { - return &defaultPermissionModel{ - CachedConn: m.CachedConn.WithSession(session), - table: "`permission`", - } -} - -func (m *defaultPermissionModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, id) - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, data.Name) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - return conn.ExecCtx(ctx, query, id) - }, permissionIdKey, permissionNameKey) - return err -} - -func (m *defaultPermissionModel) FindOne(ctx context.Context, id int64) (*Permission, error) { - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, id) - var resp Permission - err := m.QueryRowCtx(ctx, &resp, permissionIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", permissionRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultPermissionModel) FindOneByName(ctx context.Context, name string) (*Permission, error) { - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, name) - var resp Permission - err := m.QueryRowIndexCtx(ctx, &resp, permissionNameKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { - query := fmt.Sprintf("select %s from %s where `name` = ? limit 1", permissionRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, name); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultPermissionModel) Insert(ctx context.Context, data *Permission) (sql.Result, error) { - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, data.Id) - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, data.Name) - ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?)", m.table, permissionRowsExpectAutoSet) - return conn.ExecCtx(ctx, query, data.Parent, data.Name, data.HttpMethod, data.HttpPath, data.Status, data.Type, data.CreateTime, data.UpdateTime) - }, permissionIdKey, permissionNameKey) - return ret, err -} - -func (m *defaultPermissionModel) Update(ctx context.Context, newData *Permission) error { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, data.Id) - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, data.Name) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, permissionRowsWithPlaceHolder) - return conn.ExecCtx(ctx, query, newData.Parent, newData.Name, newData.HttpMethod, newData.HttpPath, newData.Status, newData.Type, newData.CreateTime, newData.UpdateTime, newData.Id) - }, permissionIdKey, permissionNameKey) - return err -} - -func (m *defaultPermissionModel) formatPrimary(primary any) string { - return fmt.Sprintf("%s%v", cachePermissionIdPrefix, primary) -} - -func (m *defaultPermissionModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", permissionRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary) -} - -func (m *defaultPermissionModel) tableName() string { - return m.table -} diff --git a/internal/model/role_model.go b/internal/model/role_model.go deleted file mode 100755 index 7c4c733..0000000 --- a/internal/model/role_model.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ RoleModel = (*customRoleModel)(nil) - -type ( - // RoleModel is an interface to be customized, add more methods here, - // and implement the added methods in customRoleModel. - RoleModel interface { - roleModel - } - - customRoleModel struct { - *defaultRoleModel - } -) - -// NewRoleModel returns a model for the database table. -func NewRoleModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) RoleModel { - return &customRoleModel{ - defaultRoleModel: newRoleModel(conn, c, opts...), - } -} diff --git a/internal/model/role_model_gen.go b/internal/model/role_model_gen.go deleted file mode 100755 index 681eb15..0000000 --- a/internal/model/role_model_gen.go +++ /dev/null @@ -1,179 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - roleFieldNames = builder.RawFieldNames(&Role{}) - roleRows = strings.Join(roleFieldNames, ",") - roleRowsExpectAutoSet = strings.Join(stringx.Remove(roleFieldNames, "`id`"), ",") - roleRowsWithPlaceHolder = strings.Join(stringx.Remove(roleFieldNames, "`id`"), "=?,") + "=?" - - cacheRoleIdPrefix = "cache:role:id:" - cacheRoleDisplayNamePrefix = "cache:role:displayName:" - cacheRoleRoleIdPrefix = "cache:role:roleId:" -) - -type ( - roleModel interface { - Insert(ctx context.Context, data *Role) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*Role, error) - FindOneByDisplayName(ctx context.Context, displayName string) (*Role, error) - FindOneByRoleId(ctx context.Context, roleId string) (*Role, error) - Update(ctx context.Context, data *Role) error - Delete(ctx context.Context, id int64) error - } - - defaultRoleModel struct { - sqlc.CachedConn - table string - } - - Role struct { - Id int64 `db:"id"` // PK - RoleId string `db:"role_id"` - DisplayName string `db:"display_name"` // 名稱 - Status int64 `db:"status"` // 狀態 1: 啟用, 2: 禁用 - CreateTime int64 `db:"create_time"` // 創建時間 - UpdateTime int64 `db:"update_time"` // 更新時間 - } -) - -func newRoleModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultRoleModel { - return &defaultRoleModel{ - CachedConn: sqlc.NewConn(conn, c, opts...), - table: "`role`", - } -} - -func (m *defaultRoleModel) withSession(session sqlx.Session) *defaultRoleModel { - return &defaultRoleModel{ - CachedConn: m.CachedConn.WithSession(session), - table: "`role`", - } -} - -func (m *defaultRoleModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - roleDisplayNameKey := fmt.Sprintf("%s%v", cacheRoleDisplayNamePrefix, data.DisplayName) - roleIdKey := fmt.Sprintf("%s%v", cacheRoleIdPrefix, id) - roleRoleIdKey := fmt.Sprintf("%s%v", cacheRoleRoleIdPrefix, data.RoleId) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - return conn.ExecCtx(ctx, query, id) - }, roleDisplayNameKey, roleIdKey, roleRoleIdKey) - return err -} - -func (m *defaultRoleModel) FindOne(ctx context.Context, id int64) (*Role, error) { - roleIdKey := fmt.Sprintf("%s%v", cacheRoleIdPrefix, id) - var resp Role - err := m.QueryRowCtx(ctx, &resp, roleIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", roleRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultRoleModel) FindOneByDisplayName(ctx context.Context, displayName string) (*Role, error) { - roleDisplayNameKey := fmt.Sprintf("%s%v", cacheRoleDisplayNamePrefix, displayName) - var resp Role - err := m.QueryRowIndexCtx(ctx, &resp, roleDisplayNameKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { - query := fmt.Sprintf("select %s from %s where `display_name` = ? limit 1", roleRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, displayName); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultRoleModel) FindOneByRoleId(ctx context.Context, roleId string) (*Role, error) { - roleRoleIdKey := fmt.Sprintf("%s%v", cacheRoleRoleIdPrefix, roleId) - var resp Role - err := m.QueryRowIndexCtx(ctx, &resp, roleRoleIdKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { - query := fmt.Sprintf("select %s from %s where `role_id` = ? limit 1", roleRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, roleId); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultRoleModel) Insert(ctx context.Context, data *Role) (sql.Result, error) { - roleDisplayNameKey := fmt.Sprintf("%s%v", cacheRoleDisplayNamePrefix, data.DisplayName) - roleIdKey := fmt.Sprintf("%s%v", cacheRoleIdPrefix, data.Id) - roleRoleIdKey := fmt.Sprintf("%s%v", cacheRoleRoleIdPrefix, data.RoleId) - ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, roleRowsExpectAutoSet) - return conn.ExecCtx(ctx, query, data.RoleId, data.DisplayName, data.Status, data.CreateTime, data.UpdateTime) - }, roleDisplayNameKey, roleIdKey, roleRoleIdKey) - return ret, err -} - -func (m *defaultRoleModel) Update(ctx context.Context, newData *Role) error { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - - roleDisplayNameKey := fmt.Sprintf("%s%v", cacheRoleDisplayNamePrefix, data.DisplayName) - roleIdKey := fmt.Sprintf("%s%v", cacheRoleIdPrefix, data.Id) - roleRoleIdKey := fmt.Sprintf("%s%v", cacheRoleRoleIdPrefix, data.RoleId) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, roleRowsWithPlaceHolder) - return conn.ExecCtx(ctx, query, newData.RoleId, newData.DisplayName, newData.Status, newData.CreateTime, newData.UpdateTime, newData.Id) - }, roleDisplayNameKey, roleIdKey, roleRoleIdKey) - return err -} - -func (m *defaultRoleModel) formatPrimary(primary any) string { - return fmt.Sprintf("%s%v", cacheRoleIdPrefix, primary) -} - -func (m *defaultRoleModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", roleRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary) -} - -func (m *defaultRoleModel) tableName() string { - return m.table -} diff --git a/internal/model/role_permission_model.go b/internal/model/role_permission_model.go deleted file mode 100755 index d1d8e30..0000000 --- a/internal/model/role_permission_model.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ RolePermissionModel = (*customRolePermissionModel)(nil) - -type ( - // RolePermissionModel is an interface to be customized, add more methods here, - // and implement the added methods in customRolePermissionModel. - RolePermissionModel interface { - rolePermissionModel - } - - customRolePermissionModel struct { - *defaultRolePermissionModel - } -) - -// NewRolePermissionModel returns a model for the database table. -func NewRolePermissionModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) RolePermissionModel { - return &customRolePermissionModel{ - defaultRolePermissionModel: newRolePermissionModel(conn, c, opts...), - } -} diff --git a/internal/model/role_permission_model_gen.go b/internal/model/role_permission_model_gen.go deleted file mode 100755 index 1a0dabe..0000000 --- a/internal/model/role_permission_model_gen.go +++ /dev/null @@ -1,118 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - rolePermissionFieldNames = builder.RawFieldNames(&RolePermission{}) - rolePermissionRows = strings.Join(rolePermissionFieldNames, ",") - rolePermissionRowsExpectAutoSet = strings.Join(stringx.Remove(rolePermissionFieldNames, "`id`"), ",") - rolePermissionRowsWithPlaceHolder = strings.Join(stringx.Remove(rolePermissionFieldNames, "`id`"), "=?,") + "=?" - - cacheRolePermissionIdPrefix = "cache:rolePermission:id:" -) - -type ( - rolePermissionModel interface { - Insert(ctx context.Context, data *RolePermission) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*RolePermission, error) - Update(ctx context.Context, data *RolePermission) error - Delete(ctx context.Context, id int64) error - } - - defaultRolePermissionModel struct { - sqlc.CachedConn - table string - } - - RolePermission struct { - Id int64 `db:"id"` // PK - RoleId sql.NullInt64 `db:"role_id"` // role.id - PermissionId sql.NullInt64 `db:"permission_id"` // permission.id - CreateTime int64 `db:"create_time"` // 創建時間 - UpdateTime int64 `db:"update_time"` // 更新時間 - } -) - -func newRolePermissionModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultRolePermissionModel { - return &defaultRolePermissionModel{ - CachedConn: sqlc.NewConn(conn, c, opts...), - table: "`role_permission`", - } -} - -func (m *defaultRolePermissionModel) withSession(session sqlx.Session) *defaultRolePermissionModel { - return &defaultRolePermissionModel{ - CachedConn: m.CachedConn.WithSession(session), - table: "`role_permission`", - } -} - -func (m *defaultRolePermissionModel) Delete(ctx context.Context, id int64) error { - rolePermissionIdKey := fmt.Sprintf("%s%v", cacheRolePermissionIdPrefix, id) - _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - return conn.ExecCtx(ctx, query, id) - }, rolePermissionIdKey) - return err -} - -func (m *defaultRolePermissionModel) FindOne(ctx context.Context, id int64) (*RolePermission, error) { - rolePermissionIdKey := fmt.Sprintf("%s%v", cacheRolePermissionIdPrefix, id) - var resp RolePermission - err := m.QueryRowCtx(ctx, &resp, rolePermissionIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", rolePermissionRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultRolePermissionModel) Insert(ctx context.Context, data *RolePermission) (sql.Result, error) { - rolePermissionIdKey := fmt.Sprintf("%s%v", cacheRolePermissionIdPrefix, data.Id) - ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?)", m.table, rolePermissionRowsExpectAutoSet) - return conn.ExecCtx(ctx, query, data.RoleId, data.PermissionId, data.CreateTime, data.UpdateTime) - }, rolePermissionIdKey) - return ret, err -} - -func (m *defaultRolePermissionModel) Update(ctx context.Context, data *RolePermission) error { - rolePermissionIdKey := fmt.Sprintf("%s%v", cacheRolePermissionIdPrefix, data.Id) - _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, rolePermissionRowsWithPlaceHolder) - return conn.ExecCtx(ctx, query, data.RoleId, data.PermissionId, data.CreateTime, data.UpdateTime, data.Id) - }, rolePermissionIdKey) - return err -} - -func (m *defaultRolePermissionModel) formatPrimary(primary any) string { - return fmt.Sprintf("%s%v", cacheRolePermissionIdPrefix, primary) -} - -func (m *defaultRolePermissionModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", rolePermissionRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary) -} - -func (m *defaultRolePermissionModel) tableName() string { - return m.table -} diff --git a/internal/model/user_role_model.go b/internal/model/user_role_model.go deleted file mode 100755 index 4f2427f..0000000 --- a/internal/model/user_role_model.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ UserRoleModel = (*customUserRoleModel)(nil) - -type ( - // UserRoleModel is an interface to be customized, add more methods here, - // and implement the added methods in customUserRoleModel. - UserRoleModel interface { - userRoleModel - } - - customUserRoleModel struct { - *defaultUserRoleModel - } -) - -// NewUserRoleModel returns a model for the database table. -func NewUserRoleModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) UserRoleModel { - return &customUserRoleModel{ - defaultUserRoleModel: newUserRoleModel(conn, c, opts...), - } -} diff --git a/internal/model/user_role_model_gen.go b/internal/model/user_role_model_gen.go deleted file mode 100755 index 1a4698d..0000000 --- a/internal/model/user_role_model_gen.go +++ /dev/null @@ -1,155 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - userRoleFieldNames = builder.RawFieldNames(&UserRole{}) - userRoleRows = strings.Join(userRoleFieldNames, ",") - userRoleRowsExpectAutoSet = strings.Join(stringx.Remove(userRoleFieldNames, "`id`"), ",") - userRoleRowsWithPlaceHolder = strings.Join(stringx.Remove(userRoleFieldNames, "`id`"), "=?,") + "=?" - - cacheUserRoleIdPrefix = "cache:userRole:id:" - cacheUserRoleUidPrefix = "cache:userRole:uid:" -) - -type ( - userRoleModel interface { - Insert(ctx context.Context, data *UserRole) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*UserRole, error) - FindOneByUid(ctx context.Context, uid string) (*UserRole, error) - Update(ctx context.Context, data *UserRole) error - Delete(ctx context.Context, id int64) error - } - - defaultUserRoleModel struct { - sqlc.CachedConn - table string - } - - UserRole struct { - Id int64 `db:"id"` // PK - Brand string `db:"brand"` - Uid string `db:"uid"` - RoleId string `db:"role_id"` - Status int64 `db:"status"` // 狀態 1: 啟用, 2: 禁用 - CreateTime int64 `db:"create_time"` // 創建時間 - UpdateTime int64 `db:"update_time"` // 更新時間 - } -) - -func newUserRoleModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultUserRoleModel { - return &defaultUserRoleModel{ - CachedConn: sqlc.NewConn(conn, c, opts...), - table: "`user_role`", - } -} - -func (m *defaultUserRoleModel) withSession(session sqlx.Session) *defaultUserRoleModel { - return &defaultUserRoleModel{ - CachedConn: m.CachedConn.WithSession(session), - table: "`user_role`", - } -} - -func (m *defaultUserRoleModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - userRoleIdKey := fmt.Sprintf("%s%v", cacheUserRoleIdPrefix, id) - userRoleUidKey := fmt.Sprintf("%s%v", cacheUserRoleUidPrefix, data.Uid) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - return conn.ExecCtx(ctx, query, id) - }, userRoleIdKey, userRoleUidKey) - return err -} - -func (m *defaultUserRoleModel) FindOne(ctx context.Context, id int64) (*UserRole, error) { - userRoleIdKey := fmt.Sprintf("%s%v", cacheUserRoleIdPrefix, id) - var resp UserRole - err := m.QueryRowCtx(ctx, &resp, userRoleIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRoleRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultUserRoleModel) FindOneByUid(ctx context.Context, uid string) (*UserRole, error) { - userRoleUidKey := fmt.Sprintf("%s%v", cacheUserRoleUidPrefix, uid) - var resp UserRole - err := m.QueryRowIndexCtx(ctx, &resp, userRoleUidKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { - query := fmt.Sprintf("select %s from %s where `uid` = ? limit 1", userRoleRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, uid); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultUserRoleModel) Insert(ctx context.Context, data *UserRole) (sql.Result, error) { - userRoleIdKey := fmt.Sprintf("%s%v", cacheUserRoleIdPrefix, data.Id) - userRoleUidKey := fmt.Sprintf("%s%v", cacheUserRoleUidPrefix, data.Uid) - ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, userRoleRowsExpectAutoSet) - return conn.ExecCtx(ctx, query, data.Brand, data.Uid, data.RoleId, data.Status, data.CreateTime, data.UpdateTime) - }, userRoleIdKey, userRoleUidKey) - return ret, err -} - -func (m *defaultUserRoleModel) Update(ctx context.Context, newData *UserRole) error { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - - userRoleIdKey := fmt.Sprintf("%s%v", cacheUserRoleIdPrefix, data.Id) - userRoleUidKey := fmt.Sprintf("%s%v", cacheUserRoleUidPrefix, data.Uid) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRoleRowsWithPlaceHolder) - return conn.ExecCtx(ctx, query, newData.Brand, newData.Uid, newData.RoleId, newData.Status, newData.CreateTime, newData.UpdateTime, newData.Id) - }, userRoleIdKey, userRoleUidKey) - return err -} - -func (m *defaultUserRoleModel) formatPrimary(primary any) string { - return fmt.Sprintf("%s%v", cacheUserRoleIdPrefix, primary) -} - -func (m *defaultUserRoleModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRoleRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary) -} - -func (m *defaultUserRoleModel) tableName() string { - return m.table -} diff --git a/internal/model/vars.go b/internal/model/vars.go deleted file mode 100644 index 69ca814..0000000 --- a/internal/model/vars.go +++ /dev/null @@ -1,5 +0,0 @@ -package model - -import "github.com/zeromicro/go-zero/core/stores/sqlx" - -var ErrNotFound = sqlx.ErrNotFound diff --git a/internal/repository/member_status.go b/internal/repository/member_status.go deleted file mode 100644 index adcf99e..0000000 --- a/internal/repository/member_status.go +++ /dev/null @@ -1,104 +0,0 @@ -package repository - -import ( - "app-cloudep-permission-server/internal/domain/repository" - "code.30cm.net/digimon/library-go/utils/invited_code" - "context" - "fmt" - "github.com/zeromicro/go-zero/core/stores/redis" -) - -type MemberOnlineStatusRepositoryParam struct { - Store *redis.Redis `name:"redis"` -} - -type memberOnlineStatusRepository struct { - store *redis.Redis -} - -// 使用 UID 計算 Bitmap 的 offset -func (t *memberOnlineStatusRepository) uidToOffset(uid string) (int64, error) { - converter := invited_code.MustConverter(10, invited_code.DefaultCodeLen, invited_code.ConvertTable) - - // 將 UID 轉換為整數型,並減去基礎 UID (1000000) - uidInt, err := converter.DecodeFromCode(uid) - if err != nil { - return 0, fmt.Errorf("invalid UID: %w", err) - } - // 以 1000000 作為基準 - baseUID := invited_code.InitAutoId - if uidInt < int64(baseUID) { - return 0, fmt.Errorf("UID smaller than base: %d", baseUID) - } - - return uidInt - int64(baseUID), nil -} - -func (t *memberOnlineStatusRepository) SetMemberOnline(ctx context.Context, uid string) (bool, error) { - offset, err := t.uidToOffset(uid) - if err != nil { - return false, err - } - - // 使用 SET BIT 設置對應的位為 1 - _, err = t.store.SetBit("member_status", offset, 1) - if err != nil { - return false, fmt.Errorf("failed to set member online: %w", err) - } - - return true, nil -} - -func (t *memberOnlineStatusRepository) SetMemberOffline(ctx context.Context, uid string) (bool, error) { - offset, err := t.uidToOffset(uid) - if err != nil { - return false, err - } - - // 使用 SET BIT 設置對應的位為 1 - _, err = t.store.SetBit("member_status", offset, 0) - if err != nil { - return false, fmt.Errorf("failed to set member offline: %w", err) - } - - return true, nil -} - -func (t *memberOnlineStatusRepository) IsMemberOnline(ctx context.Context, uid string) (bool, error) { - offset, err := t.uidToOffset(uid) - if err != nil { - return false, err - } - - // 使用 GET BIT 獲取對應的位,1 表示在線,0 表示離線 - status, err := t.store.GetBit("member_status", offset) - if err != nil { - return false, fmt.Errorf("failed to get member status: %w", err) - } - - return status == 1, nil -} - -func (t *memberOnlineStatusRepository) QueryMemberOnlineList(ctx context.Context, uids []string) ([]repository.MemberOnlineStatusResp, error) { - statusMap := make([]repository.MemberOnlineStatusResp, 0, len(uids)) - - // 遍歷所有用戶的 UID,查詢他們的在線狀態 - for _, uid := range uids { - isOnline, err := t.IsMemberOnline(ctx, uid) - if err != nil { - return nil, fmt.Errorf("failed to query member status for UID %s: %w", uid, err) - } - statusMap = append(statusMap, repository.MemberOnlineStatusResp{ - UID: uid, - Status: isOnline, - }) - } - - return statusMap, nil -} - -func NewMemberOnlineStatusRepository(param MemberOnlineStatusRepositoryParam) repository.MemberOnlineStatusRepository { - return &memberOnlineStatusRepository{ - store: param.Store, - } -} diff --git a/internal/repository/token.go b/internal/repository/token.go deleted file mode 100644 index 7af977a..0000000 --- a/internal/repository/token.go +++ /dev/null @@ -1,312 +0,0 @@ -package repository - -import ( - "app-cloudep-permission-server/internal/domain" - "app-cloudep-permission-server/internal/domain/repository" - "app-cloudep-permission-server/internal/entity" - "context" - "encoding/json" - "errors" - "fmt" - "time" - - ers "code.30cm.net/digimon/library-go/errors" - - "github.com/zeromicro/go-zero/core/stores/redis" -) - -type TokenRepositoryParam struct { - Store *redis.Redis `name:"redis"` -} - -type tokenRepository struct { - store *redis.Redis -} - -func NewTokenRepository(param TokenRepositoryParam) repository.TokenRepository { - return &tokenRepository{ - store: param.Store, - } -} - -func (t *tokenRepository) Create(ctx context.Context, token entity.Token) error { - body, err := json.Marshal(token) - if err != nil { - return ers.ArkInternal("json.Marshal token error", err.Error()) - } - if err := t.store.Pipelined(func(tx redis.Pipeliner) error { - refreshTTL := time.Duration(token.RedisRefreshExpiredSec()) * time.Second - - if err := t.setToken(ctx, tx, token, body, refreshTTL); err != nil { - return err - } - - if err := t.setRefreshToken(ctx, tx, token, refreshTTL); err != nil { - return err - } - - return t.setRelation(ctx, tx, token.UID, token.DeviceID, token.ID, refreshTTL) - }); err != nil { - return repository.RedisPipLineError(err.Error()) - } - - return nil -} - -func (t *tokenRepository) Delete(ctx context.Context, token entity.Token) error { - keys := []string{ - domain.GetAccessTokenRedisKey(token.ID), - domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString(), - } - - if err := t.deleteKeys(ctx, keys...); err != nil { - return repository.RedisPipLineError(err.Error()) - } - - _, _ = t.store.Srem(domain.DeviceTokenRedisKey.With(token.DeviceID).ToString(), token.ID) - _, _ = t.store.Srem(domain.UIDTokenRedisKey.With(token.UID).ToString(), token.ID) - - return nil -} - -func (t *tokenRepository) GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) { - token, err := t.get(ctx, domain.GetAccessTokenRedisKey(id)) - if err != nil { - return entity.Token{}, err - } - - return token, nil -} - -func (t *tokenRepository) DeleteAccessTokensByUID(ctx context.Context, uid string) error { - tokens, err := t.GetAccessTokensByUID(ctx, uid) - if err != nil { - return err - } - - for _, token := range tokens { - if err := t.Delete(ctx, token); err != nil { - return err - } - } - - return nil -} - -func (t *tokenRepository) DeleteAccessTokenByID(ctx context.Context, ids []string) error { - for _, tokenID := range ids { - token, err := t.GetAccessTokenByID(ctx, tokenID) - if err != nil { - continue - } - - keys := []string{ - domain.GetAccessTokenRedisKey(token.ID), - domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString(), - } - - if err := t.deleteKeys(ctx, keys...); err != nil { - continue - } - - _, _ = t.store.Srem(domain.DeviceTokenRedisKey.With(token.DeviceID).ToString(), token.ID) - _, _ = t.store.Srem(domain.UIDTokenRedisKey.With(token.UID).ToString(), token.ID) - } - - return nil -} - -func (t *tokenRepository) GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) { - return t.getTokensBySet(ctx, domain.GetUIDTokenRedisKey(uid)) -} - -func (t *tokenRepository) GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) { - return t.getTokensBySet(ctx, domain.DeviceTokenRedisKey.With(deviceID).ToString()) -} - -func (t *tokenRepository) DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error { - - tokens, err := t.GetAccessTokensByDeviceID(ctx, deviceID) - if err != nil { - return repository.RedisDelError(fmt.Sprintf("GetAccessTokensByDeviceID error: %v", err)) - } - - var keys []string - for _, token := range tokens { - keys = append(keys, domain.GetAccessTokenRedisKey(token.ID)) - keys = append(keys, domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString()) - - } - - err = t.store.Pipelined(func(tx redis.Pipeliner) error { - for _, token := range tokens { - _, _ = t.store.Srem(domain.UIDTokenRedisKey.With(token.UID).ToString(), token.ID) - } - return nil - }) - if err != nil { - return err - } - - if err := t.deleteKeys(ctx, keys...); err != nil { - return err - } - - _, err = t.store.Del(domain.DeviceTokenRedisKey.With(deviceID).ToString()) - return err -} - -func (t *tokenRepository) GetAccessTokenCountByDeviceID(deviceID string) (int, error) { - return t.getCountBySet(domain.DeviceTokenRedisKey.With(deviceID).ToString()) -} - -func (t *tokenRepository) GetAccessTokenCountByUID(uid string) (int, error) { - return t.getCountBySet(domain.UIDTokenRedisKey.With(uid).ToString()) -} - -func (t *tokenRepository) GetAccessTokenByByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) { - id, err := t.store.Get(domain.RefreshTokenRedisKey.With(oneTimeToken).ToString()) - if err != nil { - return entity.Token{}, repository.RedisError(fmt.Sprintf("GetAccessTokenByByOneTimeToken store.Get error: %s", err.Error())) - } - - if id == "" { - return entity.Token{}, ers.ResourceNotFound("token key not found in redis", domain.RefreshTokenRedisKey.With(oneTimeToken).ToString()) - } - - return t.GetAccessTokenByID(ctx, id) -} - -func (t *tokenRepository) DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error { - var keys []string - - for _, id := range ids { - keys = append(keys, domain.RefreshTokenRedisKey.With(id).ToString()) - } - - for _, token := range tokens { - keys = append(keys, domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString()) - } - - return t.deleteKeys(ctx, keys...) -} - -func (t *tokenRepository) CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, expires time.Duration) error { - body, err := json.Marshal(ticket) - if err != nil { - return ers.InvalidFormat("CreateOneTimeToken json.Marshal error", err.Error()) - } - - _, err = t.store.SetnxEx(domain.RefreshTokenRedisKey.With(key).ToString(), string(body), int(expires.Seconds())) - if err != nil { - return repository.RedisError(fmt.Sprintf("CreateOneTimeToken store.SetnxEx error: %s", err.Error())) - } - - return nil -} - -// -------------------- Private area -------------------- - -func (t *tokenRepository) get(ctx context.Context, key string) (entity.Token, error) { - body, err := t.store.GetCtx(ctx, key) - if err != nil { - return entity.Token{}, repository.RedisError(fmt.Sprintf("token %s not found in redis: %s", key, err.Error())) - } - - if body == "" { - return entity.Token{}, ers.ResourceNotFound("this token not found") - } - - var token entity.Token - if err := json.Unmarshal([]byte(body), &token); err != nil { - return entity.Token{}, ers.ArkInternal("json.Unmarshal token error", err.Error()) - } - - return token, nil -} - -func (t *tokenRepository) setToken(ctx context.Context, tx redis.Pipeliner, token entity.Token, body []byte, ttl time.Duration) error { - return tx.Set(ctx, domain.GetAccessTokenRedisKey(token.ID), body, ttl).Err() -} - -func (t *tokenRepository) setRefreshToken(ctx context.Context, tx redis.Pipeliner, token entity.Token, ttl time.Duration) error { - if token.RefreshToken != "" { - return tx.Set(ctx, domain.RefreshTokenRedisKey.With(token.RefreshToken).ToString(), token.ID, ttl).Err() - } - return nil -} - -func (t *tokenRepository) setRelation(ctx context.Context, tx redis.Pipeliner, uid, deviceID, tokenID string, ttl time.Duration) error { - if err := tx.SAdd(ctx, domain.UIDTokenRedisKey.With(uid).ToString(), tokenID).Err(); err != nil { - return err - } - - // 設置 UID 鍵的過期時間 - if err := tx.Expire(ctx, domain.UIDTokenRedisKey.With(uid).ToString(), ttl).Err(); err != nil { - return err - } - - if err := tx.SAdd(ctx, domain.DeviceTokenRedisKey.With(deviceID).ToString(), tokenID).Err(); err != nil { - return err - } - - // 設置 deviceID 鍵的過期時間 - if err := tx.Expire(ctx, domain.DeviceTokenRedisKey.With(deviceID).ToString(), ttl).Err(); err != nil { - return err - } - - return nil -} - -func (t *tokenRepository) deleteKeys(ctx context.Context, keys ...string) error { - return t.store.Pipelined(func(tx redis.Pipeliner) error { - for _, key := range keys { - if err := tx.Del(ctx, key).Err(); err != nil { - return repository.RedisDelError(fmt.Sprintf("store.Del key error: %v", err)) - } - } - return nil - }) -} - -func (t *tokenRepository) getTokensBySet(ctx context.Context, setKey string) ([]entity.Token, error) { - ids, err := t.store.Smembers(setKey) - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, nil - } - return nil, repository.RedisError(fmt.Sprintf("getTokensBySet store.Get %s error: %v", setKey, err.Error())) - } - - var tokens []entity.Token - var deleteTokens []string - now := time.Now().Unix() - for _, id := range ids { - token, err := t.get(ctx, domain.GetAccessTokenRedisKey(id)) - if err != nil { - deleteTokens = append(deleteTokens, id) - continue - } - - if int64(token.ExpiresIn) < now { - deleteTokens = append(deleteTokens, id) - continue - } - - tokens = append(tokens, token) - } - - if len(deleteTokens) > 0 { - _ = t.DeleteAccessTokenByID(ctx, deleteTokens) - } - - return tokens, nil -} - -func (t *tokenRepository) getCountBySet(setKey string) (int, error) { - count, err := t.store.Scard(setKey) - if err != nil { - return 0, err - } - return int(count), nil -} diff --git a/internal/server/tokenservice/token_service_server.go b/internal/server/tokenservice/token_service_server.go index c6d0352..cbdaf94 100644 --- a/internal/server/tokenservice/token_service_server.go +++ b/internal/server/tokenservice/token_service_server.go @@ -1,4 +1,5 @@ // Code generated by goctl. DO NOT EDIT. +// goctl 1.7.3 // Source: permission.proto package server @@ -6,9 +7,9 @@ package server import ( "context" - "app-cloudep-permission-server/gen_result/pb/permission" - tokenservicelogic "app-cloudep-permission-server/internal/logic/tokenservice" - "app-cloudep-permission-server/internal/svc" + "code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/internal/logic/tokenservice" + "code.30cm.net/digimon/app-cloudep-permission-server/internal/svc" ) type TokenServiceServer struct { @@ -52,32 +53,20 @@ func (s *TokenServiceServer) CancelTokens(ctx context.Context, in *permission.Do return l.CancelTokens(in) } -// CancelTokenByDeviceId 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token -func (s *TokenServiceServer) CancelTokenByDeviceId(ctx context.Context, in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) { - l := tokenservicelogic.NewCancelTokenByDeviceIdLogic(ctx, s.svcCtx) - return l.CancelTokenByDeviceId(in) +// CancelTokenByDeviceID 取消 Token, 從 Device 視角出發,可以選,登出這個Device 下所有 token ,登出這個Device 下指定token +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 *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) { - l := tokenservicelogic.NewGetUserTokensByDeviceIdLogic(ctx, s.svcCtx) - return l.GetUserTokensByDeviceId(in) +// GetUserTokensByDeviceID 取得目前所對應的 DeviceID 所存在的 Tokens +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 *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 *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) { - l := tokenservicelogic.NewNewOneTimeTokenLogic(ctx, s.svcCtx) - return l.NewOneTimeToken(in) -} - -// CancelOneTimeToken 取消一次性使用 -func (s *TokenServiceServer) CancelOneTimeToken(ctx context.Context, in *permission.CancelOneTimeTokenReq) (*permission.OKResp, error) { - l := tokenservicelogic.NewCancelOneTimeTokenLogic(ctx, s.svcCtx) - return l.CancelOneTimeToken(in) +// GetUserTokensByUID 取得目前所對應的 UID 所存在的 Tokens +func (s *TokenServiceServer) GetUserTokensByUID(ctx context.Context, in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) { + l := tokenservicelogic.NewGetUserTokensByUIDLogic(ctx, s.svcCtx) + return l.GetUserTokensByUID(in) } diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 17c9abe..11fddce 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -1,42 +1,32 @@ package svc import ( - "app-cloudep-permission-server/internal/config" - "app-cloudep-permission-server/internal/domain/repository" - repo "app-cloudep-permission-server/internal/repository" - - ers "code.30cm.net/digimon/library-go/errors" - "code.30cm.net/digimon/library-go/errors/code" - vi "code.30cm.net/digimon/library-go/validator" + "code.30cm.net/digimon/app-cloudep-permission-server/internal/config" + duc "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/repository" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/usecase" "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/core/stores/sqlx" ) type ServiceContext struct { - Config config.Config - Conn sqlx.SqlConn - - Validate vi.Validate - Redis redis.Redis - TokenRedisRepo repository.TokenRepository + Config config.Config + TokenUseCase duc.TokenUseCase } func NewServiceContext(c config.Config) *ServiceContext { - ers.Scope = code.CloudEPPermission - sqlConn := sqlx.NewMysql(c.DB.DsnString) - - newRedis, err := redis.NewRedis(c.RedisCluster, redis.Cluster()) + newRedis, err := redis.NewRedis(c.RedisCluster) if err != nil { panic(err) } + repo := repository.NewTokenRepository(repository.TokenRepositoryParam{Redis: newRedis}) return &ServiceContext{ - Conn: sqlConn, - Config: c, - Validate: vi.MustValidator(), - Redis: *newRedis, - TokenRedisRepo: repo.NewTokenRepository(repo.TokenRepositoryParam{ - Store: newRedis, + Config: c, + TokenUseCase: usecase.NewTokenUseCase(usecase.TokenUseCaseParam{ + TokenRepo: repo, + RefreshExpires: c.Token.RefreshExpires, + Expired: c.Token.Expired, + Secret: c.Token.Secret, }), } } diff --git a/internal/usecase/bitmap.go b/internal/usecase/bitmap.go deleted file mode 100644 index 6660472..0000000 --- a/internal/usecase/bitmap.go +++ /dev/null @@ -1,117 +0,0 @@ -package usecase - -// Bitmap 基礎結構 -// Bitmap 是一個位圖結構,使用 byte slice 來表示大量的位(bit)。 -// 每個 byte 由 8 個位組成,因此可以高效地管理大量的開關狀態(true/false)。 -// Bitmap 的優點在於它能節省空間,尤其是在需要大量布爾值的場合。 -// 缺點是,如果需要動態擴充 Bitmap 的大小,會導致效率下降,因為需要重新分配和移動內存。 -// 因此,最好在初始化時就規劃好所需的位數大小,避免在之後頻繁擴充。 - -type Bitmap []byte - -// MakeBitmapWithBitSize 通過指定的 bit 數創建一個新的 Bitmap。 -// 參數 nBits 表示所需的位(bit)數。 -// 如果指定的位數少於 64,則默認將 Bitmap 初始化為 64 位(這是最低的限制)。 -// 此外,位數(nBits)會被自動調整為 8 的倍數,以適配 byte 的長度(每 8 位為一個 byte)。 -// 返回值是一個 Bitmap(byte slice),其大小根據位數確定。 -func MakeBitmapWithBitSize(nBits int) Bitmap { - // 如果指定的位數少於 64,則設置為 64 位(8 個 byte) - if nBits < 64 { - nBits = 64 - } - // 計算需要的 byte 數,確保每 8 位為一個 byte,並調整 nBits 以達到 8 的倍數 - return MustBitMap((nBits + 7) / 8) -} - -// MustBitMap 根據指定的 byte 數創建一個 Bitmap(byte slice)。 -// 參數 nBytes 表示所需的 byte 數。 -// 返回值是一個長度為 nBytes 的 Bitmap。 -func MustBitMap(nBytes int) Bitmap { - // 使用 make 函數創建一個 byte slice,大小為 nBytes。 - return make([]byte, nBytes) -} - -// SetTrue 設置指定位置的 bit 為 true(1)。 -// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。 -// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 1。 -func (b Bitmap) SetTrue(bitPos uint32) { - // |= 是一種位運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位或運算(bitwise OR),並將結果賦值 - b[bitPos/8] |= 1 << (bitPos % 8) -} - -// SetFalse 設置指定位置的 bit 為 false(0)。 -// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。 -// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 0。 -func (b Bitmap) SetFalse(bitPos uint32) { - // 取出對應 byte,使用位與和取反運算將對應的 bit 設置為 0 - // 假設我們有以下情況: - - // • b[1](即 b[bitPos/8])是 10101111(十進制 175)。 - // • bitPos = 10,也就是我們想清除第 10 位的值。 - // - // 操作步驟: - // - // 1. bitPos/8 = 1:所以我們要修改 b[1] 這個 byte。 - // 2. bitPos % 8 = 2:表示我們要清除的位是這個 byte 中的第 3 位(從右數起第 3 位)。 - // 3. 1 << (bitPos % 8) = 1 << 2 = 00000100:生成位掩碼 00000100。 - // 4. 取反:^(1 << 2) = ^00000100 = 11111011,這樣的掩碼表示除了第 3 位,其他位都是 1。 - // 5. 位與運算:10101111 & 11111011 = 10101011,結果將第 3 位清除,其餘位保持不變。即,b[1] 變成了 10101011(十進制 171)。 - // &= 是一種 位與運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位與運算(bitwise AND),然後將結果賦值給左邊的變數。 - b[bitPos/8] &= ^(1 << (bitPos % 8)) -} - -// IsTrue 檢查指定位置的 bit 是否為 true(1)。 -// 參數 bitPos 是要檢查的位的位置(以 0 為基準的位索引)。 -// 如果該 bit 是 1,則返回 true;否則返回 false。 -func (b Bitmap) IsTrue(bitPos uint32) bool { - /* - 這一行程式碼 b[bitPos/8]&(1<<(bitPos%8)) != 0 是用來檢查 指定位(bit) 是否為 true(1), - 它的核心是位運算。讓我們逐步拆解並解釋這一行程式碼: - - 1. 背景知識: - - • 位運算 是在二進制層面操作數字。每個 byte 有 8 個位(bit),所以位圖結構是以 byte 來表示位的集合。 - • b 是一個 Bitmap 結構,也就是 []byte,即 byte 的切片。 - • bitPos 是一個 uint32 類型的變數,表示我們想要檢查的位(bit)在整個位圖中的索引(位置)。 - 3. 完整流程舉例: - 假設: - • b = []byte{0b10101010, 0b01010101} (即二進制表示的兩個 byte,分別是 10101010 和 01010101)。 - • bitPos = 10(我們要檢查第 10 位是否為 1)。 - 操作順序: - - 1. 計算 bitPos/8 = 10/8 = 1,所以我們要檢查的是第二個 byte:0b01010101。 - 2. 計算 bitPos%8 = 10%8 = 2,所以我們要檢查的是該 byte 中的第 3 位(從右數起第 3 位)。 - 3. 位移:1 << 2 = 00000100。 - 4. 位與:0b01010101 & 0b00000100 = 0b00000100(因為該 byte 的第 3 位是 1,結果不等於 0)。 - 5. 判斷結果:結果不等於 0,因此第 10 位是 1(true)。 - - 4. 總結: - - • b[bitPos/8]&(1<<(bitPos%8)) != 0 是一個經典的位操作,用來檢查位圖中某一個位是否為 1。 - • bitPos/8 找到對應的 byte,bitPos % 8 找到該位在這個 byte 中的具體位置。 - • 最後的位與運算和比較用來確定該位的狀態是 true(1)還是 false(0)。 - */ - return b[bitPos/8]&(1<<(bitPos%8)) != 0 -} - -// Reset 重置 Bitmap,使所有的 bit 都設置為 false(0)。 -// 這個操作會將整個 Bitmap 的所有 byte 都設置為 0,從而達到重置的效果。 -func (b Bitmap) Reset() { - // 迭代 Bitmap 中的每個 byte,並將其設置為 0 - for i := range b { - b[i] = 0 - } -} - -// ByteSize 返回 Bitmap 的 byte 長度。 -// 這個函數返回 Bitmap 目前占用的 byte 數量,該值等於 Bitmap 的長度(slice 的長度)。 -func (b Bitmap) ByteSize() int { - return len(b) -} - -// BitSize 返回 Bitmap 的位(bit)長度。 -// 這個函數通過將 byte 長度乘以 8 來計算 Bitmap 中的總位數。 -func (b Bitmap) BitSize() int { - // 每個 byte 包含 8 個 bit,因此將 byte 長度乘以 8 - return len(b) * 8 -} diff --git a/internal/usecase/bitmap_benchmark_test.go b/internal/usecase/bitmap_benchmark_test.go deleted file mode 100644 index 03545b2..0000000 --- a/internal/usecase/bitmap_benchmark_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package usecase - -import "testing" - -// 基準測試 SetTrue 函數,測試在不同大小的 Bitmap 上設置位元為 true 的效能 -func BenchmarkBitmapSetTrue(b *testing.B) { - // 以 10^6 位元作為基準測試的 Bitmap 大小 - bitmap := MakeBitmapWithBitSize(1000000) - b.ResetTimer() // 重設計時器,排除初始化的時間 - - for i := 0; i < b.N; i++ { - bitmap.SetTrue(uint32(i % 1000000)) // 設置位元為 true - } -} - -// 基準測試 SetFalse 函數,測試在不同大小的 Bitmap 上清除位元為 false 的效能 -func BenchmarkBitmapSetFalse(b *testing.B) { - // 以 10^6 位元作為基準測試的 Bitmap 大小 - bitmap := MakeBitmapWithBitSize(1000000) - b.ResetTimer() // 重設計時器,排除初始化的時間 - - for i := 0; i < b.N; i++ { - bitmap.SetFalse(uint32(i % 1000000)) // 清除位元為 false - } -} - -// 基準測試 IsTrue 函數,測試在不同大小的 Bitmap 上檢查位元狀態的效能 -func BenchmarkBitmapIsTrue(b *testing.B) { - // 以 10^6 位元作為基準測試的 Bitmap 大小 - bitmap := MakeBitmapWithBitSize(1000000) - b.ResetTimer() // 重設計時器,排除初始化的時間 - - for i := 0; i < b.N; i++ { - _ = bitmap.IsTrue(uint32(i % 1000000)) // 檢查位元是否為 true - } -} - -// 基準測試 Reset 函數,測試重置不同大小的 Bitmap 的效能 -func BenchmarkBitmapReset(b *testing.B) { - // 以 10^6 位元作為基準測試的 Bitmap 大小 - bitmap := MakeBitmapWithBitSize(1000000) - b.ResetTimer() // 重設計時器,排除初始化的時間 - - for i := 0; i < b.N; i++ { - bitmap.Reset() // 重置 Bitmap - } -} - -// 基準測試 BitSize 函數,測試返回位圖的 bit 長度的效能 -func BenchmarkBitmapBitSize(b *testing.B) { - // 以 10^6 位元作為基準測試的 Bitmap 大小 - bitmap := MakeBitmapWithBitSize(1000000) - b.ResetTimer() // 重設計時器,排除初始化的時間 - - for i := 0; i < b.N; i++ { - _ = bitmap.BitSize(0) // 測試返回位圖的 bit 長度 - } -} - -// 基準測試 ByteSize 函數,測試返回位圖的 byte 長度的效能 -func BenchmarkBitmapByteSize(b *testing.B) { - // 以 10^6 位元作為基準測試的 Bitmap 大小 - bitmap := MakeBitmapWithBitSize(1000000) - b.ResetTimer() // 重設計時器,排除初始化的時間 - - for i := 0; i < b.N; i++ { - _ = bitmap.ByteSize(0) // 測試返回位圖的 byte 長度 - } -} diff --git a/internal/usecase/bitmap_test.go b/internal/usecase/bitmap_test.go deleted file mode 100644 index 544cf45..0000000 --- a/internal/usecase/bitmap_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package usecase - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBitmap_SetTrueAndIsTrue(t *testing.T) { - tests := []struct { - name string - bitPos uint32 - expected bool - }{ - {"Set bit 0 to true", 0, true}, - {"Set bit 1 to true", 1, true}, - {"Set bit 63 to true", 63, true}, - {"Set bit 64 to true", 64, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap - bitmap.SetTrue(tt.bitPos) - result := bitmap.IsTrue(tt.bitPos) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestBitmap_SetFalse(t *testing.T) { - tests := []struct { - name string - bitPos uint32 - expected bool - }{ - {"Set bit 0 to false", 0, false}, - {"Set bit 1 to false", 1, false}, - {"Set bit 63 to false", 63, false}, - {"Set bit 64 to false", 64, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap - bitmap.SetTrue(tt.bitPos) // 先設置該 bit 為 true - bitmap.SetFalse(tt.bitPos) // 然後設置該 bit 為 false - result := bitmap.IsTrue(tt.bitPos) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestBitmap_Reset(t *testing.T) { - bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap - bitmap.SetTrue(0) - bitmap.SetTrue(64) - - // 確認 bit 0 和 bit 64 是 true - assert.True(t, bitmap.IsTrue(0)) - assert.True(t, bitmap.IsTrue(64)) - - bitmap.Reset() // 重置位圖 - - // 確認所有的位都已經重置為 false - assert.False(t, bitmap.IsTrue(0)) - assert.False(t, bitmap.IsTrue(64)) -} - -func TestBitmap_ByteSize(t *testing.T) { - bitmap := MakeBitmapWithBitSize(64) // 初始化一個 64 位的 Bitmap - assert.Equal(t, 8, bitmap.ByteSize(0)) // 64 位應該佔用 8 個 byte -} - -func TestBitmap_BitSize(t *testing.T) { - bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap - assert.Equal(t, 128, bitmap.BitSize(0)) // 128 位應該有 128 個 bit -} diff --git a/permission.go b/permission.go index 465f2da..9024563 100644 --- a/permission.go +++ b/permission.go @@ -2,12 +2,13 @@ package main import ( "flag" - "fmt" - "app-cloudep-permission-server/gen_result/pb/permission" - "app-cloudep-permission-server/internal/config" - tokenserviceServer "app-cloudep-permission-server/internal/server/tokenservice" - "app-cloudep-permission-server/internal/svc" + "github.com/zeromicro/go-zero/core/logx" + + "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" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/core/service" @@ -34,6 +35,6 @@ func main() { }) defer s.Stop() - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + logx.Infof("Starting rpc server at %s...\n", c.ListenOn) s.Start() } diff --git a/internal/entity/claims.go b/pkg/domain/entity/claims.go similarity index 100% rename from internal/entity/claims.go rename to pkg/domain/entity/claims.go diff --git a/pkg/domain/entity/permission.go b/pkg/domain/entity/permission.go new file mode 100644 index 0000000..07e033b --- /dev/null +++ b/pkg/domain/entity/permission.go @@ -0,0 +1,22 @@ +package entity + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Permission struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Parent string `bson:"parent"` // 父權限的 ID (用字串儲存 ObjectID 的 Hex) + Name string `bson:"name"` + HTTPMethod string `bson:"http_method"` // 視作操作(Action) + HTTPPath string `bson:"http_path"` // 視作資源(Object) + Status permission.Status `bson:"status"` // 例如 1: 啟用, 0: 停用 + Type permission.Type `bson:"type"` + UpdateAt int64 `bson:"update_at"` + CreateAt int64 `bson:"create_at"` +} + +func (c *Permission) Collection() string { + return "permission" +} diff --git a/pkg/domain/entity/role.go b/pkg/domain/entity/role.go new file mode 100644 index 0000000..1d421ac --- /dev/null +++ b/pkg/domain/entity/role.go @@ -0,0 +1,38 @@ +package entity + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 1. UID (使用者ID) +// +// 這個欄位通常代表創建這個角色的使用者,可能的用途: +// • 追蹤角色的建立者:確保知道誰創建了這個角色。 +// • 限定角色的管理範圍:例如,只有 UID 對應的使用者可以修改或刪除這個角色。 +// • 支援多租戶 (Multi-Tenancy):如果一個 UID 只能看到自己創建的角色,那這個系統可能是多租戶架構的一部分。 +// +// 2. ClientID (客戶端ID) +// +// 這個欄位通常代表該角色所屬的應用程式或客戶端,可能的用途: +// • 多租戶架構 (Multi-Tenancy):不同 ClientID 的角色可能互相隔離,確保不同組織不會影響彼此的角色權限。 +// • 對應 OAuth 2.0 或 OpenID Connect: +// • 在 OAuth2 / OIDC 的架構下,每個應用程式 (client_id) 可能有不同的角色和權限。 +// • 例如,client_id 為 web-app-1 的角色與 client_id 為 mobile-app-1 的角色可能完全不同。 +// • API 權限控制: +// • ClientID 可能是用來限制某些角色只能在特定應用程式中被使用,例如 Web 端和行動端的角色不同。 + +// Role 是這樣,如果有綁定某個 UID +type Role struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Name string `bson:"name"` // 角色名稱 + UID string `bson:"uid"` // 限定角色的管理範圍:例如,只有 UID 對應的使用者可以修改或刪除這個角色。 + ClientID string `bson:"client_id"` + Status permission.Status `bson:"status"` // 例如 1: 啟用, 0: 停用 + CreateAt int64 `bson:"create_at"` + UpdateAt int64 `bson:"update_at"` +} + +func (c *Role) Collection() string { + return "role" +} diff --git a/pkg/domain/entity/role_permission.go b/pkg/domain/entity/role_permission.go new file mode 100644 index 0000000..a28fb08 --- /dev/null +++ b/pkg/domain/entity/role_permission.go @@ -0,0 +1,15 @@ +package entity + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type RolePermission struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + RoleID string `bson:"role_id"` // 對應 Role 的 _id + PermissionID string `bson:"permission_id"` // 對應 Permission 的 _id + CreateAt int64 `bson:"create_at"` + UpdateAt int64 `bson:"update_at"` +} + +func (c *RolePermission) Collection() string { + return "role_permission" +} diff --git a/pkg/domain/entity/ticket.go b/pkg/domain/entity/ticket.go new file mode 100644 index 0000000..6fa0fed --- /dev/null +++ b/pkg/domain/entity/ticket.go @@ -0,0 +1,6 @@ +package entity + +type Ticket struct { + Data interface{} `json:"data"` + Token Token `json:"token"` +} diff --git a/pkg/domain/entity/token.go b/pkg/domain/entity/token.go new file mode 100644 index 0000000..6de960a --- /dev/null +++ b/pkg/domain/entity/token.go @@ -0,0 +1,56 @@ +package entity + +import "time" + +// 時間都以 UnixNano 來做 + +// Token 定義 +type Token struct { + ID string `json:"id"` // Token 的唯一標識符 + UID string `json:"uid"` // UID + DeviceID string `json:"device_id"` // Device + AccessToken string `json:"access_token"` // Access Token + ExpiresIn int64 `json:"expires_in"` // 存取 Token 的有效時間 UnixNano e.g 過期時間的Expire unix nano + AccessCreateAt int64 `json:"access_create_at"` // 存取 Token 的創建時間 timestamp + RefreshToken string `json:"refresh_token"` // 刷新 Token one time token + RefreshExpiresIn int64 `json:"refresh_expires_in"` // 刷新 Token one time token 的有效時間 UnixNano e.g 過期時間的Expire unix nano + RefreshCreateAt int64 `json:"refresh_create_at"` // 刷新 Token one time token 的創建時間 +} + +// AccessTokenExpires 返回存取 Token 的有效期(以秒為單位)。 +func (t *Token) AccessTokenExpires() time.Duration { + return time.Duration(t.ExpiresIn) * time.Second +} + +// RefreshTokenExpires 返回刷新 Token 的有效期(以秒為單位)。 +func (t *Token) RefreshTokenExpires() time.Duration { + return time.Duration(t.RefreshExpiresIn) * time.Second +} + +// RefreshTokenExpiresUnix 返回刷新 Token 的到期時間(UnixNano 時間戳)。 +func (t *Token) RefreshTokenExpiresUnix() int64 { + return time.Now().Add(t.RefreshTokenExpires()).UnixNano() +} + +// IsExpires 檢查存取 Token 是否已過期。如果存取 Token 的創建時間加上其有效期早於當前時間,則返回 true。 +func (t *Token) IsExpires() bool { + accessCreateAt := time.Unix(0, t.AccessCreateAt) + + return accessCreateAt.Add(t.AccessTokenExpires()).Before(time.Now()) +} + +// RedisExpiredSec 返回存取 Token 在 Redis 中的剩餘有效時間(秒)。 +// 計算方法為:從到期時間的 Unix 時間戳減去當前時間。 +func (t *Token) RedisExpiredSec() int64 { + sec := time.Unix(0, t.ExpiresIn).Sub(time.Now().UTC()) + + return int64(sec.Seconds()) +} + +// RedisRefreshExpiredSec 返回刷新 Token 在 Redis 中的剩餘有效時間(秒)。 +// 計算方法為:從刷新到期時間的 Unix 時間戳減去當前時間。 +func (t *Token) RedisRefreshExpiredSec() int64 { + sec := time.Unix(0, t.RefreshExpiresIn).Sub(time.Now().UTC()) + + return int64(sec.Seconds()) +} diff --git a/pkg/domain/entity/token_test.go b/pkg/domain/entity/token_test.go new file mode 100644 index 0000000..6dc1742 --- /dev/null +++ b/pkg/domain/entity/token_test.go @@ -0,0 +1,163 @@ +package entity + +import ( + "testing" + "time" +) + +// TestAccessTokenExpires 測試 AccessTokenExpires 方法是否能正確將 ExpiresIn 轉換成 time.Duration(秒) +func TestAccessTokenExpires(t *testing.T) { + tests := []struct { + name string // 測試案例名稱 + expiresIn int64 // 輸入的 ExpiresIn 值,單位為秒 + want time.Duration // 預期返回的時間間隔 + }{ + {"zero expiration", 0, 0}, + {"1 second expiration", 1, 1 * time.Second}, + {"60 seconds expiration", 60, 60 * time.Second}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 建立一個 Token 實例,僅設置 ExpiresIn 欄位 + token := Token{ + ExpiresIn: tt.expiresIn, + } + // 呼叫 AccessTokenExpires 方法 + got := token.AccessTokenExpires() // ex 1m0s, 1s, 0s etc .... + // 檢查返回值是否與預期相符 + if got != tt.want { + t.Errorf("AccessTokenExpires() = %v, want %v", got, tt.want) + } + }) + } +} + +// TestRefreshTokenExpires 測試 RefreshTokenExpires 方法是否能正確將 RefreshExpiresIn(秒)轉換成 time.Duration +func TestRefreshTokenExpires(t *testing.T) { + tests := []struct { + name string + refreshExpiresIn int64 + want time.Duration + }{ + {"zero", 0, 0}, + {"1 second", 1, 1 * time.Second}, + {"60 seconds", 60, 60 * time.Second}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + token := Token{ + RefreshExpiresIn: tt.refreshExpiresIn, + } + got := token.RefreshTokenExpires() + if got != tt.want { + t.Errorf("RefreshTokenExpires() = %v, want %v", got, tt.want) + } + }) + } +} + +// TestRefreshTokenExpiresUnix 測試 RefreshTokenExpiresUnix 方法返回的 UnixNano 時間戳是否大致符合預期 +func TestRefreshTokenExpiresUnix(t *testing.T) { + // 設定 refreshExpiresIn 為 60 秒 + token := Token{ + RefreshExpiresIn: 60, + } + // 取得測試開始時的時間 + now := time.Now() + // 預期值:now + 60 秒 + expected := now.Add(60 * time.Second).UnixNano() + got := token.RefreshTokenExpiresUnix() + + // 設定允許誤差 50 毫秒 + tolerance := int64(50 * time.Millisecond) + if got < expected-tolerance || got > expected+tolerance { + t.Errorf("RefreshTokenExpiresUnix() = %v, want approx %v", got, expected) + } +} + +// TestIsExpires 測試 IsExpires 方法判斷 token 是否過期 +func TestIsExpires(t *testing.T) { + now := time.Now() + + // 測試未過期:token 創建時間為現在,過期時長為 10 秒 + tokenValid := Token{ + AccessCreateAt: now.UnixNano(), + ExpiresIn: 10, // 此處 ExpiresIn 為有效時長(秒) + } + if tokenValid.IsExpires() { + t.Errorf("IsExpires() 返回 true,但 token 尚未過期") + } + + // 測試已過期:token 創建時間為 20 秒前,過期時長為 10 秒 + tokenExpired := Token{ + AccessCreateAt: now.Add(-20 * time.Second).UnixNano(), + ExpiresIn: 10, + } + if !tokenExpired.IsExpires() { + t.Errorf("IsExpires() 返回 false,但 token 已過期") + } +} + +// TestRedisExpiredSec 測試 RedisExpiredSec 方法 +// 此方法根據 t.ExpiresIn (UnixNano 表示的到期時間) 與當前 UTC 時間計算剩餘秒數 +func TestRedisExpiredSec(t *testing.T) { + // 定義測試案例: + // - expireOffset 為從當前時間的偏移量(正數表示未過期,負數表示已過期) + // - minExpected 與 maxExpected 為允許的返回值範圍(因為執行期間會有少量延遲) + tests := []struct { + name string + expireOffset time.Duration // token 過期時間相對於當前的偏移量 + minExpected int64 // 預期返回值下限(秒) + maxExpected int64 // 預期返回值上限(秒) + }{ + {"Not expired +10s", 10 * time.Second, 9, 10}, + {"Expired -5s", -5 * time.Second, -6, -5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now().UTC() + // token 的到期時間設定為當前時間加上偏移量 + expTime := now.Add(tt.expireOffset) + token := Token{ + ExpiresIn: expTime.UnixNano(), + } + + got := token.RedisExpiredSec() + if got < tt.minExpected || got > tt.maxExpected { + t.Errorf("RedisExpiredSec() = %d, want between %d and %d", got, tt.minExpected, tt.maxExpected) + } + }) + } +} + +// TestRedisRefreshExpiredSec 測試 RedisRefreshExpiredSec 方法 +// 此方法根據 t.RefreshExpiresIn (UnixNano 表示的刷新到期時間) 與當前 UTC 時間計算剩餘秒數 +func TestRedisRefreshExpiredSec(t *testing.T) { + tests := []struct { + name string + expireOffset time.Duration // 刷新 token 到期時間相對於當前的偏移量 + minExpected int64 // 預期返回值下限(秒) + maxExpected int64 // 預期返回值上限(秒) + }{ + {"Not expired +20s", 20 * time.Second, 19, 20}, + {"Expired -5s", -5 * time.Second, -6, -5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now().UTC() + expTime := now.Add(tt.expireOffset) + token := Token{ + RefreshExpiresIn: expTime.UnixNano(), + } + + got := token.RedisRefreshExpiredSec() + if got < tt.minExpected || got > tt.maxExpected { + t.Errorf("RedisRefreshExpiredSec() = %d, want between %d and %d", got, tt.minExpected, tt.maxExpected) + } + }) + } +} diff --git a/pkg/domain/entity/user_role.go b/pkg/domain/entity/user_role.go new file mode 100644 index 0000000..d55927e --- /dev/null +++ b/pkg/domain/entity/user_role.go @@ -0,0 +1,15 @@ +package entity + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type UserRole struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + UID string `bson:"uid"` // 對應 User 的 _id + RoleID string `bson:"role_id"` // 對應 Role 的 _id + CreateAt int64 `bson:"create_at"` + UpdateAt int64 `bson:"update_at"` +} + +func (c *UserRole) Collection() string { + return "user_role" +} diff --git a/pkg/domain/error.go b/pkg/domain/error.go new file mode 100644 index 0000000..10f7ba6 --- /dev/null +++ b/pkg/domain/error.go @@ -0,0 +1,45 @@ +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 + TokenValidateErrorCode + TokenClaimErrorCode + TokenCreateErrorCode + TokenRefreshErrorCode + TokenCancelErrorCode + TokensCancelErrorCode + TokenGetErrorCode + + FailedToGetRolePermission + FailedToGetUserRole + FailedToGetUserRoleByUID + FailedToCreateUserRole + FailedToUpdateUserRole + FailedToListRole + FailedToGetRoleByID + FailedToGetRoleByUID + FailedToCreateRole + FailedToUpdateRole + FailedToDelRole +) + +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/permission/status.go b/pkg/domain/permission/status.go new file mode 100644 index 0000000..b9a6023 --- /dev/null +++ b/pkg/domain/permission/status.go @@ -0,0 +1,30 @@ +package permission + +type Status int8 + +const ( + Open Status = iota + 1 + Close +) + +const ( + ClosePermission StatusCode = "close" + OpenPermission StatusCode = "open" +) + +func (s Status) String() string { + status, ok := statusMap[s] + if ok { + return string(status) + } + + return string(ClosePermission) +} + +var statusMap = map[Status]StatusCode{ + Open: OpenPermission, + Close: ClosePermission, +} + +type StatusCode string +type Permissions map[string]StatusCode diff --git a/pkg/domain/permission/type.go b/pkg/domain/permission/type.go new file mode 100644 index 0000000..4a74707 --- /dev/null +++ b/pkg/domain/permission/type.go @@ -0,0 +1,10 @@ +package permission + +type Type int8 + +const ( + BackendUser Type = iota + 1 + FrontendUser +) + +const AdminRoleUID = "GD1000001" // 管理員 GodDog 1000001 diff --git a/pkg/domain/rbac/rule.go b/pkg/domain/rbac/rule.go new file mode 100644 index 0000000..299f1a0 --- /dev/null +++ b/pkg/domain/rbac/rule.go @@ -0,0 +1,61 @@ +package rbac + +// Rule 最多六個參數,例如 +// +// p, admin, data1, read +// PolicyType:p +// Field0:admin +// Field01:data1 +// Field02:read +type Rule struct { + PolicyType string // p or g + Field0 string + Field1 string + Field2 string + Field3 string + Field4 string + Field5 string +} + +// ToString 轉換成這樣 []string{"p", "admin", "data1", "read"} 讓 casbin 看得懂 +func (rule Rule) ToString() []string { + fields := []string{ + rule.PolicyType, rule.Field0, rule.Field1, + rule.Field2, rule.Field3, rule.Field4, rule.Field5, + } + // 移除空字串,提高效能 + result := make([]string, 0, len(fields)) + for _, field := range fields { + if field != "" { + result = append(result, field) + } + } + + return result +} + +func StringToPolicy(policyType string, rule []string) Rule { + line := Rule{} + + line.PolicyType = policyType + if len(rule) > 0 { + line.Field0 = rule[0] + } + if len(rule) > 1 { + line.Field1 = rule[1] + } + if len(rule) > 2 { + line.Field2 = rule[2] + } + if len(rule) > 3 { + line.Field3 = rule[3] + } + if len(rule) > 4 { + line.Field4 = rule[4] + } + if len(rule) > 5 { + line.Field5 = rule[5] + } + + return line +} diff --git a/pkg/domain/rbac/rule_test.go b/pkg/domain/rbac/rule_test.go new file mode 100644 index 0000000..d37163e --- /dev/null +++ b/pkg/domain/rbac/rule_test.go @@ -0,0 +1,122 @@ +package rbac + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRule_ToString(t *testing.T) { + tests := []struct { + name string + input Rule + expected []string + }{ + { + name: "完整六個欄位", + input: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "data1", + Field2: "read", + Field3: "extra1", + Field4: "extra2", + Field5: "extra3", + }, + expected: []string{"p", "admin", "data1", "read", "extra1", "extra2", "extra3"}, + }, + { + name: "部分欄位空白", + input: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "", + Field2: "read", + Field3: "", + Field4: "", + Field5: "extra3", + }, + expected: []string{"p", "admin", "read", "extra3"}, + }, + { + name: "只有 PolicyType", + input: Rule{PolicyType: "p"}, + expected: []string{"p"}, + }, + { + name: "所有欄位皆空", + input: Rule{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.input.ToString() + assert.Equal(t, tt.expected, result, "ToString output should match expected") + }) + } +} + +func TestStringToPolicy(t *testing.T) { + tests := []struct { + name string + policyType string + input []string + expected Rule + }{ + { + name: "完整六個欄位", + policyType: "p", + input: []string{"admin", "data1", "read", "extra1", "extra2", "extra3"}, + expected: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "data1", + Field2: "read", + Field3: "extra1", + Field4: "extra2", + Field5: "extra3", + }, + }, + { + name: "部分欄位空白", + policyType: "p", + input: []string{"admin", "", "read", "", "", "extra3"}, + expected: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "", + Field2: "read", + Field3: "", + Field4: "", + Field5: "extra3", + }, + }, + { + name: "只有 PolicyType", + policyType: "p", + input: []string{}, + expected: Rule{ + PolicyType: "p", + }, + }, + { + name: "只有兩個欄位", + policyType: "g", + input: []string{"user", "admin"}, + expected: Rule{ + PolicyType: "g", + Field0: "user", + Field1: "admin", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := StringToPolicy(tt.policyType, tt.input) + assert.Equal(t, tt.expected, result, "StringToPolicy output should match expected") + }) + } +} diff --git a/internal/domain/redis.go b/pkg/domain/redis.go similarity index 69% rename from internal/domain/redis.go rename to pkg/domain/redis.go index 2e31a91..ff43511 100644 --- a/internal/domain/redis.go +++ b/pkg/domain/redis.go @@ -2,23 +2,14 @@ package domain import "strings" -const ( - TicketKeyPrefix = "tic/" -) - -const ( - ClientDataKey = "permission:clients" -) - type RedisKey string const ( AccessTokenRedisKey RedisKey = "access_token" RefreshTokenRedisKey RedisKey = "refresh_token" - DeviceTokenRedisKey RedisKey = "device_token" UIDTokenRedisKey RedisKey = "uid_token" TicketRedisKey RedisKey = "ticket" - DeviceUIDRedisKey RedisKey = "device_uid" + DeviceTokenRedisKey RedisKey = "device_token" ) func (key RedisKey) ToString() string { @@ -35,10 +26,28 @@ func GetAccessTokenRedisKey(id string) string { return AccessTokenRedisKey.With(id).ToString() } +func GetRefreshTokenRedisKey(id string) string { + return RefreshTokenRedisKey.With(id).ToString() +} + func GetUIDTokenRedisKey(uid string) string { return UIDTokenRedisKey.With(uid).ToString() } +func GetDeviceTokenRedisKey(device string) string { + return DeviceTokenRedisKey.With(device).ToString() +} + func GetTicketRedisKey(ticket string) string { return TicketRedisKey.With(ticket).ToString() } + +// =================== + +const ( + CasbinRuleRedisKey RedisKey = "casbin_rule" +) + +func GetCasbinRuleRedisKey() string { + return CasbinRuleRedisKey.ToString() +} diff --git a/pkg/domain/repository/casbin_redis_adapter.go b/pkg/domain/repository/casbin_redis_adapter.go new file mode 100644 index 0000000..1d1f107 --- /dev/null +++ b/pkg/domain/repository/casbin_redis_adapter.go @@ -0,0 +1,20 @@ +package repository + +import ( + "github.com/casbin/casbin/v2/persist" +) + +// 角色權限策略 (policy.csv) 以 const 定義 +/* + p, admin, data1, read + p, admin, data1, write + p, user, data1, read + + g, alice, admin + g, bob, user +*/ + +// RBACAdapter 資料接收器,讓 Casbin 讀取規則的,類似上面這種規則 +type RBACAdapter interface { + persist.Adapter +} diff --git a/pkg/domain/repository/permission.go b/pkg/domain/repository/permission.go new file mode 100644 index 0000000..9aa5f15 --- /dev/null +++ b/pkg/domain/repository/permission.go @@ -0,0 +1,46 @@ +package repository + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "go.mongodb.org/mongo-driver/mongo" +) + +type PermissionRepository interface { + Insert(ctx context.Context, permission entity.Permission) error + Update(ctx context.Context, id string, req UpdatePermission) error + Delete(ctx context.Context, id string) error + GetPermission + Index +} + +type GetPermission interface { + // GetAll 取得所有權限列表 + GetAll(ctx context.Context, status *permission.Status) ([]entity.Permission, error) + // GetAllIntoIDMap 以權限 ID 作為鍵取得所有權限的映射 + GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) + // FindOne 根據查詢條件取得單筆權限資料 + FindOne(ctx context.Context, query PermissionQuery) (entity.Permission, error) + // FindByNames 根據權限名稱列表查詢權限資料 + FindByNames(ctx context.Context, names []string) ([]entity.Permission, error) +} + +type Index interface { + Index20250214UP(ctx context.Context) (*mongo.Cursor, error) +} + +type PermissionQuery struct { + HTTPMethod string + HTTPPath string +} + +type UpdatePermission struct { + ParentID *string + Name *string + HTTPMethod *string + HTTPPath *string + Status *permission.Status + Type *permission.Type +} diff --git a/pkg/domain/repository/role.go b/pkg/domain/repository/role.go new file mode 100644 index 0000000..48410ee --- /dev/null +++ b/pkg/domain/repository/role.go @@ -0,0 +1,41 @@ +package repository + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "go.mongodb.org/mongo-driver/mongo" +) + +type RoleRepository interface { + List(ctx context.Context, param ListQuery) ([]*entity.Role, int64, error) + GetByID(ctx context.Context, id string) (*entity.Role, error) + GetByUID(ctx context.Context, uid string) (*entity.Role, error) + All(ctx context.Context, clientID *string) ([]*entity.Role, error) + Create(ctx context.Context, role *entity.Role) error + Update(ctx context.Context, data UpdateReq) error + Delete(ctx context.Context, id string) error + RoleIndex +} + +type RoleIndex interface { + Index20250224UP(ctx context.Context) (*mongo.Cursor, error) +} + +type ListQuery struct { + PageSize int64 // 必填 + PageIndex int64 // 必填 + ClientID *string + UID *string + Name *string + Status *permission.Status +} + +type UpdateReq struct { + ID string + Name *string + UID *string + ClientID *string + Status *permission.Status +} diff --git a/pkg/domain/repository/role_permission.go b/pkg/domain/repository/role_permission.go new file mode 100644 index 0000000..0f87bdf --- /dev/null +++ b/pkg/domain/repository/role_permission.go @@ -0,0 +1,20 @@ +package repository + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "go.mongodb.org/mongo-driver/mongo" +) + +type RolePermissionRepository interface { + Get(ctx context.Context, roleID string) ([]*entity.RolePermission, error) + GetByPermissionID(ctx context.Context, permissionIDs []string) ([]*entity.RolePermission, error) + Create(ctx context.Context, entity []entity.RolePermission) error + Delete(ctx context.Context, roleID string, permissions []string) error + RolePermissionIndex +} + +type RolePermissionIndex interface { + Index20250225UP(ctx context.Context) (*mongo.Cursor, error) +} diff --git a/pkg/domain/repository/token.go b/pkg/domain/repository/token.go new file mode 100644 index 0000000..4c2d1f2 --- /dev/null +++ b/pkg/domain/repository/token.go @@ -0,0 +1,50 @@ +package repository + +import ( + "context" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" +) + +// TokenRepo 管理Token +type TokenRepo interface { + Create + Get + Delete +} + +type Create interface { + // Create 建立新的 Token + Create(ctx context.Context, token entity.Token) error + // CreateOneTimeToken 建立臨時(一次性)Token,並指定有效期限 + CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, et time.Duration) error +} + +type Get interface { + // GetAccessTokenByOneTimeToken 根據一次性 Token 獲取對應的存取 Token + GetAccessTokenByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) + // GetAccessTokenByID 根據 Token ID 獲取對應的存取 Token + GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) + // GetAccessTokensByUID 根據用戶 ID 獲取該用戶的所有存取 Token + GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) + // GetAccessTokenCountByUID 根據用戶 ID 獲取該用戶的存取 Token 數量 + GetAccessTokenCountByUID(ctx context.Context, uid string) (int, error) + // GetAccessTokensByDeviceID 根據裝置 ID 獲取該裝置的所有存取 Token + GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) + // GetAccessTokenCountByDeviceID 根據裝置 ID 獲取該裝置的存取 Token 數量 + GetAccessTokenCountByDeviceID(ctx context.Context, deviceID string) (int, error) +} + +type Delete interface { + // Delete 刪除指定的 Token + Delete(ctx context.Context, token entity.Token) error + // DeleteAccessTokenByID 根據 Token ID 批量刪除存取 Token + DeleteAccessTokenByID(ctx context.Context, ids []string) error + // DeleteAccessTokensByUID 根據用戶 ID 刪除該用戶的所有存取 Token + DeleteAccessTokensByUID(ctx context.Context, uid string) error + // DeleteAccessTokensByDeviceID 根據裝置 ID 刪除該裝置的所有存取 Token + DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error + // DeleteOneTimeToken 批量刪除一次性 Token + DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error +} diff --git a/pkg/domain/repository/user_role.go b/pkg/domain/repository/user_role.go new file mode 100644 index 0000000..1126925 --- /dev/null +++ b/pkg/domain/repository/user_role.go @@ -0,0 +1,27 @@ +package repository + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "go.mongodb.org/mongo-driver/mongo" +) + +type UserRoleRepository interface { + GetAll(ctx context.Context) ([]*entity.UserRole, error) // `All` -> `GetAll`,更直觀 + GetByUserID(ctx context.Context, uid string) (entity.UserRole, error) // `Get` -> `GetByUserID`,明確是透過 `uid` 查詢 + GetUsersByRoleID(ctx context.Context, roleID string) ([]entity.UserRole, error) // `GetByRoleID` -> `GetUsersByRoleID`,更具語意 + CountUsersByRole(ctx context.Context) ([]RoleUserCount, error) // `UserCount` -> `CountUsersByRole`,清楚表達它是計算使用者數量 + CreateUserRole(ctx context.Context, param entity.UserRole) error // `Create` -> `CreateUserRole`,明確表示新增的是使用者與角色的關係 + UpdateUserRole(ctx context.Context, uid, roleID string) (entity.UserRole, error) // `Update` -> `UpdateUserRole`,明確描述更新的是哪個對象 + UserRoleIndex +} + +type UserRoleIndex interface { + Index20250225UP(ctx context.Context) (*mongo.Cursor, error) +} + +type RoleUserCount struct { + RoleID string `bson:"_id"` + Count int `bson:"count"` +} diff --git a/pkg/domain/token/additional.go b/pkg/domain/token/additional.go new file mode 100644 index 0000000..f1535d9 --- /dev/null +++ b/pkg/domain/token/additional.go @@ -0,0 +1,39 @@ +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/additional_test.go b/pkg/domain/token/additional_test.go new file mode 100644 index 0000000..357eb6d --- /dev/null +++ b/pkg/domain/token/additional_test.go @@ -0,0 +1,55 @@ +package token + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAdditional_String(t *testing.T) { + tests := []struct { + name string + input Additional + expected string + }{ + {"ID to String", ID, "id"}, + {"Role to String", Role, "role"}, + {"Device to String", Device, "device"}, + {"UID to String", UID, "uid"}, + {"Account to String", Account, "account"}, + {"Scope to String", Scope, "scope"}, + {"Type to String", Type, "token_type"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.input.String() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsValidAdditional(t *testing.T) { + validKeys := []Additional{ + ID, Role, Device, + UID, Account, Scope, Type, + } + + invalidKeys := []Additional{ + "invalid", "unknown", "random", "test", + } + + // 測試有效 Key + for _, key := range validKeys { + t.Run("ValidKey_"+key.String(), func(t *testing.T) { + assert.True(t, IsValidAdditional(key)) + }) + } + + // 測試無效 Key + for _, key := range invalidKeys { + t.Run("InvalidKey_"+string(key), func(t *testing.T) { + assert.False(t, IsValidAdditional(key)) + }) + } +} 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/scope_test.go b/pkg/domain/token/scope_test.go new file mode 100644 index 0000000..89de9ce --- /dev/null +++ b/pkg/domain/token/scope_test.go @@ -0,0 +1,28 @@ +package token + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTScope_ToString(t *testing.T) { + tests := []struct { + name string + input TScope + expected string + }{ + {"Empty String", TScope(""), ""}, + {"Simple String", TScope("read"), "read"}, + {"Complex String", TScope("user:write"), "user:write"}, + {"Special Characters", TScope("@dmin!"), "@dmin!"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 使用指標調用方法 + result := tt.input.ToString() + assert.Equal(t, tt.expected, result) + }) + } +} 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/token/type_test.go b/pkg/domain/token/type_test.go new file mode 100644 index 0000000..c0bafed --- /dev/null +++ b/pkg/domain/token/type_test.go @@ -0,0 +1,28 @@ +package token + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestType_ToString(t *testing.T) { + tests := []struct { + name string + input VerifyType + expected string + }{ + {"Empty String", VerifyType(""), ""}, + {"Simple String", VerifyType("read"), "read"}, + {"Complex String", VerifyType("user:write"), "user:write"}, + {"Special Characters", VerifyType("@dmin!"), "@dmin!"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 使用指標調用方法 + result := tt.input.ToString() + assert.Equal(t, tt.expected, result) + }) + } +} 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/casbin_redis_rbac.go b/pkg/domain/usecase/casbin_redis_rbac.go new file mode 100644 index 0000000..1fde3ef --- /dev/null +++ b/pkg/domain/usecase/casbin_redis_rbac.go @@ -0,0 +1,20 @@ +package usecase + +import ( + "context" + "time" +) + +type RBACUseCase interface { + Check(ctx context.Context, role, path, method string) (CheckRolePermissionStatus, error) + LoadPolicy(ctx context.Context) error + SyncPolicy(ctx context.Context, cron time.Duration) +} + +type CheckRolePermissionStatus struct { + Allow bool `json:"allow"` + Select struct { + PermissionName string `json:"permission_name"` + PlainCode bool `json:"plain_code"` + } `json:"select"` +} diff --git a/pkg/domain/usecase/error.go b/pkg/domain/usecase/error.go new file mode 100644 index 0000000..b6b0afe --- /dev/null +++ b/pkg/domain/usecase/error.go @@ -0,0 +1,7 @@ +package usecase + +import "fmt" + +var ( + ErrNotFound = fmt.Errorf("permission not found") +) diff --git a/pkg/domain/usecase/permission.go b/pkg/domain/usecase/permission.go new file mode 100644 index 0000000..2f79115 --- /dev/null +++ b/pkg/domain/usecase/permission.go @@ -0,0 +1,44 @@ +package usecase + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" +) + +type PermissionUseCase interface { + Insert(ctx context.Context, req CreatePermissionReq) error + Del(ctx context.Context, id string) error + Update(ctx context.Context, id string, req UpdatePermissionReq) error + All(ctx context.Context, status *permission.Status) ([]entity.Permission, error) + // FilterAll 用樹的結構,使得付節點若關閉,子節點也不會顯示 + FilterAll(ctx context.Context) ([]entity.Permission, error) + + // Get(ctx context.Context, id string) (entity.Permission, error) + // List(ctx context.Context, param ListParam) ([]entity.Permission, int64, error) +} + +type CreatePermissionReq struct { + Parent *string // 父權限的 ID (用字串儲存 ObjectID 的 Hex) + Name string // 權限名字 + HTTPMethod string // 視作操作(Action) + HTTPPath string // 視作資源(Object) + Status permission.Status // 例如 1: 啟用, 0: 停用 + Type permission.Type +} + +type UpdatePermissionReq struct { + Parent *string // 父權限的 ID (用字串儲存 ObjectID 的 Hex) + Name *string // 權限名字 + HTTPMethod *string // 視作操作(Action) + HTTPPath *string // 視作資源(Object) + Status *permission.Status // 例如 1: 啟用, 0: 停用 + Type *permission.Type +} + +type ListParam struct { + PageIndex int64 + PageSize int64 + Parent *string +} diff --git a/pkg/domain/usecase/role.go b/pkg/domain/usecase/role.go new file mode 100644 index 0000000..75c003a --- /dev/null +++ b/pkg/domain/usecase/role.go @@ -0,0 +1,44 @@ +package usecase + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" +) + +type RoleUseCase interface { + List(ctx context.Context, param ListQuery) ([]Role, int64, error) + All(ctx context.Context, clientID *string) ([]Role, error) + GetByID(ctx context.Context, id string) (*Role, error) + GetByUID(ctx context.Context, uid string) (*Role, error) // -> 限定角色的管理範圍:查詢誰建立的,用某個角色管理,目前用不到 + Create(ctx context.Context, role CreateRoleReq) error + Update(ctx context.Context, id string, data CreateRoleReq) error + Delete(ctx context.Context, id string) error +} + +type ListQuery struct { + PageSize int64 // 必填 + PageIndex int64 // 必填 + ClientID *string + UID *string + Name *string + Status *permission.Status + Permission []string +} + +type Role struct { + ID string `json:"id"` + Name string `json:"name"` // 角色名稱 + UID string `json:"uid"` + ClientID string `json:"client_id"` + Status permission.Status `json:"status"` // 例如 1: 啟用, 0: 停用 + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` +} + +type CreateRoleReq struct { + Name string `json:"name"` // 角色名稱 + UID string `json:"uid"` + ClientID string `json:"client_id"` + Status permission.Status `json:"status"` // 例如 1: 啟用, 0: 停用 +} diff --git a/pkg/domain/usecase/role_permission.go b/pkg/domain/usecase/role_permission.go new file mode 100644 index 0000000..238b081 --- /dev/null +++ b/pkg/domain/usecase/role_permission.go @@ -0,0 +1,31 @@ +package usecase + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" +) + +type RolePermissionUseCase interface { + Get(ctx context.Context, roleID string) (permission.Permissions, error) // -> role 有哪些Permission + Create(ctx context.Context, roleID string, permissions permission.Permissions) error + Delete(ctx context.Context, roleID string, permissions permission.Permissions) error + List(ctx context.Context, req ListQuery) (RoleResp, error) + // GetByRoleUID(ctx context.Context, uid string) (permission.Permissions, error) + // GetByUser(ctx context.Context, uid string) (UserPermission, error) +} + +type UserPermission struct { + RoleID string `json:"role_id"` + Permissions permission.Permissions `json:"permissions"` +} + +type UserRoleCountResp struct { + Role + UserCount int `json:"user_count"` +} + +type RoleResp struct { + Roles []Role + Total int64 `json:"total"` +} diff --git a/pkg/domain/usecase/token.go b/pkg/domain/usecase/token.go new file mode 100644 index 0000000..59f9972 --- /dev/null +++ b/pkg/domain/usecase/token.go @@ -0,0 +1,92 @@ +package usecase + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "github.com/golang-jwt/jwt/v4" +) + +type TokenUseCase interface { + 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) (Additional, error) +} + +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/domain/usecase/user_role.go b/pkg/domain/usecase/user_role.go new file mode 100644 index 0000000..9cb8863 --- /dev/null +++ b/pkg/domain/usecase/user_role.go @@ -0,0 +1,21 @@ +package usecase + +import "context" + +type UserRoleUseCase interface { + Select(ctx context.Context, filter UserRoleFilter) ([]UserRole, error) + Get(ctx context.Context, uid string) (UserRole, error) + Create(ctx context.Context, uid, roleID string) (UserRole, error) + Update(ctx context.Context, uid, roleID string) error +} + +type UserRole struct { + UID string `json:"uid"` + RoleID string `json:"role_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` +} + +type UserRoleFilter struct { + RoleID string +} diff --git a/pkg/mock/repository/permission.go b/pkg/mock/repository/permission.go new file mode 100644 index 0000000..4470315 --- /dev/null +++ b/pkg/mock/repository/permission.go @@ -0,0 +1,285 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/permission.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/permission.go -destination=./pkg/mock/repository/permission.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + permission "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + repository "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockPermissionRepository is a mock of PermissionRepository interface. +type MockPermissionRepository struct { + ctrl *gomock.Controller + recorder *MockPermissionRepositoryMockRecorder + isgomock struct{} +} + +// MockPermissionRepositoryMockRecorder is the mock recorder for MockPermissionRepository. +type MockPermissionRepositoryMockRecorder struct { + mock *MockPermissionRepository +} + +// NewMockPermissionRepository creates a new mock instance. +func NewMockPermissionRepository(ctrl *gomock.Controller) *MockPermissionRepository { + mock := &MockPermissionRepository{ctrl: ctrl} + mock.recorder = &MockPermissionRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPermissionRepository) EXPECT() *MockPermissionRepositoryMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockPermissionRepository) Delete(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockPermissionRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPermissionRepository)(nil).Delete), ctx, id) +} + +// FindByNames mocks base method. +func (m *MockPermissionRepository) FindByNames(ctx context.Context, names []string) ([]entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByNames", ctx, names) + ret0, _ := ret[0].([]entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByNames indicates an expected call of FindByNames. +func (mr *MockPermissionRepositoryMockRecorder) FindByNames(ctx, names any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByNames", reflect.TypeOf((*MockPermissionRepository)(nil).FindByNames), ctx, names) +} + +// FindOne mocks base method. +func (m *MockPermissionRepository) FindOne(ctx context.Context, query repository.PermissionQuery) (entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, query) + ret0, _ := ret[0].(entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockPermissionRepositoryMockRecorder) FindOne(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockPermissionRepository)(nil).FindOne), ctx, query) +} + +// GetAll mocks base method. +func (m *MockPermissionRepository) GetAll(ctx context.Context, status *permission.Status) ([]entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx, status) + ret0, _ := ret[0].([]entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockPermissionRepositoryMockRecorder) GetAll(ctx, status any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockPermissionRepository)(nil).GetAll), ctx, status) +} + +// GetAllIntoIDMap mocks base method. +func (m *MockPermissionRepository) GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllIntoIDMap", ctx, status) + ret0, _ := ret[0].(map[string]entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllIntoIDMap indicates an expected call of GetAllIntoIDMap. +func (mr *MockPermissionRepositoryMockRecorder) GetAllIntoIDMap(ctx, status any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllIntoIDMap", reflect.TypeOf((*MockPermissionRepository)(nil).GetAllIntoIDMap), ctx, status) +} + +// Index20250214UP mocks base method. +func (m *MockPermissionRepository) Index20250214UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250214UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250214UP indicates an expected call of Index20250214UP. +func (mr *MockPermissionRepositoryMockRecorder) Index20250214UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250214UP", reflect.TypeOf((*MockPermissionRepository)(nil).Index20250214UP), ctx) +} + +// Insert mocks base method. +func (m *MockPermissionRepository) Insert(ctx context.Context, permission entity.Permission) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, permission) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockPermissionRepositoryMockRecorder) Insert(ctx, permission any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockPermissionRepository)(nil).Insert), ctx, permission) +} + +// Update mocks base method. +func (m *MockPermissionRepository) Update(ctx context.Context, id string, req repository.UpdatePermission) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, id, req) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockPermissionRepositoryMockRecorder) Update(ctx, id, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPermissionRepository)(nil).Update), ctx, id, req) +} + +// MockGetPermission is a mock of GetPermission interface. +type MockGetPermission struct { + ctrl *gomock.Controller + recorder *MockGetPermissionMockRecorder + isgomock struct{} +} + +// MockGetPermissionMockRecorder is the mock recorder for MockGetPermission. +type MockGetPermissionMockRecorder struct { + mock *MockGetPermission +} + +// NewMockGetPermission creates a new mock instance. +func NewMockGetPermission(ctrl *gomock.Controller) *MockGetPermission { + mock := &MockGetPermission{ctrl: ctrl} + mock.recorder = &MockGetPermissionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGetPermission) EXPECT() *MockGetPermissionMockRecorder { + return m.recorder +} + +// FindByNames mocks base method. +func (m *MockGetPermission) FindByNames(ctx context.Context, names []string) ([]entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByNames", ctx, names) + ret0, _ := ret[0].([]entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByNames indicates an expected call of FindByNames. +func (mr *MockGetPermissionMockRecorder) FindByNames(ctx, names any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByNames", reflect.TypeOf((*MockGetPermission)(nil).FindByNames), ctx, names) +} + +// FindOne mocks base method. +func (m *MockGetPermission) FindOne(ctx context.Context, query repository.PermissionQuery) (entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, query) + ret0, _ := ret[0].(entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockGetPermissionMockRecorder) FindOne(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockGetPermission)(nil).FindOne), ctx, query) +} + +// GetAll mocks base method. +func (m *MockGetPermission) GetAll(ctx context.Context, status *permission.Status) ([]entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx, status) + ret0, _ := ret[0].([]entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockGetPermissionMockRecorder) GetAll(ctx, status any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockGetPermission)(nil).GetAll), ctx, status) +} + +// GetAllIntoIDMap mocks base method. +func (m *MockGetPermission) GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllIntoIDMap", ctx, status) + ret0, _ := ret[0].(map[string]entity.Permission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllIntoIDMap indicates an expected call of GetAllIntoIDMap. +func (mr *MockGetPermissionMockRecorder) GetAllIntoIDMap(ctx, status any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllIntoIDMap", reflect.TypeOf((*MockGetPermission)(nil).GetAllIntoIDMap), ctx, status) +} + +// MockIndex is a mock of Index interface. +type MockIndex struct { + ctrl *gomock.Controller + recorder *MockIndexMockRecorder + isgomock struct{} +} + +// MockIndexMockRecorder is the mock recorder for MockIndex. +type MockIndexMockRecorder struct { + mock *MockIndex +} + +// NewMockIndex creates a new mock instance. +func NewMockIndex(ctrl *gomock.Controller) *MockIndex { + mock := &MockIndex{ctrl: ctrl} + mock.recorder = &MockIndexMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIndex) EXPECT() *MockIndexMockRecorder { + return m.recorder +} + +// Index20250214UP mocks base method. +func (m *MockIndex) Index20250214UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250214UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250214UP indicates an expected call of Index20250214UP. +func (mr *MockIndexMockRecorder) Index20250214UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250214UP", reflect.TypeOf((*MockIndex)(nil).Index20250214UP), ctx) +} diff --git a/pkg/mock/repository/role.go b/pkg/mock/repository/role.go new file mode 100644 index 0000000..5e24906 --- /dev/null +++ b/pkg/mock/repository/role.go @@ -0,0 +1,201 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/role.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/role.go -destination=./pkg/mock/repository/role.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + repository "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockRoleRepository is a mock of RoleRepository interface. +type MockRoleRepository struct { + ctrl *gomock.Controller + recorder *MockRoleRepositoryMockRecorder + isgomock struct{} +} + +// MockRoleRepositoryMockRecorder is the mock recorder for MockRoleRepository. +type MockRoleRepositoryMockRecorder struct { + mock *MockRoleRepository +} + +// NewMockRoleRepository creates a new mock instance. +func NewMockRoleRepository(ctrl *gomock.Controller) *MockRoleRepository { + mock := &MockRoleRepository{ctrl: ctrl} + mock.recorder = &MockRoleRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRoleRepository) EXPECT() *MockRoleRepositoryMockRecorder { + return m.recorder +} + +// All mocks base method. +func (m *MockRoleRepository) All(ctx context.Context, clientID *string) ([]*entity.Role, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "All", ctx, clientID) + ret0, _ := ret[0].([]*entity.Role) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// All indicates an expected call of All. +func (mr *MockRoleRepositoryMockRecorder) All(ctx, clientID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockRoleRepository)(nil).All), ctx, clientID) +} + +// Create mocks base method. +func (m *MockRoleRepository) Create(ctx context.Context, role *entity.Role) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, role) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockRoleRepositoryMockRecorder) Create(ctx, role any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRoleRepository)(nil).Create), ctx, role) +} + +// Delete mocks base method. +func (m *MockRoleRepository) Delete(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRoleRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRoleRepository)(nil).Delete), ctx, id) +} + +// GetByID mocks base method. +func (m *MockRoleRepository) GetByID(ctx context.Context, id string) (*entity.Role, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByID", ctx, id) + ret0, _ := ret[0].(*entity.Role) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByID indicates an expected call of GetByID. +func (mr *MockRoleRepositoryMockRecorder) GetByID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockRoleRepository)(nil).GetByID), ctx, id) +} + +// GetByUID mocks base method. +func (m *MockRoleRepository) GetByUID(ctx context.Context, uid string) (*entity.Role, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByUID", ctx, uid) + ret0, _ := ret[0].(*entity.Role) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByUID indicates an expected call of GetByUID. +func (mr *MockRoleRepositoryMockRecorder) GetByUID(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByUID", reflect.TypeOf((*MockRoleRepository)(nil).GetByUID), ctx, uid) +} + +// Index20250224UP mocks base method. +func (m *MockRoleRepository) Index20250224UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250224UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250224UP indicates an expected call of Index20250224UP. +func (mr *MockRoleRepositoryMockRecorder) Index20250224UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250224UP", reflect.TypeOf((*MockRoleRepository)(nil).Index20250224UP), ctx) +} + +// List mocks base method. +func (m *MockRoleRepository) List(ctx context.Context, param repository.ListQuery) ([]*entity.Role, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, param) + ret0, _ := ret[0].([]*entity.Role) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// List indicates an expected call of List. +func (mr *MockRoleRepositoryMockRecorder) List(ctx, param any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRoleRepository)(nil).List), ctx, param) +} + +// Update mocks base method. +func (m *MockRoleRepository) Update(ctx context.Context, data repository.UpdateReq) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockRoleRepositoryMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRoleRepository)(nil).Update), ctx, data) +} + +// MockRoleIndex is a mock of RoleIndex interface. +type MockRoleIndex struct { + ctrl *gomock.Controller + recorder *MockRoleIndexMockRecorder + isgomock struct{} +} + +// MockRoleIndexMockRecorder is the mock recorder for MockRoleIndex. +type MockRoleIndexMockRecorder struct { + mock *MockRoleIndex +} + +// NewMockRoleIndex creates a new mock instance. +func NewMockRoleIndex(ctrl *gomock.Controller) *MockRoleIndex { + mock := &MockRoleIndex{ctrl: ctrl} + mock.recorder = &MockRoleIndexMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRoleIndex) EXPECT() *MockRoleIndexMockRecorder { + return m.recorder +} + +// Index20250224UP mocks base method. +func (m *MockRoleIndex) Index20250224UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250224UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250224UP indicates an expected call of Index20250224UP. +func (mr *MockRoleIndexMockRecorder) Index20250224UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250224UP", reflect.TypeOf((*MockRoleIndex)(nil).Index20250224UP), ctx) +} diff --git a/pkg/mock/repository/role_permission.go b/pkg/mock/repository/role_permission.go new file mode 100644 index 0000000..6acb35d --- /dev/null +++ b/pkg/mock/repository/role_permission.go @@ -0,0 +1,155 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/role_permission.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/role_permission.go -destination=./pkg/mock/repository/role_permission.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockRolePermissionRepository is a mock of RolePermissionRepository interface. +type MockRolePermissionRepository struct { + ctrl *gomock.Controller + recorder *MockRolePermissionRepositoryMockRecorder + isgomock struct{} +} + +// MockRolePermissionRepositoryMockRecorder is the mock recorder for MockRolePermissionRepository. +type MockRolePermissionRepositoryMockRecorder struct { + mock *MockRolePermissionRepository +} + +// NewMockRolePermissionRepository creates a new mock instance. +func NewMockRolePermissionRepository(ctrl *gomock.Controller) *MockRolePermissionRepository { + mock := &MockRolePermissionRepository{ctrl: ctrl} + mock.recorder = &MockRolePermissionRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRolePermissionRepository) EXPECT() *MockRolePermissionRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRolePermissionRepository) Create(ctx context.Context, entity []entity.RolePermission) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, entity) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockRolePermissionRepositoryMockRecorder) Create(ctx, entity any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRolePermissionRepository)(nil).Create), ctx, entity) +} + +// Delete mocks base method. +func (m *MockRolePermissionRepository) Delete(ctx context.Context, roleID string, permissions []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, roleID, permissions) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRolePermissionRepositoryMockRecorder) Delete(ctx, roleID, permissions any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRolePermissionRepository)(nil).Delete), ctx, roleID, permissions) +} + +// Get mocks base method. +func (m *MockRolePermissionRepository) Get(ctx context.Context, roleID string) ([]*entity.RolePermission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, roleID) + ret0, _ := ret[0].([]*entity.RolePermission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockRolePermissionRepositoryMockRecorder) Get(ctx, roleID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRolePermissionRepository)(nil).Get), ctx, roleID) +} + +// GetByPermissionID mocks base method. +func (m *MockRolePermissionRepository) GetByPermissionID(ctx context.Context, permissionIDs []string) ([]*entity.RolePermission, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByPermissionID", ctx, permissionIDs) + ret0, _ := ret[0].([]*entity.RolePermission) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByPermissionID indicates an expected call of GetByPermissionID. +func (mr *MockRolePermissionRepositoryMockRecorder) GetByPermissionID(ctx, permissionIDs any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByPermissionID", reflect.TypeOf((*MockRolePermissionRepository)(nil).GetByPermissionID), ctx, permissionIDs) +} + +// Index20250225UP mocks base method. +func (m *MockRolePermissionRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250225UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250225UP indicates an expected call of Index20250225UP. +func (mr *MockRolePermissionRepositoryMockRecorder) Index20250225UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250225UP", reflect.TypeOf((*MockRolePermissionRepository)(nil).Index20250225UP), ctx) +} + +// MockRolePermissionIndex is a mock of RolePermissionIndex interface. +type MockRolePermissionIndex struct { + ctrl *gomock.Controller + recorder *MockRolePermissionIndexMockRecorder + isgomock struct{} +} + +// MockRolePermissionIndexMockRecorder is the mock recorder for MockRolePermissionIndex. +type MockRolePermissionIndexMockRecorder struct { + mock *MockRolePermissionIndex +} + +// NewMockRolePermissionIndex creates a new mock instance. +func NewMockRolePermissionIndex(ctrl *gomock.Controller) *MockRolePermissionIndex { + mock := &MockRolePermissionIndex{ctrl: ctrl} + mock.recorder = &MockRolePermissionIndexMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRolePermissionIndex) EXPECT() *MockRolePermissionIndexMockRecorder { + return m.recorder +} + +// Index20250225UP mocks base method. +func (m *MockRolePermissionIndex) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250225UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250225UP indicates an expected call of Index20250225UP. +func (mr *MockRolePermissionIndexMockRecorder) Index20250225UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250225UP", reflect.TypeOf((*MockRolePermissionIndex)(nil).Index20250225UP), ctx) +} 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/mock/repository/user_role.go b/pkg/mock/repository/user_role.go new file mode 100644 index 0000000..b2f9dfd --- /dev/null +++ b/pkg/mock/repository/user_role.go @@ -0,0 +1,187 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/user_role.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/user_role.go -destination=./pkg/mock/repository/user_role.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + repository "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockUserRoleRepository is a mock of UserRoleRepository interface. +type MockUserRoleRepository struct { + ctrl *gomock.Controller + recorder *MockUserRoleRepositoryMockRecorder + isgomock struct{} +} + +// MockUserRoleRepositoryMockRecorder is the mock recorder for MockUserRoleRepository. +type MockUserRoleRepositoryMockRecorder struct { + mock *MockUserRoleRepository +} + +// NewMockUserRoleRepository creates a new mock instance. +func NewMockUserRoleRepository(ctrl *gomock.Controller) *MockUserRoleRepository { + mock := &MockUserRoleRepository{ctrl: ctrl} + mock.recorder = &MockUserRoleRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserRoleRepository) EXPECT() *MockUserRoleRepositoryMockRecorder { + return m.recorder +} + +// CountUsersByRole mocks base method. +func (m *MockUserRoleRepository) CountUsersByRole(ctx context.Context) ([]repository.RoleUserCount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CountUsersByRole", ctx) + ret0, _ := ret[0].([]repository.RoleUserCount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountUsersByRole indicates an expected call of CountUsersByRole. +func (mr *MockUserRoleRepositoryMockRecorder) CountUsersByRole(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountUsersByRole", reflect.TypeOf((*MockUserRoleRepository)(nil).CountUsersByRole), ctx) +} + +// CreateUserRole mocks base method. +func (m *MockUserRoleRepository) CreateUserRole(ctx context.Context, param entity.UserRole) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserRole", ctx, param) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateUserRole indicates an expected call of CreateUserRole. +func (mr *MockUserRoleRepositoryMockRecorder) CreateUserRole(ctx, param any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserRole", reflect.TypeOf((*MockUserRoleRepository)(nil).CreateUserRole), ctx, param) +} + +// GetAll mocks base method. +func (m *MockUserRoleRepository) GetAll(ctx context.Context) ([]*entity.UserRole, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx) + ret0, _ := ret[0].([]*entity.UserRole) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockUserRoleRepositoryMockRecorder) GetAll(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockUserRoleRepository)(nil).GetAll), ctx) +} + +// GetByUserID mocks base method. +func (m *MockUserRoleRepository) GetByUserID(ctx context.Context, uid string) (entity.UserRole, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByUserID", ctx, uid) + ret0, _ := ret[0].(entity.UserRole) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByUserID indicates an expected call of GetByUserID. +func (mr *MockUserRoleRepositoryMockRecorder) GetByUserID(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByUserID", reflect.TypeOf((*MockUserRoleRepository)(nil).GetByUserID), ctx, uid) +} + +// GetUsersByRoleID mocks base method. +func (m *MockUserRoleRepository) GetUsersByRoleID(ctx context.Context, roleID string) ([]entity.UserRole, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsersByRoleID", ctx, roleID) + ret0, _ := ret[0].([]entity.UserRole) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUsersByRoleID indicates an expected call of GetUsersByRoleID. +func (mr *MockUserRoleRepositoryMockRecorder) GetUsersByRoleID(ctx, roleID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByRoleID", reflect.TypeOf((*MockUserRoleRepository)(nil).GetUsersByRoleID), ctx, roleID) +} + +// Index20250225UP mocks base method. +func (m *MockUserRoleRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250225UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250225UP indicates an expected call of Index20250225UP. +func (mr *MockUserRoleRepositoryMockRecorder) Index20250225UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250225UP", reflect.TypeOf((*MockUserRoleRepository)(nil).Index20250225UP), ctx) +} + +// UpdateUserRole mocks base method. +func (m *MockUserRoleRepository) UpdateUserRole(ctx context.Context, uid, roleID string) (entity.UserRole, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserRole", ctx, uid, roleID) + ret0, _ := ret[0].(entity.UserRole) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserRole indicates an expected call of UpdateUserRole. +func (mr *MockUserRoleRepositoryMockRecorder) UpdateUserRole(ctx, uid, roleID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserRole", reflect.TypeOf((*MockUserRoleRepository)(nil).UpdateUserRole), ctx, uid, roleID) +} + +// MockUserRoleIndex is a mock of UserRoleIndex interface. +type MockUserRoleIndex struct { + ctrl *gomock.Controller + recorder *MockUserRoleIndexMockRecorder + isgomock struct{} +} + +// MockUserRoleIndexMockRecorder is the mock recorder for MockUserRoleIndex. +type MockUserRoleIndexMockRecorder struct { + mock *MockUserRoleIndex +} + +// NewMockUserRoleIndex creates a new mock instance. +func NewMockUserRoleIndex(ctrl *gomock.Controller) *MockUserRoleIndex { + mock := &MockUserRoleIndex{ctrl: ctrl} + mock.recorder = &MockUserRoleIndexMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserRoleIndex) EXPECT() *MockUserRoleIndexMockRecorder { + return m.recorder +} + +// Index20250225UP mocks base method. +func (m *MockUserRoleIndex) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20250225UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20250225UP indicates an expected call of Index20250225UP. +func (mr *MockUserRoleIndexMockRecorder) Index20250225UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20250225UP", reflect.TypeOf((*MockUserRoleIndex)(nil).Index20250225UP), ctx) +} diff --git a/pkg/repository/casbin_redis_adapter.go b/pkg/repository/casbin_redis_adapter.go new file mode 100644 index 0000000..9f7f81c --- /dev/null +++ b/pkg/repository/casbin_redis_adapter.go @@ -0,0 +1,150 @@ +package repository + +import ( + "context" + "encoding/json" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/rbac" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + "github.com/casbin/casbin/v2/model" + "github.com/casbin/casbin/v2/persist" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +type RBACAdapterParam struct { + Redis *redis.Redis +} + +type rbacAdapter struct { + redis *redis.Redis +} + +func NewRBACAdapter(param RBACAdapterParam) (repository.RBACAdapter, error) { + return &rbacAdapter{ + redis: param.Redis, + }, nil +} + +// loadPolicyByString 真正將 Rule 存到 casbin 讓他可以使用的地方 +func loadPolicyByString(line rbac.Rule, model model.Model) error { + text := line.ToString() + // 雖然是從Redis 來,不過最終還是有一份會存在記憶體當中,這樣做的原因只是不要存第二次 + err := persist.LoadPolicyArray(text, model) + if err != nil { + return err + } + + return nil +} + +// LoadPolicy 將檔案從記憶體當中載入本地 Casbin +func (adapter *rbacAdapter) LoadPolicy(model model.Model) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + rk := domain.GetCasbinRuleRedisKey() + stop, err := adapter.redis.LlenCtx(ctx, rk) + if err != nil { + return err + } + + rules, err := adapter.redis.LrangeCtx(ctx, rk, 0, stop) + if err != nil { + return err + } + + for _, ruleStr := range rules { + var rule rbac.Rule + err := json.Unmarshal([]byte(ruleStr), &rule) + if err != nil { + return err + } + + err = loadPolicyByString(rule, model) + if err != nil { + return err + } + } + + return nil +} + +// SavePolicy saves policy to database. 將 Rule 一次就從資料庫中存到 redis 裡面 -> 打API 初始化 Redis 同步的資料 +func (adapter *rbacAdapter) SavePolicy(model model.Model) error { + rk := domain.GetCasbinRuleRedisKey() + _, err := adapter.redis.Del(rk) + if err != nil { + return err + } + + var texts []string + + for policy, ast := range model["p"] { + for _, rule := range ast.Policy { + line := rbac.StringToPolicy(policy, rule) + text, err := json.Marshal(line) + if err != nil { + return err + } + texts = append(texts, string(text)) + } + } + + for policy, ast := range model["g"] { + for _, rule := range ast.Policy { + line := rbac.StringToPolicy(policy, rule) + text, err := json.Marshal(line) + if err != nil { + return err + } + texts = append(texts, string(text)) + } + } + + _, err = adapter.redis.Rpush(rk, texts) + if err != nil { + return err + } + + return nil +} + +// AddPolicy adds a policy rule to the storage. +func (adapter *rbacAdapter) AddPolicy(_ string, policyType string, rule []string) error { + policy := rbac.StringToPolicy(policyType, rule) + text, err := json.Marshal(policy) + if err != nil { + return err + } + + rk := domain.GetCasbinRuleRedisKey() + _, err = adapter.redis.Rpush(rk, text) + if err != nil { + return err + } + + return nil +} + +func (adapter *rbacAdapter) RemovePolicy(_ string, policyType string, rule []string) error { + policy := rbac.StringToPolicy(policyType, rule) + text, err := json.Marshal(policy) + if err != nil { + return err + } + + rk := domain.GetCasbinRuleRedisKey() + _, err = adapter.redis.Lrem(rk, 1, string(text)) + if err != nil { + return err + } + + return nil +} + +// RemoveFilteredPolicy 沒用到先不實作 +func (adapter *rbacAdapter) RemoveFilteredPolicy(_ string, _ string, _ int, _ ...string) error { + return nil +} diff --git a/pkg/repository/casbin_redis_adapter_test.go b/pkg/repository/casbin_redis_adapter_test.go new file mode 100644 index 0000000..ec442c8 --- /dev/null +++ b/pkg/repository/casbin_redis_adapter_test.go @@ -0,0 +1,196 @@ +package repository + +import ( + "context" + "encoding/json" + "testing" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/rbac" + "github.com/casbin/casbin/v2/model" + "github.com/stretchr/testify/assert" +) + +func Test_rbacAdapter_AddPolicy(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + adapter, err := NewRBACAdapter(RBACAdapterParam{Redis: r}) + assert.NoError(t, err) + + // 新增策略 + err = adapter.AddPolicy("", "p", []string{"user", "data2", "read"}) + assert.NoError(t, err) + + // 驗證 Redis 內的數據 + rk := domain.GetCasbinRuleRedisKey() + data, err := r.LrangeCtx(context.Background(), rk, 0, -1) + assert.NoError(t, err) + assert.Len(t, data, 1) + + // 解析 JSON 確認內容 + var policy rbac.Rule + err = json.Unmarshal([]byte(data[0]), &policy) + assert.NoError(t, err) + assert.Equal(t, "p", policy.PolicyType) + assert.Equal(t, "user", policy.Field0) + assert.Equal(t, "data2", policy.Field1) + assert.Equal(t, "read", policy.Field2) +} + +// 測試 `RemovePolicy` +func TestRBACAdapter_RemovePolicy(t *testing.T) { + mr, rdb := setupMiniRedis() + defer mr.Close() + + adapter, err := NewRBACAdapter(RBACAdapterParam{Redis: rdb}) + assert.NoError(t, err) + + // 新增策略 + err = adapter.AddPolicy("", "p", []string{"user", "data2", "read"}) + assert.NoError(t, err) + + // 確保策略存在 + rk := domain.GetCasbinRuleRedisKey() + data, err := rdb.LrangeCtx(context.Background(), rk, 0, -1) + assert.NoError(t, err) + assert.Len(t, data, 1) + + // 刪除策略 + err = adapter.RemovePolicy("", "p", []string{"user", "data2", "read"}) + assert.NoError(t, err) + + // 確保 Redis 已經刪除該策略 + data, err = rdb.LrangeCtx(context.Background(), rk, 0, -1) + assert.NoError(t, err) + assert.Len(t, data, 0) +} + +// 測試 SavePolicy: 確保能夠存入 Redis +func TestRBACAdapter_SavePolicy(t *testing.T) { + mr, rdb := setupMiniRedis() + defer mr.Close() + + adapter, err := NewRBACAdapter(RBACAdapterParam{Redis: rdb}) + assert.NoError(t, err) + + // 準備 Casbin Model + m, _ := model.NewModelFromString(` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act + `) + + // 加入測試策略 + m.AddPolicy("p", "p", []string{"admin", "data1", "read"}) + m.AddPolicy("p", "p", []string{"admin", "data1", "write"}) + m.AddPolicy("p", "g", []string{"alice", "admin"}) + + // 確保 Redis 內最開始是空的 + rk := domain.GetCasbinRuleRedisKey() + rulesBefore, _ := rdb.LrangeCtx(context.Background(), rk, 0, -1) + assert.Len(t, rulesBefore, 0) + + // 執行 SavePolicy + err = adapter.SavePolicy(m) + assert.NoError(t, err) + + // 確保 Redis 內的數據與 `policy` 一致 + rulesAfter, err := rdb.LrangeCtx(context.Background(), rk, 0, -1) + assert.NoError(t, err) + + // 解析 JSON 並確認內容 + expectedPolicies := [][]string{ + {"p", "admin", "data1", "read"}, + {"p", "admin", "data1", "write"}, + {"g", "alice", "admin"}, + } + + for i, ruleStr := range rulesAfter { + var rule rbac.Rule + err := json.Unmarshal([]byte(ruleStr), &rule) + assert.NoError(t, err) + + assert.Equal(t, expectedPolicies[i][0], rule.PolicyType) + assert.Equal(t, expectedPolicies[i][1], rule.Field0) + assert.Equal(t, expectedPolicies[i][2], rule.Field1) + if len(expectedPolicies[i]) > 3 { + assert.Equal(t, expectedPolicies[i][3], rule.Field2) + } + } +} + +// 測試 SavePolicy: Redis 寫入失敗 +func TestRBACAdapter_SavePolicy_RedisError(t *testing.T) { + mr, rdb := setupMiniRedis() + defer mr.Close() + + adapter, err := NewRBACAdapter(RBACAdapterParam{Redis: rdb}) + assert.NoError(t, err) + + m, _ := model.NewModelFromString(` + [policy_definition] + p = sub, obj, act + `) + m.AddPolicy("p", "p", []string{"admin", "data1", "read"}) + + err = adapter.SavePolicy(m) + assert.Error(t, err) // 應該要回傳錯誤 +} + +// 測試 `LoadPolicy` 是否能從 Redis 讀取 `p` 和 `g` 規則 +func TestRBACAdapter_LoadPolicy(t *testing.T) { + mr, rdb := setupMiniRedis() + defer mr.Close() + + adapter, err := NewRBACAdapter(RBACAdapterParam{Redis: rdb}) + assert.NoError(t, err) + + // 預先寫入 Redis 測試數據 + rk := domain.GetCasbinRuleRedisKey() + testRules := []rbac.Rule{ + {PolicyType: "p", Field0: "admin", Field1: "data1", Field2: "read"}, + {PolicyType: "p", Field0: "admin", Field1: "data1", Field2: "write"}, + {PolicyType: "g", Field0: "alice", Field1: "admin"}, + {PolicyType: "g", Field0: "bob", Field1: "user"}, + } + + for _, rule := range testRules { + data, _ := json.Marshal(rule) + _, err := rdb.Rpush(rk, string(data)) + assert.NoError(t, err) + } + + // 創建 Casbin Model + m, _ := model.NewModelFromString(` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act + `) + + // 測試 `LoadPolicy` + err = adapter.LoadPolicy(m) + assert.NoError(t, err) +} diff --git a/pkg/repository/error.go b/pkg/repository/error.go new file mode 100644 index 0000000..c98cf79 --- /dev/null +++ b/pkg/repository/error.go @@ -0,0 +1,12 @@ +package repository + +import ( + "errors" + + "github.com/zeromicro/go-zero/core/stores/mon" +) + +var ( + ErrNotFound = mon.ErrNotFound + ErrInvalidObjectID = errors.New("invalid objectId") +) diff --git a/pkg/repository/permission.go b/pkg/repository/permission.go new file mode 100644 index 0000000..f84ed90 --- /dev/null +++ b/pkg/repository/permission.go @@ -0,0 +1,176 @@ +package repository + +import ( + "context" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type PermissionRepositoryParam struct { + Conf *mgo.Conf + DBOpts []mon.Option +} + +type PermissionRepository struct { + DB mgo.DocumentDBUseCase +} + +// TODO 量小不需要 Redis 以後有需要再增加 + +func NewPermissionRepository(param PermissionRepositoryParam) repository.PermissionRepository { + e := entity.Permission{} + db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...) + if err != nil { + panic(err) + } + + return &PermissionRepository{ + DB: db, + } +} + +func (repo *PermissionRepository) Insert(ctx context.Context, data entity.Permission) error { + if data.ID.IsZero() { + now := time.Now().UTC().UnixNano() + data.ID = primitive.NewObjectID() + data.CreateAt = now + data.UpdateAt = now + } + + _, err := repo.DB.GetClient().InsertOne(ctx, data) + + return err +} + +func (repo *PermissionRepository) Update(ctx context.Context, id string, req repository.UpdatePermission) error { + now := time.Now().UTC().UnixNano() + // 動態構建更新內容 + updateFields := bson.M{ + "update_at": now, // 確保 `updateAt` 總是更新 + } + if req.Name != nil { + updateFields["name"] = *req.Name + } + + if req.HTTPMethod != nil { + updateFields["http_method"] = *req.HTTPMethod + } + + if req.HTTPPath != nil { + updateFields["http_path"] = *req.HTTPPath + } + + if req.Status != nil { + updateFields["status"] = *req.Status + } + + if req.Type != nil { + updateFields["type"] = *req.Type + } + + if req.ParentID != nil { + updateFields["parent"] = *req.ParentID + } + + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return ErrInvalidObjectID + } + + _, err = repo.DB.GetClient().UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": updateFields}) + if err != nil { + return err + } + + return nil +} + +func (repo *PermissionRepository) Delete(ctx context.Context, id string) error { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return ErrInvalidObjectID + } + + _, err = repo.DB.GetClient().DeleteOne(ctx, bson.M{"_id": oid}) + if err != nil { + return err + } + + return nil +} + +func (repo *PermissionRepository) GetAll(ctx context.Context, status *permission.Status) ([]entity.Permission, error) { + filter := bson.M{} + if status != nil { + filter["status"] = status + } + opt := options.Find().SetSort(bson.M{"id": 1}) + + result := make([]entity.Permission, 0) + err := repo.DB.GetClient().Find(ctx, &result, filter, opt) + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo *PermissionRepository) GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) { + permissions, err := repo.GetAll(ctx, status) + if err != nil { + return nil, err + } + + permissionMap := make(map[string]entity.Permission) + for _, v := range permissions { + permissionMap[v.Name] = v + } + + return permissionMap, nil +} + +func (repo *PermissionRepository) FindOne(ctx context.Context, query repository.PermissionQuery) (entity.Permission, error) { + filter := bson.M{"http_method": query.HTTPMethod, "http_path": query.HTTPPath} + var result entity.Permission + + err := repo.DB.GetClient().FindOne(ctx, &result, filter) + if err != nil { + return entity.Permission{}, err + } + + return result, nil +} + +func (repo *PermissionRepository) FindByNames(ctx context.Context, names []string) ([]entity.Permission, error) { + result := make([]entity.Permission, 0) + // 使用 $in 操作符查詢 name 在 names 切片中的文件 + filter := bson.M{"name": bson.M{"$in": names}} + err := repo.DB.GetClient().Find(ctx, &result, filter) + if err != nil { + return []entity.Permission{}, err + } + + return result, nil +} + +func (repo *PermissionRepository) Index20250214UP(ctx context.Context) (*mongo.Cursor, error) { + // 等價於 db.account.createIndex({ "http_method": 1, "http_path": 1}, {unique: false}) + repo.DB.PopulateMultiIndex(ctx, []string{ + "http_method", + "http_path", + }, []int32{1, 1}, false) + + // 等價於 db.account.createIndex({"create_at": 1}) + repo.DB.PopulateIndex(ctx, "name", 1, true) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/permission_test.go b/pkg/repository/permission_test.go new file mode 100644 index 0000000..6fd5f2b --- /dev/null +++ b/pkg/repository/permission_test.go @@ -0,0 +1,474 @@ +package repository + +import ( + "context" + "fmt" + "testing" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func SetupTestPermissionRepository(db string) (repository.PermissionRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + + conf := &mgo.Conf{ + Schema: Schema, + Host: fmt.Sprintf("%s:%s", h, p), + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + param := PermissionRepositoryParam{ + Conf: conf, + } + repo := NewPermissionRepository(param) + _, _ = repo.Index20250214UP(context.Background()) + + return repo, tearDown, nil +} + +// 測試 Insert +func TestPermissionRepository_Insert(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input entity.Permission + expectErr bool + }{ + { + name: "成功插入", + input: entity.Permission{ + Name: "test-permission", + HTTPMethod: "GET", + HTTPPath: "/test", + Status: 1, + Type: permission.BackendUser, + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Insert(context.Background(), tc.input) + if tc.expectErr { + assert.NotNil(t, err, "應該要返回錯誤") + } else { + assert.Nil(t, err, "不應該返回錯誤") + } + }) + } +} + +func TestPermissionRepository_Update(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先插入一條測試數據 + existingPermission := entity.Permission{ + Name: "original-permission", + HTTPMethod: "GET", + HTTPPath: "/original", + Status: 1, + Type: permission.BackendUser, + } + err = repo.Insert(context.Background(), existingPermission) + assert.Nil(t, err, "插入初始數據失敗") + + // 取得剛插入的 ID + found, err := repo.FindOne(context.Background(), repository.PermissionQuery{ + HTTPMethod: existingPermission.HTTPMethod, + HTTPPath: existingPermission.HTTPPath, + }) + assert.Nil(t, err, "應該能找到插入的權限") + id := found.ID.Hex() + + testCases := []struct { + name string + id string + updateReq repository.UpdatePermission + expectErr bool + }{ + { + name: "成功更新名稱", + id: id, + updateReq: repository.UpdatePermission{ + Name: ToPointer("updated-name"), + }, + expectErr: false, + }, + { + name: "成功更新 HTTP 方法", + id: id, + updateReq: repository.UpdatePermission{ + HTTPMethod: ToPointer("PUT"), + }, + expectErr: false, + }, + { + name: "成功更新 HTTP 路徑", + id: id, + updateReq: repository.UpdatePermission{ + HTTPPath: ToPointer("/updated-path"), + }, + expectErr: false, + }, + { + name: "成功更新狀態", + id: id, + updateReq: repository.UpdatePermission{ + Status: ToPointer(permission.Open), + }, + expectErr: false, + }, + { + name: "成功更新多個欄位", + id: id, + updateReq: repository.UpdatePermission{ + Name: ToPointer("multi-updated"), + HTTPMethod: ToPointer("PATCH"), + HTTPPath: ToPointer("/multi-update"), + Status: ToPointer(permission.Close), + }, + expectErr: false, + }, + { + name: "更新失敗 - 錯誤的 ObjectID", + id: "invalid", + updateReq: repository.UpdatePermission{ + Name: ToPointer("invalid-update"), + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Update(context.Background(), tc.id, tc.updateReq) + if tc.expectErr { + assert.NotNil(t, err, "應該要返回錯誤") + } else { + assert.Nil(t, err, "不應該返回錯誤") + + // 確保更新後的資料正確 + updated, err := repo.GetAll(context.Background(), nil) + assert.Nil(t, err, "應該能找到更新後的權限") + + if tc.updateReq.Name != nil { + assert.Equal(t, *tc.updateReq.Name, updated[0].Name, "名稱應該被更新") + } + if tc.updateReq.HTTPMethod != nil { + assert.Equal(t, *tc.updateReq.HTTPMethod, updated[0].HTTPMethod, "HTTP 方法應該被更新") + } + if tc.updateReq.HTTPPath != nil { + assert.Equal(t, *tc.updateReq.HTTPPath, updated[0].HTTPPath, "HTTP 路徑應該被更新") + } + if tc.updateReq.Status != nil { + assert.Equal(t, *tc.updateReq.Status, updated[0].Status, "狀態應該被更新") + } + } + }) + } +} + +// 測試 Delete 方法 +func TestPermissionRepository_Delete(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + permission := entity.Permission{ + ID: primitive.NewObjectID(), + Name: "delete-test", + HTTPMethod: "DELETE", + HTTPPath: "/test-delete", + Status: 1, + } + + err = repo.Insert(context.Background(), permission) + assert.NoError(t, err, "插入測試數據時不應發生錯誤") + + testCases := []struct { + name string + id string + expectErr bool + }{ + { + name: "成功刪除存在的權限", + id: permission.ID.Hex(), + expectErr: false, + }, + { + name: "刪除不存在的權限", + id: primitive.NewObjectID().Hex(), + expectErr: false, // MongoDB 刪除不存在的 ID 仍然不會報錯 + }, + { + name: "刪除無效的 ObjectID", + id: "invalid-object-id", + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Delete(context.Background(), tc.id) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + }) + } +} + +// 測試 GetAll 方法 +func TestPermissionRepository_GetAll(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testPermissions := []entity.Permission{ + { + ID: primitive.NewObjectID(), + Name: "read", + HTTPMethod: "GET", + HTTPPath: "/read", + Status: permission.Open, + }, + { + ID: primitive.NewObjectID(), + Name: "write", + HTTPMethod: "POST", + HTTPPath: "/write", + Status: permission.Close, + }, + } + + for _, p := range testPermissions { + err := repo.Insert(context.Background(), p) + assert.NoError(t, err, "插入測試數據時不應該發生錯誤") + } + + testCases := []struct { + name string + status *permission.Status + expectLen int + expectErr bool + }{ + { + name: "查詢所有權限", + status: nil, + expectLen: 2, + expectErr: false, + }, + { + name: "查詢開啟的權限", + status: ToPointer(permission.Open), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢關閉的權限", + status: ToPointer(permission.Close), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢不存在的權限狀態", + status: ToPointer(permission.Status(-1)), + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetAll(context.Background(), tc.status) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "查詢結果數量不符合預期") + } + }) + } +} + +// 測試 GetAllIntoIDMap 方法 +func TestPermissionRepository_GetAllIntoIDMap(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testPermissions := []entity.Permission{ + { + ID: primitive.NewObjectID(), + Name: "read", + HTTPMethod: "GET", + HTTPPath: "/read", + Status: permission.Open, + }, + { + ID: primitive.NewObjectID(), + Name: "write", + HTTPMethod: "POST", + HTTPPath: "/write", + Status: permission.Close, + }, + } + + for _, p := range testPermissions { + err := repo.Insert(context.Background(), p) + assert.NoError(t, err, "插入測試數據時不應該發生錯誤") + } + + testCases := []struct { + name string + status *permission.Status + expectLen int + expectErr bool + }{ + { + name: "查詢所有權限並轉為 Map", + status: nil, + expectLen: 2, + expectErr: false, + }, + { + name: "查詢開啟的權限並轉為 Map", + status: ToPointer(permission.Open), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢關閉的權限並轉為 Map", + status: ToPointer(permission.Close), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢不存在的權限狀態並轉為 Map", + status: ToPointer(permission.Status(-1)), + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetAllIntoIDMap(context.Background(), tc.status) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "查詢結果數量不符合預期") + + // 確保 Map 的 key 是權限名稱 + for key, permission := range result { + assert.Equal(t, key, permission.Name, "Map 的 Key 應該與 Name 相符") + } + } + }) + } +} + +func TestPermissionRepository_FindByNames(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 測試數據 + testPermissions := []entity.Permission{ + { + ID: primitive.NewObjectID(), + Name: "read-data", + HTTPMethod: "GET", + HTTPPath: "/data", + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "write-data", + HTTPMethod: "POST", + HTTPPath: "/data", + Status: 1, + }, + } + + // 插入測試數據 + for _, perm := range testPermissions { + err := repo.Insert(context.Background(), perm) + assert.NoError(t, err) + } + + testCases := []struct { + name string + input []string + expectLen int + expectErr bool + }{ + { + name: "成功查詢單個名稱", + input: []string{"read-data"}, + expectLen: 1, + expectErr: false, + }, + { + name: "成功查詢多個名稱", + input: []string{"read-data", "write-data"}, + expectLen: 2, + expectErr: false, + }, + { + name: "查詢名稱不存在時應返回空結果", + input: []string{"unknown-permission"}, + expectLen: 0, + expectErr: false, + }, + { + name: "當查詢發生錯誤時,應返回錯誤", + input: nil, // 無效查詢 + expectLen: 0, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.FindByNames(context.Background(), tc.input) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "返回的數據長度應符合預期") + } + }) + } +} + +func ToPointer[T any](v T) *T { + return &v +} diff --git a/pkg/repository/role.go b/pkg/repository/role.go new file mode 100644 index 0000000..28b73e9 --- /dev/null +++ b/pkg/repository/role.go @@ -0,0 +1,200 @@ +package repository + +import ( + "context" + "fmt" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type RoleRepositoryParam struct { + Conf *mgo.Conf + DBOpts []mon.Option +} + +type RoleRepository struct { + DB mgo.DocumentDBUseCase +} + +func NewRoleRepository(param RoleRepositoryParam) repository.RoleRepository { + e := entity.Role{} + db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...) + if err != nil { + panic(err) + } + + return &RoleRepository{ + DB: db, + } +} + +func (repo *RoleRepository) List(ctx context.Context, params repository.ListQuery) ([]*entity.Role, int64, error) { + // 構建查詢條件 + filter := bson.M{} + + if params.Name != nil { + filter["name"] = *params.Name + } + + if params.ClientID != nil { + filter["client_id"] = *params.ClientID + } + + if params.Status != nil { + filter["status"] = *params.Status + } + + if params.UID != nil { + filter["uid"] = *params.UID + } + + // 計算符合條件的總數 + count, err := repo.DB.GetClient().CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + // 構建查詢選項(分頁) + opts := options.Find(). + SetSkip(params.PageSize * (params.PageIndex - 1)). + SetLimit(params.PageSize) + + // 執行查詢 + var result = make([]*entity.Role, 0, params.PageSize) + err = repo.DB.GetClient().Find(ctx, &result, filter, opts) + if err != nil { + return nil, 0, err + } + + return result, count, nil +} + +func (repo *RoleRepository) GetByID(ctx context.Context, id string) (*entity.Role, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + var result entity.Role + err = repo.DB.GetClient().FindOne(ctx, &result, bson.M{"_id": oid}) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (repo *RoleRepository) GetByUID(ctx context.Context, uid string) (*entity.Role, error) { + var result entity.Role + err := repo.DB.GetClient().FindOne(ctx, &result, bson.M{"uid": uid}) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (repo *RoleRepository) All(ctx context.Context, clientID *string) ([]*entity.Role, error) { + opt := options.Find().SetSort(bson.M{"_id": 1}) + filter := bson.M{} + if clientID != nil { + filter["client_id"] = *clientID + } + + result := make([]*entity.Role, 0) + err := repo.DB.GetClient().Find(ctx, &result, filter, opt) + if err != nil { + return nil, err + } + + return result, nil +} + +// Create 建立新的 Role +func (repo *RoleRepository) Create(ctx context.Context, role *entity.Role) error { + if role == nil { + return fmt.Errorf("failed to get role") + } + + if role.ID.IsZero() { + now := time.Now().UTC().UnixNano() + role.ID = primitive.NewObjectID() + role.CreateAt = now + role.UpdateAt = now + } + + _, err := repo.DB.GetClient().InsertOne(ctx, role) + + return err +} + +func (repo *RoleRepository) Update(ctx context.Context, role repository.UpdateReq) error { + now := time.Now().UTC().UnixNano() + // 動態構建更新內容 + updateFields := bson.M{ + "update_at": now, // 確保 `updateAt` 總是更新 + } + + if role.Name != nil { + updateFields["name"] = *role.Name + } + + if role.Status != nil { + updateFields["status"] = *role.Status + } + + if role.UID != nil { + updateFields["uid"] = *role.UID + } + + if role.ClientID != nil { + updateFields["client_id"] = *role.ClientID + } + + oid, err := primitive.ObjectIDFromHex(role.ID) + if err != nil { + return ErrInvalidObjectID + } + + _, err = repo.DB.GetClient().UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": updateFields}) + if err != nil { + return err + } + + return nil +} + +func (repo *RoleRepository) Delete(ctx context.Context, id string) error { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return ErrInvalidObjectID + } + + _, err = repo.DB.GetClient().DeleteOne(ctx, bson.M{"_id": oid}) + if err != nil { + return err + } + + return nil +} + +func (repo *RoleRepository) Index20250224UP(ctx context.Context) (*mongo.Cursor, error) { + repo.DB.PopulateMultiIndex(ctx, []string{ + "name", + "client_id", + "status", + "uid", + }, []int32{1, 1, 1, 1}, false) + + repo.DB.PopulateIndex(ctx, "uid", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/role_permission.go b/pkg/repository/role_permission.go new file mode 100644 index 0000000..500bf4e --- /dev/null +++ b/pkg/repository/role_permission.go @@ -0,0 +1,113 @@ +package repository + +import ( + "context" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type RolePermissionRepositoryParam struct { + Conf *mgo.Conf + DBOpts []mon.Option +} + +type RolePermissionRepository struct { + DB mgo.DocumentDBUseCase +} + +func NewRolePermissionRepository(param RoleRepositoryParam) repository.RolePermissionRepository { + e := entity.RolePermission{} + db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...) + if err != nil { + panic(err) + } + + return &RolePermissionRepository{ + DB: db, + } +} + +func (repo *RolePermissionRepository) Get(ctx context.Context, roleID string) ([]*entity.RolePermission, error) { + var result []*entity.RolePermission + err := repo.DB.GetClient().Find(ctx, &result, bson.M{"role_id": roleID}) + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo *RolePermissionRepository) GetByPermissionID(ctx context.Context, permissionIDs []string) ([]*entity.RolePermission, error) { + var result []*entity.RolePermission // 修正 []*entity.RolePermission -> []entity.RolePermission + + filter := bson.M{ + "permission_id": bson.M{"$in": permissionIDs}, // 使用 $in 運算子來匹配多個 permission_id + } + + err := repo.DB.GetClient().Find(ctx, &result, filter) + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo *RolePermissionRepository) Create(ctx context.Context, roles []entity.RolePermission) error { + if len(roles) == 0 { + return nil // 如果 roles 是空的,則不執行任何操作 + } + + now := time.Now().UTC().UnixNano() + + // 將 []entity.RolePermission 轉換為 []interface{} + roleInterfaces := make([]any, 0, len(roles)) + for i := range roles { + if roles[i].ID.IsZero() { + roles[i].ID = primitive.NewObjectID() + roles[i].CreateAt = now + roles[i].UpdateAt = now + } + roleInterfaces = append(roleInterfaces, roles[i]) + } + + _, err := repo.DB.GetClient().InsertMany(ctx, roleInterfaces) + + return err +} + +func (repo *RolePermissionRepository) Delete(ctx context.Context, roleID string, permissions []string) error { + if len(permissions) == 0 { + return nil // 如果 permissions 為空,則不執行刪除操作 + } + + filter := bson.M{ + "role_id": roleID, + "permission_id": bson.M{"$in": permissions}, // 使用 $in 刪除多個 permission_id + } + + _, err := repo.DB.GetClient().DeleteMany(ctx, filter) + if err != nil { + return err + } + + return nil +} + +func (repo *RolePermissionRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + repo.DB.PopulateMultiIndex(ctx, []string{ + "role_id", + "permission_id", + }, []int32{1, 1}, true) + + repo.DB.PopulateIndex(ctx, "role_id", 1, false) + repo.DB.PopulateIndex(ctx, "permission_id", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/role_permission_test.go b/pkg/repository/role_permission_test.go new file mode 100644 index 0000000..06471aa --- /dev/null +++ b/pkg/repository/role_permission_test.go @@ -0,0 +1,266 @@ +package repository + +import ( + "context" + "fmt" + "testing" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func SetupTestRolePermissionRepository(db string) (repository.RolePermissionRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + + conf := &mgo.Conf{ + Schema: Schema, + Host: fmt.Sprintf("%s:%s", h, p), + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + param := RoleRepositoryParam{ + Conf: conf, + } + repo := NewRolePermissionRepository(param) + _, _ = repo.Index20250225UP(context.Background()) + + return repo, tearDown, nil +} + +func TestRolePermissionRepository_Create(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input entity.RolePermission + expectErr bool + }{ + { + name: "成功插入新的 RolePermission", + input: entity.RolePermission{ + RoleID: "role_1", + PermissionID: "perm_1", + }, + expectErr: false, + }, + { + name: "插入已經有 ID 的 RolePermission", + input: entity.RolePermission{ + ID: primitive.NewObjectID(), + RoleID: "role_2", + PermissionID: "perm_2", + CreateAt: time.Now().UnixNano(), + UpdateAt: time.Now().UnixNano(), + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Create(context.Background(), []entity.RolePermission{tc.input}) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + }) + } +} + +func TestRolePermissionRepository_Delete(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先準備測試資料 + existingRolePermission := entity.RolePermission{ + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_1", + } + + err = repo.Create(context.Background(), []entity.RolePermission{existingRolePermission}) + assert.NoError(t, err, "應該成功插入測試資料") + + testCases := []struct { + name string + roleID string + permission string + expectErr bool + }{ + { + name: "成功刪除已存在的 RolePermission", + roleID: "role_1", + permission: "perm_1", + expectErr: false, + }, + { + name: "刪除不存在的 RolePermission,不應該報錯", + roleID: "role_2", + permission: "perm_2", + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Delete(context.Background(), tc.roleID, []string{tc.permission}) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + }) + } +} + +func TestRolePermissionRepository_GetByPermissionID(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先準備測試資料 + existingRolePermissions := []entity.RolePermission{ + { + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_1", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_2", + PermissionID: "perm_2", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_3", + PermissionID: "perm_3", + }, + } + + for _, rp := range existingRolePermissions { + err := repo.Create(context.Background(), []entity.RolePermission{rp}) + assert.NoError(t, err, "應該成功插入測試資料") + } + + testCases := []struct { + name string + permissionIDs []string + expectedCount int + expectErr bool + }{ + { + name: "成功查詢符合的 RolePermission", + permissionIDs: []string{"perm_1", "perm_2"}, + expectedCount: 2, + expectErr: false, + }, + { + name: "查詢時沒有符合條件的 RolePermission", + permissionIDs: []string{"perm_99"}, + expectedCount: 0, + expectErr: false, + }, + { + name: "查詢時發生資料庫錯誤", + permissionIDs: nil, // 模擬無效的參數 + expectedCount: 0, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + results, err := repo.GetByPermissionID(context.Background(), tc.permissionIDs) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, results, tc.expectedCount, "回傳結果數量應符合預期") + } + }) + } +} + +func TestRolePermissionRepository_Get(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先準備測試資料 + existingRolePermissions := []entity.RolePermission{ + { + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_1", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_2", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_2", + PermissionID: "perm_3", + }, + } + + for _, rp := range existingRolePermissions { + err := repo.Create(context.Background(), []entity.RolePermission{rp}) + assert.NoError(t, err, "應該成功插入測試資料") + } + + testCases := []struct { + name string + roleID string + expectedCount int + expectErr bool + }{ + { + name: "成功查詢符合的 RolePermission", + roleID: "role_1", + expectedCount: 2, + expectErr: false, + }, + { + name: "查詢時沒有符合條件的 RolePermission", + roleID: "role_99", + expectedCount: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + results, err := repo.Get(context.Background(), tc.roleID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, results, tc.expectedCount, "回傳結果數量應符合預期") + } + }) + } +} diff --git a/pkg/repository/role_test.go b/pkg/repository/role_test.go new file mode 100644 index 0000000..0598264 --- /dev/null +++ b/pkg/repository/role_test.go @@ -0,0 +1,542 @@ +package repository + +import ( + "context" + "fmt" + "testing" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func SetupTestRoleRepository(db string) (repository.RoleRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + + conf := &mgo.Conf{ + Schema: Schema, + Host: fmt.Sprintf("%s:%s", h, p), + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + param := RoleRepositoryParam{ + Conf: conf, + } + repo := NewRoleRepository(param) + _, _ = repo.Index20250224UP(context.Background()) + + return repo, tearDown, nil +} + +func TestRoleRepository_Create(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input *entity.Role + expectErr bool + }{ + { + name: "成功建立新的角色", + input: &entity.Role{ + Name: "Admin", + UID: "user123", + ClientID: "client456", + Status: 1, + }, + expectErr: false, + }, + { + name: "自動生成 ID 及時間戳", + input: &entity.Role{ + Name: "User", + UID: "user789", + ClientID: "client987", + Status: 1, + }, + expectErr: false, + }, + { + name: "當角色為 nil 時應返回錯誤", + input: nil, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Create(context.Background(), tc.input) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.NotEqual(t, primitive.NilObjectID, tc.input.ID, "應該自動生成 ObjectID") + assert.True(t, tc.input.CreateAt > 0, "應該自動設定 CreateAt") + assert.True(t, tc.input.UpdateAt > 0, "應該自動設定 UpdateAt") + } + }) + } +} + +func TestRoleRepository_Update(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立一個角色 + existingRole := &entity.Role{ + Name: "Old Name", + UID: "old_uid", + ClientID: "old_client_id", + Status: 1, + } + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + newName := "New Name" + newStatus := permission.Close + newUID := "new_uid" + newClientID := "new_client_id" + + testCases := []struct { + name string + input repository.UpdateReq + expectErr bool + }{ + { + name: "成功更新角色", + input: repository.UpdateReq{ + ID: existingRole.ID.Hex(), + Name: &newName, + Status: &newStatus, + UID: &newUID, + ClientID: &newClientID, + }, + expectErr: false, + }, + { + name: "更新部分欄位", + input: repository.UpdateReq{ + ID: existingRole.ID.Hex(), + Name: &newName, + Status: &newStatus, + }, + expectErr: false, + }, + { + name: "無效的 ObjectID 應返回錯誤", + input: repository.UpdateReq{ + ID: "invalid_object_id", + }, + expectErr: true, + }, + { + name: "當 UpdateOne 失敗時應返回錯誤", + input: repository.UpdateReq{ + ID: primitive.NewObjectID().Hex(), // 模擬一個不存在的 ID + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Update(context.Background(), tc.input) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + + // 驗證更新結果 + updatedRole, err := repo.GetByID(context.Background(), existingRole.ID.Hex()) + assert.NoError(t, err) + if tc.input.Name != nil { + assert.Equal(t, *tc.input.Name, updatedRole.Name, "名稱應該被更新") + } + if tc.input.Status != nil { + assert.Equal(t, *tc.input.Status, updatedRole.Status, "狀態應該被更新") + } + if tc.input.UID != nil { + assert.Equal(t, *tc.input.UID, updatedRole.UID, "UID 應該被更新") + } + if tc.input.ClientID != nil { + assert.Equal(t, *tc.input.ClientID, updatedRole.ClientID, "ClientID 應該被更新") + } + } + }) + } +} + +func TestRoleRepository_Delete(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立一個角色 + existingRole := &entity.Role{ + Name: "Test Role", + UID: "test_uid", + ClientID: "test_client_id", + Status: 1, + } + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + testCases := []struct { + name string + inputID string + expectErr bool + }{ + { + name: "成功刪除角色", + inputID: existingRole.ID.Hex(), + expectErr: false, + }, + { + name: "刪除不存在的角色不應報錯", + inputID: primitive.NewObjectID().Hex(), + expectErr: false, + }, + { + name: "無效的 ObjectID 應返回錯誤", + inputID: "invalid_object_id", + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Delete(context.Background(), tc.inputID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + + // 驗證角色是否已刪除 + if tc.inputID == existingRole.ID.Hex() { + _, err := repo.GetByID(context.Background(), existingRole.ID.Hex()) + assert.Error(t, err, "應該找不到該角色") + } + } + }) + } +} + +func TestRoleRepository_All(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + clientID1 := "client_1" + clientID2 := "client_2" + + // 預先建立角色資料 + roles := []*entity.Role{ + { + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: clientID1, + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "User", + UID: "user_uid", + ClientID: clientID1, + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "Manager", + UID: "manager_uid", + ClientID: clientID2, + Status: 1, + }, + } + + // 插入測試資料 + for _, role := range roles { + err := repo.Create(context.Background(), role) + assert.NoError(t, err) + } + + testCases := []struct { + name string + clientID *string + expectLen int + expectErr bool + }{ + { + name: "查詢所有角色", + clientID: nil, + expectLen: len(roles), + expectErr: false, + }, + { + name: "根據 clientID 查詢角色", + clientID: &clientID1, + expectLen: 2, + expectErr: false, + }, + { + name: "clientID 無匹配時應返回空", + clientID: new(string), + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.All(context.Background(), tc.clientID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "返回的角色數量應符合預期") + } + }) + } +} + +func TestRoleRepository_GetByUID(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立測試角色 + existingRole := &entity.Role{ + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: "client_1", + Status: 1, + } + + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + testCases := []struct { + name string + uid string + expectErr bool + expectNil bool + }{ + { + name: "成功查詢角色", + uid: "admin_uid", + expectErr: false, + expectNil: false, + }, + { + name: "查詢不存在的角色", + uid: "non_existent_uid", + expectErr: true, + expectNil: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetByUID(context.Background(), tc.uid) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + + if tc.expectNil { + assert.Nil(t, result, "應該返回 nil") + } else { + assert.NotNil(t, result, "不應該返回 nil") + assert.Equal(t, existingRole.UID, result.UID, "UID 應相符") + assert.Equal(t, existingRole.Name, result.Name, "名稱應相符") + } + }) + } +} + +func TestRoleRepository_GetByID(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立測試角色 + existingRole := &entity.Role{ + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: "client_1", + Status: 1, + } + + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + testCases := []struct { + name string + id string + expectErr bool + expectNil bool + }{ + { + name: "成功查詢角色", + id: existingRole.ID.Hex(), + expectErr: false, + expectNil: false, + }, + { + name: "查詢不存在的角色", + id: primitive.NewObjectID().Hex(), + expectErr: true, + expectNil: true, + }, + { + name: "提供無效的 ObjectID", + id: "invalid_id", + expectErr: true, + expectNil: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetByID(context.Background(), tc.id) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + + if tc.expectNil { + assert.Nil(t, result, "應該返回 nil") + } else { + assert.NotNil(t, result, "不應該返回 nil") + assert.Equal(t, existingRole.ID, result.ID, "ID 應相符") + assert.Equal(t, existingRole.Name, result.Name, "名稱應相符") + } + }) + } +} + +func TestRoleRepository_List(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立測試角色 + roles := []*entity.Role{ + { + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: "client_1", + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "User", + UID: "user_uid", + ClientID: "client_1", + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "Guest", + UID: "guest_uid", + ClientID: "client_2", + Status: 0, + }, + } + + // 插入測試資料 + for _, role := range roles { + err := repo.Create(context.Background(), role) + assert.NoError(t, err) + } + + testCases := []struct { + name string + query repository.ListQuery + expectLen int + expectErr bool + }{ + { + name: "查詢所有角色", + query: repository.ListQuery{PageSize: 10, PageIndex: 1}, + expectLen: 3, + expectErr: false, + }, + { + name: "篩選名稱為 Admin", + query: repository.ListQuery{Name: ToPointer("Admin"), PageSize: 10, PageIndex: 1}, + expectLen: 1, + expectErr: false, + }, + { + name: "篩選特定 ClientID", + query: repository.ListQuery{ClientID: ToPointer("client_1"), PageSize: 10, PageIndex: 1}, + expectLen: 2, + expectErr: false, + }, + { + name: "篩選啟用 (Status=1) 的角色", + query: repository.ListQuery{Status: ToPointer(permission.Open), PageSize: 10, PageIndex: 1}, + expectLen: 2, + expectErr: false, + }, + { + name: "篩選特定 UID", + query: repository.ListQuery{UID: ToPointer("guest_uid"), PageSize: 10, PageIndex: 1}, + expectLen: 1, + expectErr: false, + }, + { + name: "測試分頁 PageSize=1, PageIndex=1", + query: repository.ListQuery{PageSize: 1, PageIndex: 1}, + expectLen: 1, + expectErr: false, + }, + { + name: "查詢無符合條件的角色", + query: repository.ListQuery{Name: ToPointer("NonExist"), PageSize: 10, PageIndex: 1}, + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, _, err := repo.List(context.Background(), tc.query) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "返回的角色數量應符合預期") + } + }) + } +} diff --git a/pkg/repository/start_mongo_container_test.go b/pkg/repository/start_mongo_container_test.go new file mode 100644 index 0000000..1bcfe1a --- /dev/null +++ b/pkg/repository/start_mongo_container_test.go @@ -0,0 +1,52 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + Host = "127.0.0.1" + Port = "27017" + Schema = "mongodb" +) + +func startMongoContainer() (string, string, func(), error) { + ctx := context.Background() + + req := testcontainers.ContainerRequest{ + Image: "mongo:latest", + ExposedPorts: []string{"27017/tcp"}, + WaitingFor: wait.ForListeningPort("27017/tcp"), + } + + mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return "", "", nil, err + } + + port, err := mongoC.MappedPort(ctx, Port) + if err != nil { + return "", "", nil, err + } + + host, err := mongoC.Host(ctx) + if err != nil { + return "", "", nil, err + } + + uri := fmt.Sprintf("mongodb://%s:%s", host, port.Port()) + tearDown := func() { + mongoC.Terminate(ctx) + } + + fmt.Printf("Connecting to %s\n", uri) + + return host, port.Port(), tearDown, nil +} diff --git a/pkg/repository/token.go b/pkg/repository/token.go new file mode 100644 index 0000000..1d6c5cb --- /dev/null +++ b/pkg/repository/token.go @@ -0,0 +1,352 @@ +package repository + +import ( + "context" + "encoding/json" + "errors" + "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" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +// TokenRepositoryParam token 需要的參數 +type TokenRepositoryParam struct { + Redis *redis.Redis +} + +// TokenRepository 用於操作 token 的存儲庫 +type TokenRepository struct { + TokenRepositoryParam +} + +// NewTokenRepository 初始化並返回一個 TokenRepository 實例 +func NewTokenRepository(param TokenRepositoryParam) repository.TokenRepo { + return &TokenRepository{ + TokenRepositoryParam: param, + } +} + +// ====================== 私有工具函數 ======================= + +// runPipeline 執行 Redis pipeline 操作 +func (repo *TokenRepository) runPipeline(ctx context.Context, pipelineFunc func(tx redis.Pipeliner) error) error { + return repo.Redis.PipelinedCtx(ctx, pipelineFunc) +} + +// setToken 使用 Redis pipeline 存儲 token 並設定 TTL +func (repo *TokenRepository) setToken(ctx context.Context, tx redis.Pipeliner, id string, body []byte, ttl time.Duration) error { + return tx.Set(ctx, domain.GetAccessTokenRedisKey(id), body, ttl).Err() +} + +// setRefreshToken 若 token 中有 refresh token,則存儲之(使用 pipeline) +func (repo *TokenRepository) setRefreshToken(ctx context.Context, tx redis.Pipeliner, token entity.Token, ttl time.Duration) error { + if token.RefreshToken == "" { + return nil + } + + return tx.Set(ctx, domain.GetRefreshTokenRedisKey(token.RefreshToken), token.ID, ttl).Err() +} + +// setTokenRelation 在 Redis 中設定 token 與 UID/Device 之間的關聯,並設定過期時間 +func (repo *TokenRepository) setTokenRelation(ctx context.Context, tx redis.Pipeliner, uid, deviceID, tokenID string, ttl time.Duration) error { + // 定義需要執行的操作列表 + operations := []struct { + key string + op func() error + }{ + { + key: domain.GetUIDTokenRedisKey(uid), + op: func() error { + return tx.SAdd(ctx, domain.GetUIDTokenRedisKey(uid), tokenID).Err() + }, + }, + { + key: domain.GetDeviceTokenRedisKey(deviceID), + op: func() error { + return tx.SAdd(ctx, domain.GetDeviceTokenRedisKey(deviceID), tokenID).Err() + }, + }, + } + + // 執行每個操作,並為對應 key 設置過期時間 + for _, operation := range operations { + if err := operation.op(); err != nil { + return fmt.Errorf("failed to create token relaction: %w", err) + } + if err := tx.Expire(ctx, operation.key, ttl).Err(); err != nil { + return fmt.Errorf("failed to set expire: %w", err) + } + } + + return nil +} + +// retrieveToken 根據指定 key 從 Redis 中獲取 token +func (repo *TokenRepository) retrieveToken(ctx context.Context, key string) (entity.Token, error) { + body, err := repo.Redis.GetCtx(ctx, key) + if err != nil { + return entity.Token{}, err + } + if body == "" { + return entity.Token{}, fmt.Errorf("failed to found token") + } + + var token entity.Token + if err := json.Unmarshal([]byte(body), &token); err != nil { + return entity.Token{}, fmt.Errorf("failed to unmarshal token JSON: %w", err) + } + + return token, nil +} + +// getTokensBySet 根據集合 key 獲取所有 token +func (repo *TokenRepository) getTokensBySet(ctx context.Context, setKey string) ([]entity.Token, error) { + ids, err := repo.Redis.Smembers(setKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, nil + } + + return nil, err + } + + tokens := make([]entity.Token, 0, len(ids)) + var tokensToDelete []string + now := time.Now().UnixNano() + for _, id := range ids { + token, err := repo.retrieveToken(ctx, domain.GetAccessTokenRedisKey(id)) + if err != nil { + tokensToDelete = append(tokensToDelete, id) + + continue + } + + if token.ExpiresIn < now { + tokensToDelete = append(tokensToDelete, id) + + continue + } + tokens = append(tokens, token) + } + + // 清除過期或錯誤的 token + if len(tokensToDelete) > 0 { + _ = repo.DeleteAccessTokenByID(ctx, tokensToDelete) + } + + return tokens, nil +} + +// getCountBySet 獲取集合中元素的數量 +func (repo *TokenRepository) getCountBySet(ctx context.Context, setKey string) (int, error) { + count, err := repo.Redis.ScardCtx(ctx, setKey) + if err != nil { + return 0, err + } + + return int(count), nil +} + +// deleteKeysAndRelations 刪除指定的 Redis key 並移除相關關聯(UID 與 DeviceID) +func (repo *TokenRepository) deleteKeysAndRelations(ctx context.Context, keys []string, uid, deviceID, tokenID string) error { + err := repo.Redis.Pipelined(func(tx redis.Pipeliner) error { + // 移除 UID 與 DeviceID 關聯中的 tokenID + _ = tx.SRem(ctx, domain.GetUIDTokenRedisKey(uid), tokenID) + _ = tx.SRem(ctx, domain.GetDeviceTokenRedisKey(deviceID), tokenID) + + // 刪除所有指定的 keys + for _, key := range keys { + _ = tx.Del(ctx, key) + } + + return nil + }) + + return err +} + +// batchDeleteKeys 批量刪除 Redis keys +func (repo *TokenRepository) batchDeleteKeys(ctx context.Context, keys ...string) error { + return repo.Redis.Pipelined(func(tx redis.Pipeliner) error { + for _, key := range keys { + if err := tx.Del(ctx, key).Err(); err != nil { + return err + } + } + + return nil + }) +} + +// ====================== 公開方法 ======================= + +// Create 創建新的 token +func (repo *TokenRepository) Create(ctx context.Context, token entity.Token) error { + binToken, err := json.Marshal(token) + if err != nil { + return err + } + // 根據 token 設定 refresh 過期秒數計算 TTL + refreshTTL := time.Duration(token.RedisRefreshExpiredSec()) * time.Second + + return repo.runPipeline(ctx, func(tx redis.Pipeliner) error { + if err := repo.setToken(ctx, tx, token.ID, binToken, refreshTTL); err != nil { + return err + } + if err := repo.setRefreshToken(ctx, tx, token, refreshTTL); err != nil { + return err + } + + if err := repo.setTokenRelation(ctx, tx, token.UID, token.DeviceID, token.ID, refreshTTL); err != nil { + return err + } + + return nil + }) +} + +// CreateOneTimeToken 創建一次性 token +func (repo *TokenRepository) CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, dt time.Duration) error { + body, err := json.Marshal(ticket) + if err != nil { + return err + } + + _, err = repo.Redis.SetnxExCtx(ctx, domain.GetRefreshTokenRedisKey(key), string(body), int(dt.Seconds())) + + return err +} + +// GetAccessTokenByOneTimeToken 根據一次性 token 獲取 access token +func (repo *TokenRepository) GetAccessTokenByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) { + rk := domain.GetRefreshTokenRedisKey(oneTimeToken) + tokenID, err := repo.Redis.GetCtx(ctx, rk) + if err != nil { + return entity.Token{}, err + } + if tokenID == "" { + return entity.Token{}, fmt.Errorf("failed to found token") + } + + return repo.retrieveToken(ctx, domain.GetAccessTokenRedisKey(tokenID)) +} + +// GetAccessTokenByID 根據 token ID 獲取 access token +func (repo *TokenRepository) GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) { + return repo.retrieveToken(ctx, domain.GetAccessTokenRedisKey(id)) +} + +// GetAccessTokensByUID 根據 UID 獲取所有 access tokens +func (repo *TokenRepository) GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) { + return repo.getTokensBySet(ctx, domain.GetUIDTokenRedisKey(uid)) +} + +// GetAccessTokenCountByUID 根據 UID 獲取 access token 的數量 +func (repo *TokenRepository) GetAccessTokenCountByUID(ctx context.Context, uid string) (int, error) { + return repo.getCountBySet(ctx, domain.GetUIDTokenRedisKey(uid)) +} + +// GetAccessTokensByDeviceID 根據 DeviceID 獲取所有 access tokens +func (repo *TokenRepository) GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) { + return repo.getTokensBySet(ctx, domain.GetDeviceTokenRedisKey(deviceID)) +} + +// GetAccessTokenCountByDeviceID 根據 DeviceID 獲取 access token 的數量 +func (repo *TokenRepository) GetAccessTokenCountByDeviceID(ctx context.Context, deviceID string) (int, error) { + return repo.getCountBySet(ctx, domain.GetDeviceTokenRedisKey(deviceID)) +} + +// Delete 刪除指定的 token +func (repo *TokenRepository) Delete(ctx context.Context, token entity.Token) error { + keys := []string{ + domain.GetAccessTokenRedisKey(token.ID), + domain.GetRefreshTokenRedisKey(token.RefreshToken), + } + + return repo.deleteKeysAndRelations(ctx, keys, token.UID, token.DeviceID, token.ID) +} + +// DeleteAccessTokenByID 根據 token ID 刪除 access token +func (repo *TokenRepository) DeleteAccessTokenByID(ctx context.Context, ids []string) error { + for _, tokenID := range ids { + token, err := repo.GetAccessTokenByID(ctx, tokenID) + if err != nil { + continue + } + keys := []string{ + domain.GetAccessTokenRedisKey(token.ID), + domain.GetRefreshTokenRedisKey(token.RefreshToken), + } + _ = repo.deleteKeysAndRelations(ctx, keys, token.UID, token.DeviceID, token.ID) + } + + return nil +} + +// DeleteAccessTokensByUID 根據 UID 刪除所有 access tokens +func (repo *TokenRepository) DeleteAccessTokensByUID(ctx context.Context, uid string) error { + tokens, err := repo.GetAccessTokensByUID(ctx, uid) + if err != nil { + return err + } + for _, token := range tokens { + if err := repo.Delete(ctx, token); err != nil { + return err + } + } + + return nil +} + +// DeleteAccessTokensByDeviceID 根據 DeviceID 刪除所有 access tokens +func (repo *TokenRepository) DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error { + tokens, err := repo.GetAccessTokensByDeviceID(ctx, deviceID) + if err != nil { + return err + } + + // 預分配 keys:每個 token 包含兩個 key + keys := make([]string, 0, len(tokens)*2) + for _, token := range tokens { + keys = append(keys, domain.GetAccessTokenRedisKey(token.ID)) + keys = append(keys, domain.GetRefreshTokenRedisKey(token.RefreshToken)) + } + + // 移除 UID 關聯中的 tokenID + if err := repo.runPipeline(ctx, func(tx redis.Pipeliner) error { + for _, token := range tokens { + _ = tx.SRem(ctx, domain.GetUIDTokenRedisKey(token.UID), token.ID) + } + + return nil + }); err != nil { + return err + } + + if err := repo.batchDeleteKeys(ctx, keys...); err != nil { + return err + } + + _, err = repo.Redis.Del(domain.GetDeviceTokenRedisKey(deviceID)) + + return err +} + +// DeleteOneTimeToken 刪除一次性 token(支持多個 key 一併刪除) +func (repo *TokenRepository) DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error { + totalKeys := len(ids) + len(tokens) + keys := make([]string, 0, totalKeys) + + for _, id := range ids { + keys = append(keys, domain.GetRefreshTokenRedisKey(id)) + } + for _, token := range tokens { + keys = append(keys, domain.GetRefreshTokenRedisKey(token.RefreshToken)) + } + + return repo.batchDeleteKeys(ctx, keys...) +} diff --git a/pkg/repository/token_test.go b/pkg/repository/token_test.go new file mode 100644 index 0000000..0e81d8b --- /dev/null +++ b/pkg/repository/token_test.go @@ -0,0 +1,1750 @@ +package repository + +import ( + "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" +) + +func setupMiniRedis() (*miniredis.Miniredis, *redis.Redis) { + // 啟動 setupMiniRedis 作為模擬的 Redis 服務 + mr, err := miniredis.Run() + if err != nil { + panic("failed to start miniRedis: " + err.Error()) + } + + // 使用 setupMiniRedis 的地址配置 go-zero Redis 客戶端 + redisConf := redis.RedisConf{ + Host: mr.Addr(), + Type: "node", + } + r := redis.MustNewRedis(redisConf) + + return mr, r +} + +func TestTokenRepository_Create(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + token := entity.Token{ + ID: "token123", + UID: "user123", + DeviceID: "device123", + AccessToken: "access123", + ExpiresIn: time.Now().UTC().Add(10 * time.Second).UnixNano(), // 過期時間,現在加 10 秒 = 10 秒後 + RefreshToken: "refresh123", + RefreshExpiresIn: time.Now().UTC().Add(10 * time.Second).UnixNano(), // 過期時間,現在加 10 秒 = 10 秒後 + } + expiredTTL := 10 * time.Second // 過期時間 + + // 定義測試場景 + tests := []struct { + name string + token entity.Token + prepareFunc func() error // 用於模擬 Redis 或序列化錯誤 + wantErr bool + errMsg string + }{ + { + name: "Successful token creation", + token: token, + wantErr: false, + }, + { + name: "Redis Pipeline error", + token: token, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 操作錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 或序列化錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 Create 方法 + err := repo.Create(context.Background(), tt.token) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 檢查是否成功設置了 AccessToken、RefreshToken 和 UID 及 DeviceID 關聯 + tokenKey := domain.GetAccessTokenRedisKey(tt.token.ID) + refreshTokenKey := domain.GetRefreshTokenRedisKey(tt.token.RefreshToken) + uidKey := domain.GetUIDTokenRedisKey(tt.token.UID) + deviceIDKey := domain.GetDeviceTokenRedisKey(tt.token.DeviceID) + + // 驗證 AccessToken 是否已設置 + val, err := mr.Get(tokenKey) + assert.NoError(t, err) + expectedBody, _ := json.Marshal(tt.token) + assert.Equal(t, string(expectedBody), val) + + // 驗證 RefreshToken 是否已設置 + val, err = mr.Get(refreshTokenKey) + assert.NoError(t, err) + assert.Equal(t, tt.token.ID, val) + + // 檢查 UID 和 DeviceID 關聯是否已設置 + uidSetMembers, err := mr.SMembers(uidKey) + assert.NoError(t, err) + assert.Contains(t, uidSetMembers, tt.token.ID) + + deviceIDSetMembers, err := mr.SMembers(deviceIDKey) + assert.NoError(t, err) + assert.Contains(t, deviceIDSetMembers, tt.token.ID) + + // 檢查 AccessToken 和 RefreshToken 的過期時間 + accessTTL := mr.TTL(tokenKey) + assert.InDelta(t, expiredTTL.Seconds(), accessTTL.Seconds(), 2, "AccessToken TTL 與設置的過期 TTl 應該相近") + + refreshTTLVal := mr.TTL(refreshTokenKey) + assert.InDelta(t, expiredTTL.Seconds(), refreshTTLVal.Seconds(), 2, "Refresh TTL 與 與設置的過期 TTl 應該相近") + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_retrieveToken(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 模擬一個 Token 實例並將其存入 Redis + now := time.Now().UTC().UnixNano() + token := entity.Token{ + ID: "token123", + UID: "user123", + DeviceID: "device123", + AccessToken: "access123", + ExpiresIn: time.Now().UTC().Add(3600 * time.Second).UnixNano(), + AccessCreateAt: now, + RefreshToken: "refresh123", + RefreshExpiresIn: time.Now().UTC().Add(7200 * time.Second).UnixNano(), + RefreshCreateAt: now, + } + + // 將 Token 序列化為 JSON 並存入 Redis + tokenKey := domain.GetAccessTokenRedisKey(token.ID) + tokenData, _ := json.Marshal(token) + err := mr.Set(tokenKey, string(tokenData)) + assert.NoError(t, err) + + // 定義測試場景 + tests := []struct { + name string + key string + want entity.Token + wantErr bool + errMsg string + }{ + { + name: "ok", + key: tokenKey, + want: token, + wantErr: false, + }, + { + name: "Token not found", + key: domain.GetAccessTokenRedisKey("nonexistent"), + want: entity.Token{}, + wantErr: true, + errMsg: "failed to found token", + }, + { + name: "Invalid JSON format", + key: domain.GetAccessTokenRedisKey("invalid_json"), + want: entity.Token{}, + wantErr: true, + errMsg: "failed to unmarshal token JSON: invalid character 'i' looking for beginning of object key string", + }, + } + + // 將錯誤的 JSON 格式設置到 Redis + err = mr.Set(domain.GetAccessTokenRedisKey("invalid_json"), "{invalid_json}") + assert.NoError(t, err) + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.retrieveToken(context.Background(), tt.key) + + if tt.wantErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + } else { + assert.NoError(t, err) + // 比較 Token 的每個字段 + assert.Equal(t, tt.want.ID, got.ID) + assert.Equal(t, tt.want.UID, got.UID) + assert.Equal(t, tt.want.DeviceID, got.DeviceID) + assert.Equal(t, tt.want.AccessToken, got.AccessToken) + assert.Equal(t, tt.want.ExpiresIn, got.ExpiresIn) + assert.Equal(t, tt.want.RefreshToken, got.RefreshToken) + assert.Equal(t, tt.want.RefreshExpiresIn, got.RefreshExpiresIn) + + // 將時間字段轉換為 Unix() 格式進行比較 + assert.Equal(t, tt.want.AccessCreateAt, got.AccessCreateAt) + assert.Equal(t, tt.want.RefreshCreateAt, got.RefreshCreateAt) + } + }) + } +} + +func TestTokenRepository_GetTokensBySet(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 模擬兩個 Token 實例,一個過期,一個未過期,並將它們存入 Redis + now := time.Now().UTC() + unexpiredToken := entity.Token{ + ID: "token123", + UID: "user123", + DeviceID: "device123", + AccessToken: "access123", + ExpiresIn: now.Add(time.Hour).UnixNano(), // 1 小時後過期 + AccessCreateAt: now.UnixNano(), + RefreshToken: "refresh123", + RefreshExpiresIn: now.Add(2 * time.Hour).UnixNano(), + RefreshCreateAt: now.UnixNano(), + } + + expiredToken := entity.Token{ + ID: "token456", + UID: "user456", + DeviceID: "device456", + AccessToken: "access456", + ExpiresIn: now.Add(-time.Hour).UnixNano(), // 1 小時前過期 + AccessCreateAt: now.Add(-2 * time.Hour).UnixNano(), + RefreshToken: "refresh456", + RefreshExpiresIn: now.Add(-30 * time.Minute).UnixNano(), + RefreshCreateAt: now.Add(-90 * time.Minute).UnixNano(), + } + + // 將 Token 存入 Redis + unexpiredTokenData, _ := json.Marshal(unexpiredToken) + expiredTokenData, _ := json.Marshal(expiredToken) + err := mr.Set(domain.GetAccessTokenRedisKey(unexpiredToken.ID), string(unexpiredTokenData)) + assert.NoError(t, err) + err = mr.Set(domain.GetAccessTokenRedisKey(expiredToken.ID), string(expiredTokenData)) + assert.NoError(t, err) + + // 將兩個 Token ID 添加到 Set 集合中 + setKey := "permission:token_set" + _, err = mr.SAdd(setKey, unexpiredToken.ID) + if err != nil { + return + } + _, err = mr.SAdd(setKey, expiredToken.ID) + assert.NoError(t, err) + + // 定義測試場景 + tests := []struct { + name string + setKey string + wantTokens []entity.Token + wantErr bool + }{ + { + name: "Set contains unexpired and expired tokens", + setKey: setKey, + wantTokens: []entity.Token{unexpiredToken}, // 預期僅返回未過期的 Token + wantErr: false, + }, + { + name: "Set key not found", + setKey: "permission:nonexistent_set", + wantTokens: nil, // 預期返回 nil + wantErr: false, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.getTokensBySet(context.Background(), tt.setKey) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, len(tt.wantTokens), len(got)) + + // 比較每個返回的 Token,並檢查時間戳 + for i, token := range got { + assert.Equal(t, tt.wantTokens[i].ID, token.ID) + assert.Equal(t, tt.wantTokens[i].UID, token.UID) + assert.Equal(t, tt.wantTokens[i].DeviceID, token.DeviceID) + assert.Equal(t, tt.wantTokens[i].AccessToken, token.AccessToken) + assert.Equal(t, tt.wantTokens[i].ExpiresIn, token.ExpiresIn) + assert.Equal(t, tt.wantTokens[i].RefreshToken, token.RefreshToken) + assert.Equal(t, tt.wantTokens[i].RefreshExpiresIn, token.RefreshExpiresIn) + assert.Equal(t, tt.wantTokens[i].AccessCreateAt, token.AccessCreateAt) + assert.Equal(t, tt.wantTokens[i].RefreshCreateAt, token.RefreshCreateAt) + } + } + }) + } +} + +func TestTokenRepository_GetCountBySet(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試集合鍵和測試數據 + setKey := "permission:token_set" + + // 將測試數據存入 Redis + mr.SAdd(setKey, "token123") + mr.SAdd(setKey, "token456") + mr.SAdd(setKey, "token789") + + // 定義測試場景 + tests := []struct { + name string + setKey string + want int + wantErr bool + }{ + { + name: "Count of existing set", + setKey: setKey, + want: 3, // 預期集合中有 3 個元素 + wantErr: false, + }, + { + name: "Non-existent set", + setKey: "permission:nonexistent_set", + want: 0, // 預期集合不存在,返回 0 + wantErr: false, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.getCountBySet(context.Background(), tt.setKey) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestTokenRepository_SetRelation(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + uid := "user123" + deviceID := "device123" + tokenID := "token123" + ttl := 10 * time.Second // 設置過期時間為 10 秒 + + // 定義測試場景 + tests := []struct { + name string + uid string + deviceID string + tokenID string + ttl time.Duration + prepareFunc func() error // 用於模擬 Redis 錯誤 + wantErr bool + errMsg string + }{ + { + name: "Valid relation setting", + uid: uid, + deviceID: deviceID, + tokenID: tokenID, + ttl: ttl, + wantErr: false, + }, + { + name: "Redis SAdd error", + uid: uid, + deviceID: deviceID, + tokenID: tokenID, + ttl: ttl, + prepareFunc: func() error { + mr.SetError("forced SAdd error") // 模擬 SAdd 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced SAdd error", + }, + { + name: "Redis Expire error", + uid: uid, + deviceID: deviceID, + tokenID: tokenID, + ttl: ttl, + prepareFunc: func() error { + mr.SetError("forced Expire error") // 模擬 Expire 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Expire error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 構建 Redis 鍵 + uidKey := domain.GetUIDTokenRedisKey(tt.uid) + deviceIDKey := domain.GetDeviceTokenRedisKey(tt.deviceID) + + // 執行 Redis Pipeline + err := r.Pipelined(func(tx redis.Pipeliner) error { + return repo.setTokenRelation(context.Background(), tx, tt.uid, tt.deviceID, tt.tokenID, tt.ttl) + }) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 檢查 UID 和 DeviceID 關聯是否已設置 + uidSetMembers, err := mr.SMembers(uidKey) + assert.NoError(t, err) + assert.Contains(t, uidSetMembers, tt.tokenID) + + deviceIDSetMembers, err := mr.SMembers(deviceIDKey) + assert.NoError(t, err) + assert.Contains(t, deviceIDSetMembers, tt.tokenID) + + // 檢查 UID 和 DeviceID 鍵的過期時間 + uidTTL := mr.TTL(uidKey) + assert.Equal(t, tt.ttl.Seconds(), uidTTL.Seconds()) + + deviceIDTTL := mr.TTL(deviceIDKey) + assert.Equal(t, tt.ttl.Seconds(), deviceIDTTL.Seconds()) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_SetRefreshToken(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + ttl := 10 * time.Second // 設置過期時間為 10 秒 + + // 定義測試場景 + tests := []struct { + name string + token entity.Token + ttl time.Duration + prepareFunc func() error // 用於模擬 Redis 錯誤 + wantErr bool + errMsg string + }{ + { + name: "Valid RefreshToken setting", + token: entity.Token{ + ID: "token123", + RefreshToken: "refresh123", + }, + ttl: ttl, + wantErr: false, + }, + { + name: "Empty RefreshToken", + token: entity.Token{ + ID: "token456", + RefreshToken: "", + }, + ttl: ttl, + wantErr: false, + }, + { + name: "Redis Set error", + token: entity.Token{ + ID: "token789", + RefreshToken: "refresh789", + }, + ttl: ttl, + prepareFunc: func() error { + mr.SetError("forced Set error") // 模擬 Set 操作錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Set error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 Redis Pipeline + err := r.Pipelined(func(tx redis.Pipeliner) error { + return repo.setRefreshToken(context.Background(), tx, tt.token, tt.ttl) + }) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 如果 RefreshToken 不為空,檢查是否成功設置了鍵 + if tt.token.RefreshToken != "" { + refreshTokenKey := domain.GetRefreshTokenRedisKey(tt.token.RefreshToken) + val, err := mr.Get(refreshTokenKey) + assert.NoError(t, err) + assert.Equal(t, tt.token.ID, val) + + // 檢查 RefreshToken 鍵的過期時間 + ttlVal := mr.TTL(refreshTokenKey) + assert.Equal(t, tt.ttl.Seconds(), ttlVal.Seconds()) + } + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_SetToken(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + ttl := 10 * time.Second // 設置過期時間為 10 秒 + token := entity.Token{ + ID: "token123", + UID: "user123", + DeviceID: "device123", + AccessToken: "access123", + ExpiresIn: time.Now().UTC().Add(7200 * time.Second).UnixNano(), + RefreshToken: "refresh123", + } + body, _ := json.Marshal(token) // 將 Token 轉為 JSON 格式 + + // 定義測試場景 + tests := []struct { + name string + token entity.Token + body []byte + ttl time.Duration + prepareFunc func() error // 用於模擬 Redis 錯誤 + wantErr bool + errMsg string + }{ + { + name: "Valid Token setting", + token: token, + body: body, + ttl: ttl, + wantErr: false, + }, + { + name: "Redis Set error", + token: token, + body: body, + ttl: ttl, + prepareFunc: func() error { + mr.SetError("forced Set error") // 模擬 Set 操作錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Set error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 構建 Redis 鍵 + tokenKey := domain.GetAccessTokenRedisKey(tt.token.ID) + + // 執行 Redis Pipeline + err := r.Pipelined(func(tx redis.Pipeliner) error { + return repo.setToken(context.Background(), tx, tt.token.ID, tt.body, tt.ttl) + }) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 驗證 Token 是否已設置 + val, err := mr.Get(tokenKey) + assert.NoError(t, err) + assert.Equal(t, string(tt.body), val) + + // 檢查 Token 鍵的過期時間 + ttlVal := mr.TTL(tokenKey) + assert.Equal(t, tt.ttl.Seconds(), ttlVal.Seconds()) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_RunPipeline(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試場景 + tests := []struct { + name string + prepareFunc func() error // 準備函數,用於模擬 Redis 錯誤 + fn func(tx redis.Pipeliner) error // 要在 Pipeline 中執行的函數 + wantErr bool // 是否期望錯誤 + errMsg string // 預期的錯誤信息 + }{ + { + name: "Successful Pipeline Execution", + fn: func(tx redis.Pipeliner) error { + // 模擬一個簡單的操作 + return tx.Set(context.Background(), "testkey", "testvalue", 0).Err() + }, + wantErr: false, + }, + { + name: "Pipeline Function Error", + fn: func(tx redis.Pipeliner) error { + return errors.New("forced function error") // 模擬 Pipeline 操作中的錯誤 + }, + wantErr: true, + errMsg: "forced function error", + }, + { + name: "Redis Pipeline Error", + fn: func(tx redis.Pipeliner) error { + return tx.Set(context.Background(), "testkey", "testvalue", 0).Err() + }, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 操作錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數以模擬 Redis 錯誤 + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 runPipeline 並捕獲錯誤 + err := repo.runPipeline(context.Background(), tt.fn) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 如果操作成功,驗證 Redis 中的鍵 + if tt.name == "Successful Pipeline Execution" { + val, err := mr.Get("testkey") + assert.NoError(t, err) + assert.Equal(t, "testvalue", val) + } + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +// 定義一個無法序列化的結構以模擬序列化錯誤 +type Unserializable struct{} + +func (u Unserializable) MarshalJSON() ([]byte, error) { + return nil, errors.New("forced JSON marshal error") +} + +func TestTokenRepository_CreateOneTimeToken(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + key := "one_time_key" + duration := 10 * time.Second // 設置過期時間為 10 秒 + ticket := entity.Ticket{ + Data: "sample_data", + Token: entity.Token{ + ID: "token123", + AccessToken: "access123", + }, + } + + // 定義測試場景 + tests := []struct { + name string + key string + ticket entity.Ticket + duration time.Duration + prepareFunc func() error // 用於模擬 Redis 或序列化錯誤 + wantErr bool + errMsg string + }{ + { + name: "Successful one-time token creation", + key: key, + ticket: ticket, + duration: duration, + wantErr: false, + }, + { + name: "JSON marshal error", + key: key, + ticket: entity.Ticket{ + Data: Unserializable{}, + Token: entity.Token{ + ID: "invalid_token", + }, + }, + duration: duration, + wantErr: true, + errMsg: "json: error calling MarshalJSON for type repository.Unserializable: forced JSON marshal error", + }, + { + name: "Redis SetnxEx error", + key: key, + ticket: ticket, + duration: duration, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 操作錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 或序列化錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 CreateOneTimeToken 方法 + err := repo.CreateOneTimeToken(context.Background(), tt.key, tt.ticket, tt.duration) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 構建預期的 Redis 鍵 + oneTimeTokenKey := domain.GetRefreshTokenRedisKey(tt.key) + + // 檢查 Redis 中是否設置了臨時 Token + val, err := mr.Get(oneTimeTokenKey) + assert.NoError(t, err) + + expectedBody, _ := json.Marshal(tt.ticket) + assert.Equal(t, string(expectedBody), val) + + // 檢查過期時間 + ttl := mr.TTL(oneTimeTokenKey) + assert.Equal(t, tt.duration.Seconds(), ttl.Seconds()) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_GetAccessTokenByOneTimeToken(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + oneTimeToken := "one_time_token_123" + accessTokenID := "token123" + expectedToken := entity.Token{ + ID: accessTokenID, + UID: "user123", + DeviceID: "device123", + AccessToken: "access123", + ExpiresIn: 3600, + RefreshToken: "refresh123", + } + + // 在 Redis 中設置模擬的數據 + _ = mr.Set(domain.GetRefreshTokenRedisKey(oneTimeToken), accessTokenID) + tokenData, _ := json.Marshal(expectedToken) + _ = mr.Set(domain.GetAccessTokenRedisKey(accessTokenID), string(tokenData)) + + // 定義測試場景 + tests := []struct { + name string + oneTimeToken string + prepareFunc func() error // 用於模擬 Redis 錯誤 + expected entity.Token + wantErr bool + errMsg string + }{ + { + name: "Successful retrieval of access token by one-time token", + oneTimeToken: oneTimeToken, + expected: expectedToken, + wantErr: false, + }, + { + name: "Token not found in Redis", + oneTimeToken: "nonexistent_token", + wantErr: true, + errMsg: "failed to found token", + }, + { + name: "Redis Get error", + oneTimeToken: oneTimeToken, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 GetAccessTokenByOneTimeToken 方法 + result, err := repo.GetAccessTokenByOneTimeToken(context.Background(), tt.oneTimeToken) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_GetAccessTokensByUID(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + uid := "user123" + tokens := []entity.Token{ + { + ID: "token1", + UID: uid, + DeviceID: "device1", + AccessToken: "access1", + ExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshToken: "refresh1", + }, + { + ID: "token2", + UID: uid, + DeviceID: "device2", + AccessToken: "access2", + ExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshToken: "refresh2", + }, + } + + for _, token := range tokens { + err := repo.Create(context.Background(), token) + assert.NoError(t, err) + } + + // 定義測試場景 + tests := []struct { + name string + uid string + prepareFunc func() error // 用於模擬 Redis 錯誤 + expected []entity.Token + wantErr bool + errMsg string + }{ + { + name: "Successful retrieval of tokens by UID", + uid: uid, + expected: tokens, + wantErr: false, + }, + { + name: "UID not found in Redis", + uid: "nonexistent_user", + expected: []entity.Token{}, + wantErr: false, + }, + { + name: "Redis SMember error", + uid: uid, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 GetAccessTokensByUID 方法 + result, err := repo.GetAccessTokensByUID(context.Background(), tt.uid) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_GetAccessTokenCountByUID(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + uid := "user123" + uidKey := domain.GetUIDTokenRedisKey(uid) + + // 在 Redis 中設置模擬的數據 + _, _ = mr.SAdd(uidKey, "token1") + _, _ = mr.SAdd(uidKey, "token2") + _, _ = mr.SAdd(uidKey, "token3") + + // 定義測試場景 + tests := []struct { + name string + uid string + prepareFunc func() error // 用於模擬 Redis 錯誤 + expected int + wantErr bool + errMsg string + }{ + { + name: "Successful retrieval of token count by UID", + uid: uid, + expected: 3, + wantErr: false, + }, + { + name: "UID not found in Redis", + uid: "nonexistent_user", + expected: 0, + wantErr: false, + }, + { + name: "Redis Scard error", + uid: uid, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 GetAccessTokenCountByUID 方法 + result, err := repo.GetAccessTokenCountByUID(context.Background(), tt.uid) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_GetAccessTokensByDeviceID(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + deviceID := "device123" + deviceKey := domain.GetDeviceTokenRedisKey(deviceID) + + // 模擬在 Redis 中存儲多個 Token + tokens := []entity.Token{ + { + ID: "token1", + UID: "user123", + DeviceID: deviceID, + AccessToken: "access1", + ExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshToken: "refresh1", + }, + { + ID: "token2", + UID: "user123", + DeviceID: deviceID, + AccessToken: "access2", + ExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshToken: "refresh2", + }, + } + + // 在 Redis 中設置初始數據 + for _, token := range tokens { + tokenData, _ := json.Marshal(token) + _ = mr.Set(domain.GetAccessTokenRedisKey(token.ID), string(tokenData)) + _, _ = mr.SAdd(deviceKey, token.ID) + } + + // 定義測試場景 + tests := []struct { + name string + deviceID string + prepareFunc func() error // 用於模擬 Redis 錯誤 + expected []entity.Token + wantErr bool + errMsg string + }{ + { + name: "Successful retrieval of tokens by Device ID", + deviceID: deviceID, + expected: tokens, + wantErr: false, + }, + { + name: "Device ID not found in Redis", + deviceID: "nonexistent_device", + expected: []entity.Token{}, + wantErr: false, + }, + { + name: "Redis SMember error", + deviceID: deviceID, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 GetAccessTokensByDeviceID 方法 + result, err := repo.GetAccessTokensByDeviceID(context.Background(), tt.deviceID) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_GetAccessTokenCountByDeviceID(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + deviceID := "device123" + deviceKey := domain.GetDeviceTokenRedisKey(deviceID) + + // 在 Redis 中設置模擬的數據 + _, _ = mr.SAdd(deviceKey, "token1") + _, _ = mr.SAdd(deviceKey, "token2") + _, _ = mr.SAdd(deviceKey, "token3") + + // 定義測試場景 + tests := []struct { + name string + deviceID string + prepareFunc func() error // 用於模擬 Redis 錯誤 + expected int + wantErr bool + errMsg string + }{ + { + name: "Successful retrieval of token count by Device ID", + deviceID: deviceID, + expected: 3, + wantErr: false, + }, + { + name: "Device ID not found in Redis", + deviceID: "nonexistent_device", + expected: 0, + wantErr: false, + }, + { + name: "Redis Scard error", + deviceID: deviceID, + prepareFunc: func() error { + mr.SetError("forced Redis error") // 模擬 Redis 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 GetAccessTokenCountByDeviceID 方法 + result, err := repo.GetAccessTokenCountByDeviceID(context.Background(), tt.deviceID) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_Delete(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + token := entity.Token{ + ID: "token123", + UID: "user123", + DeviceID: "device123", + AccessToken: "access123", + RefreshToken: "refresh123", + ExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + } + + // 模擬在 Redis 中存儲 Token 的數據 + accessTokenKey := domain.GetAccessTokenRedisKey(token.ID) + refreshTokenKey := domain.GetRefreshTokenRedisKey(token.RefreshToken) + uidKey := domain.GetUIDTokenRedisKey(token.UID) + deviceIDKey := domain.GetDeviceTokenRedisKey(token.DeviceID) + + // 模擬在 Redis 中存儲 Token 的數據 + repo.Create(context.TODO(), token) + + // 定義測試場景 + tests := []struct { + name string + token entity.Token + prepareFunc func() error // 用於模擬 Redis 錯誤 + wantErr bool + errMsg string + jump bool + }{ + { + name: "Successful deletion of token", + token: token, + wantErr: false, + }, + { + name: "Redis delete error", + token: token, + prepareFunc: func() error { + mr.SetError("forced Redis delete error") // 模擬 Redis 錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis delete error", + }, + { + name: "Deletion of non-existent token", + token: entity.Token{ID: "nonexistent_token", UID: "user123", DeviceID: "device123"}, + wantErr: false, + jump: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 Delete 方法 + err := repo.Delete(context.Background(), tt.token) + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + if !tt.jump { + assert.NoError(t, err) + // 驗證 Token 的鍵已刪除 + _, err = mr.Get(accessTokenKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + _, err = mr.Get(refreshTokenKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + + // 驗證 UID 和 DeviceID 關聯已刪除 + uidSetMembers, err := mr.SMembers(uidKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + assert.NotContains(t, uidSetMembers, token.ID) + + deviceIDSetMembers, err := mr.SMembers(deviceIDKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + assert.NotContains(t, deviceIDSetMembers, token.ID) + } + } + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_DeleteAccessTokensByDeviceID(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + deviceID := "device123" + tokens := []entity.Token{ + { + ID: "token1", + UID: "user123", + DeviceID: deviceID, + AccessToken: "access1", + RefreshToken: "refresh1", + ExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + }, + { + ID: "token2", + UID: "user123", + DeviceID: deviceID, + AccessToken: "access2", + RefreshToken: "refresh2", + ExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + RefreshExpiresIn: time.Now().UTC().Add(60 * time.Minute).UnixNano(), + }, + } + + // 在 Redis 中設置初始數據 + deviceKey := domain.GetDeviceTokenRedisKey(deviceID) + for _, token := range tokens { + accessTokenKey := domain.GetAccessTokenRedisKey(token.ID) + refreshTokenKey := domain.GetRefreshTokenRedisKey(token.RefreshToken) + uidKey := domain.GetUIDTokenRedisKey(token.UID) + + _ = mr.Set(accessTokenKey, token.AccessToken) + _ = mr.Set(refreshTokenKey, token.ID) + _, _ = mr.SAdd(uidKey, token.ID) + _, _ = mr.SAdd(deviceKey, token.ID) + } + + // 定義測試場景 + tests := []struct { + name string + deviceID string + prepareFunc func() error // 用於模擬 Redis 錯誤 + wantErr bool + errMsg string + }{ + { + name: "Successful deletion of tokens by Device ID", + deviceID: deviceID, + wantErr: false, + }, + { + name: "GetAccessTokensByDeviceID error", + deviceID: deviceID, + prepareFunc: func() error { + mr.SetError("forced error in GetAccessTokensByDeviceID") // 模擬錯誤 + return nil + }, + wantErr: true, + errMsg: "forced error in GetAccessTokensByDeviceID", + }, + { + name: "Delete non-existent device ID", + deviceID: "nonexistent_device", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 DeleteAccessTokensByDeviceID 方法 + err := repo.DeleteAccessTokensByDeviceID(context.Background(), tt.deviceID) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 檢查是否刪除了 AccessToken、RefreshToken 和 UID 關聯的鍵 + for _, token := range tokens { + accessTokenKey := domain.GetAccessTokenRedisKey(token.ID) + refreshTokenKey := domain.GetRefreshTokenRedisKey(token.RefreshToken) + + _, err = mr.Get(accessTokenKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + + _, err = mr.Get(refreshTokenKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + + } + + // 檢查是否刪除了 deviceID 關聯的鍵 + _, err = mr.Get(deviceKey) + assert.Equal(t, miniredis.ErrKeyNotFound, err) + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_DeleteOneTimeToken(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + ids := []string{"one_time_token1", "one_time_token2"} + tokens := []entity.Token{ + {RefreshToken: "refresh_token1"}, + {RefreshToken: "refresh_token2"}, + } + + // 在 Redis 中設置模擬的數據 + for _, id := range ids { + _ = mr.Set(domain.GetRefreshTokenRedisKey(id), "dummy_value") + } + for _, token := range tokens { + _ = mr.Set(domain.GetRefreshTokenRedisKey(token.RefreshToken), "dummy_value") + } + + // 定義測試場景 + tests := []struct { + name string + ids []string + tokens []entity.Token + prepareFunc func() error // 用於模擬 Redis 錯誤 + wantErr bool + errMsg string + }{ + { + name: "Successful deletion of one-time tokens", + ids: ids, + tokens: tokens, + wantErr: false, + }, + { + name: "Deletion of non-existent one-time tokens", + ids: []string{"nonexistent_id1", "nonexistent_id2"}, + tokens: []entity.Token{{RefreshToken: "nonexistent_refresh1"}, {RefreshToken: "nonexistent_refresh2"}}, + wantErr: false, + }, + { + name: "Redis delete error", + ids: ids, + tokens: tokens, + prepareFunc: func() error { + mr.SetError("forced Redis delete error") // 模擬 Redis 刪除錯誤 + return nil + }, + wantErr: true, + errMsg: "forced Redis delete error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 DeleteOneTimeToken 方法 + err := repo.DeleteOneTimeToken(context.Background(), tt.ids, tt.tokens) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + // 驗證 Redis 中的鍵已刪除 + for _, id := range tt.ids { + key := domain.GetRefreshTokenRedisKey(id) + _, err := mr.Get(key) + assert.Equal(t, miniredis.ErrKeyNotFound, err) + } + for _, token := range tt.tokens { + key := domain.GetRefreshTokenRedisKey(token.RefreshToken) + _, err := mr.Get(key) + assert.Equal(t, miniredis.ErrKeyNotFound, err) + } + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} + +func TestTokenRepository_DeleteAccessTokensByUID(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + // 初始化 TokenRepository + repo := &TokenRepository{TokenRepositoryParam: TokenRepositoryParam{Redis: r}} + + // 定義測試參數 + uid := "user123" + tokens := []entity.Token{ + { + ID: "token1", + UID: uid, + DeviceID: "device1", + AccessToken: "access1", + RefreshToken: "refresh1", + }, + { + ID: "token2", + UID: uid, + DeviceID: "device2", + AccessToken: "access2", + RefreshToken: "refresh2", + }, + } + + // 在 Redis 中設置模擬的數據 + for _, token := range tokens { + accessTokenKey := domain.GetAccessTokenRedisKey(token.ID) + refreshTokenKey := domain.GetRefreshTokenRedisKey(token.RefreshToken) + uidKey := domain.GetUIDTokenRedisKey(uid) + + _ = mr.Set(accessTokenKey, token.AccessToken) + _ = mr.Set(refreshTokenKey, token.ID) + _, _ = mr.SAdd(uidKey, token.ID) + } + + // 定義測試場景 + tests := []struct { + name string + uid string + prepareFunc func() error // 用於模擬 Redis 錯誤 + wantErr bool + errMsg string + jump bool + }{ + { + name: "Successful deletion of tokens by UID", + uid: uid, + wantErr: false, + }, + { + name: "GetAccessTokensByUID error", + uid: uid, + prepareFunc: func() error { + mr.SetError("forced error in GetAccessTokensByUID") // 模擬查詢錯誤 + return nil + }, + wantErr: true, + errMsg: "forced error in GetAccessTokensByUID", + }, + { + name: "Delete non-existent UID", + uid: "nonexistent_uid", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 清除上一次的錯誤模擬 + mr.SetError("") + + // 執行準備函數(模擬 Redis 錯誤) + if tt.prepareFunc != nil { + tt.prepareFunc() + } + + // 執行 DeleteAccessTokensByUID 方法 + err := repo.DeleteAccessTokensByUID(context.Background(), tt.uid) + + // 檢查是否出現預期錯誤 + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + + if tt.jump { + // 驗證 Redis 中的鍵已刪除 + for _, token := range tokens { + accessTokenKey := domain.GetAccessTokenRedisKey(token.ID) + refreshTokenKey := domain.GetRefreshTokenRedisKey(token.RefreshToken) + uidKey := domain.GetUIDTokenRedisKey(uid) + + // 驗證 AccessToken 和 RefreshToken 鍵是否已刪除 + _, err := mr.Get(accessTokenKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + + _, err = mr.Get(refreshTokenKey) + assert.Error(t, miniredis.ErrKeyNotFound, err) + + // 驗證 UID 關聯是否已刪除 + uidSetMembers, err := mr.SMembers(uidKey) + assert.NoError(t, err) + assert.NotContains(t, uidSetMembers, token.ID) + } + } + } + + // 清除模擬錯誤 + mr.SetError("") + }) + } +} diff --git a/pkg/repository/user_role.go b/pkg/repository/user_role.go new file mode 100644 index 0000000..0f17755 --- /dev/null +++ b/pkg/repository/user_role.go @@ -0,0 +1,138 @@ +package repository + +import ( + "context" + "fmt" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +// UserRoleRepositoryParam 定義 MongoDB 配置參數 +type UserRoleRepositoryParam struct { + Conf *mgo.Conf + DBOpts []mon.Option +} + +// userRoleRepository 實作 repository.UserRoleRepository 介面 +type userRoleRepository struct { + DB mgo.DocumentDBUseCase +} + +// NewUserRoleRepository 初始化 `UserRoleRepository` +func NewUserRoleRepository(param UserRoleRepositoryParam) repository.UserRoleRepository { + e := entity.UserRole{} + db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...) + if err != nil { + panic(err) + } + + return &userRoleRepository{ + DB: db, + } +} + +// GetAll 取得所有 UserRole +func (repo *userRoleRepository) GetAll(ctx context.Context) ([]*entity.UserRole, error) { + var result []*entity.UserRole + err := repo.DB.GetClient().Find(ctx, &result, bson.M{}) + if err != nil { + return nil, err + } + + return result, nil +} + +// GetByUserID 透過 UID 查詢 UserRole +func (repo *userRoleRepository) GetByUserID(ctx context.Context, uid string) (entity.UserRole, error) { + var result entity.UserRole + err := repo.DB.GetClient().FindOne(ctx, &result, bson.M{"uid": uid}) + if err != nil { + return entity.UserRole{}, err + } + + return result, nil +} + +// GetUsersByRoleID 透過 RoleID 查詢所有使用此角色的使用者 +func (repo *userRoleRepository) GetUsersByRoleID(ctx context.Context, roleID string) ([]entity.UserRole, error) { + var result []entity.UserRole + err := repo.DB.GetClient().Find(ctx, &result, bson.M{"role_id": roleID}) + if err != nil { + return nil, err + } + + return result, nil +} + +// CountUsersByRole 統計每個角色的使用者數量 +func (repo *userRoleRepository) CountUsersByRole(ctx context.Context) ([]repository.RoleUserCount, error) { + pipeline := []bson.M{ + {"$group": bson.M{ + "_id": "$role_id", + "count": bson.M{"$sum": 1}, + }}, + } + + var result []repository.RoleUserCount + err := repo.DB.GetClient().Aggregate(ctx, &result, pipeline) + if err != nil { + return nil, err + } + + return result, nil +} + +// CreateUserRole 新增使用者角色 +func (repo *userRoleRepository) CreateUserRole(ctx context.Context, param entity.UserRole) error { + if param.UID == "" { + return fmt.Errorf("uid can't be empty") + } + + if param.RoleID == "" { + return fmt.Errorf("role_id can't be empty") + } + + if param.ID.IsZero() { + now := time.Now().UTC().UnixNano() + param.ID = primitive.NewObjectID() + param.CreateAt = now + param.UpdateAt = now + } + + _, err := repo.DB.GetClient().InsertOne(ctx, param) + + return err +} + +// UpdateUserRole 更新使用者角色 +func (repo *userRoleRepository) UpdateUserRole(ctx context.Context, uid, roleID string) (entity.UserRole, error) { + filter := bson.M{"uid": uid} + update := bson.M{ + "$set": bson.M{ + "role_id": roleID, + "update_at": time.Now().UTC().UnixNano(), + }, + } + + var updated entity.UserRole + err := repo.DB.GetClient().FindOneAndUpdate(ctx, &updated, filter, update) + if err != nil { + return entity.UserRole{}, err + } + + return updated, nil +} + +func (repo *userRoleRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + repo.DB.PopulateIndex(ctx, "role_id", 1, false) + repo.DB.PopulateIndex(ctx, "uid", 1, true) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/user_role_test.go b/pkg/repository/user_role_test.go new file mode 100644 index 0000000..0dedba2 --- /dev/null +++ b/pkg/repository/user_role_test.go @@ -0,0 +1,335 @@ +package repository + +import ( + "context" + "fmt" + "testing" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func SetupTestUserRoleRepository(db string) (repository.UserRoleRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + + conf := &mgo.Conf{ + Schema: Schema, + Host: fmt.Sprintf("%s:%s", h, p), + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + param := UserRoleRepositoryParam{ + Conf: conf, + } + repo := NewUserRoleRepository(param) + _, _ = repo.Index20250225UP(context.Background()) + + return repo, tearDown, nil +} + +func TestUserRoleRepository_CreateUserRole(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input entity.UserRole + expectErr bool + }{ + { + name: "成功新增使用者角色", + input: entity.UserRole{ + UID: "user_123", + RoleID: "role_456", + }, + expectErr: false, + }, + { + name: "異常:無效的輸入", + input: entity.UserRole{ + RoleID: "role_456", + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.CreateUserRole(context.Background(), tc.input) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + + // 檢查資料是否真的被插入 + var inserted entity.UserRole + inserted, err = repo.GetByUserID(context.Background(), tc.input.UID) + assert.NoError(t, err, "應該能找到插入的資料") + assert.Equal(t, tc.input.UID, inserted.UID, "UID 應該匹配") + assert.Equal(t, tc.input.RoleID, inserted.RoleID, "RoleID 應該匹配") + assert.NotZero(t, inserted.CreateAt, "CreateAt 應該被設定") + assert.NotZero(t, inserted.UpdateAt, "UpdateAt 應該被設定") + } + }) + } +} + +func TestUserRoleRepository_UpdateUserRole(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先插入測試數據 + existingUserRole := entity.UserRole{ + ID: primitive.NewObjectID(), + UID: "user_123", + RoleID: "role_123", + CreateAt: time.Now().UTC().UnixNano(), + UpdateAt: time.Now().UTC().UnixNano(), + } + + err = repo.CreateUserRole(context.Background(), existingUserRole) + assert.NoError(t, err, "初始化數據應該成功") + + testCases := []struct { + name string + uid string + newRoleID string + expectErr bool + expectedRole string + }{ + { + name: "成功更新使用者角色", + uid: "user_123", + newRoleID: "role_456", + expectErr: false, + expectedRole: "role_456", + }, + { + name: "異常:更新的 UID 不存在", + uid: "non_existent_user", + newRoleID: "role_789", + expectErr: true, + expectedRole: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err = repo.UpdateUserRole(context.Background(), tc.uid, tc.newRoleID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + // 檢查資料是否真的被更新 + found, err := repo.GetByUserID(context.Background(), tc.uid) + assert.NoError(t, err, "應該能找到更新後的資料") + assert.Equal(t, tc.expectedRole, found.RoleID, "RoleID 應該匹配更新的值") + assert.NotZero(t, found.UpdateAt, "UpdateAt 應該被更新") + } + }) + } +} + +func TestUserRoleRepository_CountUsersByRole(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先插入測試數據 + testUsers := []entity.UserRole{ + {ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_4", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_5", RoleID: "role_user"}, + } + + for _, user := range testUsers { + err := repo.CreateUserRole(context.Background(), user) + assert.NoError(t, err, "應該成功插入測試數據") + } + + testCases := []struct { + name string + expectedData []repository.RoleUserCount + expectErr bool + }{ + { + name: "成功統計角色使用者數量", + expectedData: []repository.RoleUserCount{ + {RoleID: "role_admin", Count: 2}, + {RoleID: "role_user", Count: 3}, + }, + expectErr: false, + }, + //{ + // name: "無資料時回傳空結果", + // expectedData: []repository.RoleUserCount{}, + // expectErr: false, + //}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.CountUsersByRole(context.Background()) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.ElementsMatch(t, tc.expectedData, result, "結果應該匹配預期的使用者數量統計") + } + }) + } +} + +// TestUserRoleRepository_GetUsersByRoleID 測試 GetUsersByRoleID 查詢指定 RoleID 的使用者 +func TestUserRoleRepository_GetUsersByRoleID(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testUsers := []entity.UserRole{ + {ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_4", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_5", RoleID: "role_user"}, + } + + for _, user := range testUsers { + err := repo.CreateUserRole(context.Background(), user) + assert.NoError(t, err, "應該成功插入測試數據") + } + + // 測試案例 + testCases := []struct { + name string + roleID string + expectedData []entity.UserRole + expectErr bool + }{ + { + name: "成功查詢 role_admin 的使用者", + roleID: "role_admin", + expectedData: []entity.UserRole{ + {UID: "user_1", RoleID: "role_admin"}, + {UID: "user_2", RoleID: "role_admin"}, + }, + expectErr: false, + }, + { + name: "成功查詢 role_user 的使用者", + roleID: "role_user", + expectedData: []entity.UserRole{ + {UID: "user_3", RoleID: "role_user"}, + {UID: "user_4", RoleID: "role_user"}, + {UID: "user_5", RoleID: "role_user"}, + }, + expectErr: false, + }, + { + name: "查詢 role_guest,應回傳空陣列", + roleID: "role_guest", + expectedData: []entity.UserRole{}, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetUsersByRoleID(context.Background(), tc.roleID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + compute := make([]entity.UserRole, 0, len(result)) + res := make([]entity.UserRole, 0, len(result)) + + for _, item := range result { + compute = append(compute, entity.UserRole{ + RoleID: item.RoleID, + UID: item.UID, + }) + } + + for _, item := range tc.expectedData { + res = append(res, entity.UserRole{ + RoleID: item.RoleID, + UID: item.UID, + }) + } + assert.ElementsMatch(t, compute, res, "結果應該匹配預期的使用者列表") + } + }) + } +} + +// TestUserRoleRepository_GetAll 測試 GetAll 取得所有使用者角色 +func TestUserRoleRepository_GetAll(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testUsers := []entity.UserRole{ + {ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_editor"}, + {ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_viewer"}, + } + + for _, user := range testUsers { + err := repo.CreateUserRole(context.Background(), user) + assert.NoError(t, err, "應該成功插入測試數據") + } + + // 測試案例 + testCases := []struct { + name string + expectedData []*entity.UserRole + expectErr bool + }{ + { + name: "成功獲取所有使用者角色", + expectedData: []*entity.UserRole{ + {UID: "user_1", RoleID: "role_admin"}, + {UID: "user_2", RoleID: "role_editor"}, + {UID: "user_3", RoleID: "role_viewer"}, + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetAll(context.Background()) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, len(tc.expectedData), "結果應該包含正確數量的使用者角色") + //assert.ElementsMatch(t, tc.expectedData, result, "結果應該匹配預期的使用者角色") + } + }) + } +} 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/casbin_redis_rbac.go b/pkg/usecase/casbin_redis_rbac.go new file mode 100644 index 0000000..2e470de --- /dev/null +++ b/pkg/usecase/casbin_redis_rbac.go @@ -0,0 +1,202 @@ +package usecase + +import ( + "context" + "fmt" + "log" + "net/http" + "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/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + "github.com/zeromicro/go-zero/core/logx" +) + +type RBACUseCaseParam struct { + ModulePath string + permissionRepo repository.PermissionRepository + roleRepo repository.RoleRepository + rolePermission repository.RolePermissionRepository + RBACRedisAdapter repository.RBACAdapter + // role permission 之類的 +} + +type RBACUseCase struct { + permissionRepo repository.PermissionRepository + roleRepo repository.RoleRepository + adapter repository.RBACAdapter + rolePermission repository.RolePermissionRepository + instance *casbin.Enforcer +} + +func NewRBACUseCase(param RBACUseCaseParam) usecase.RBACUseCase { + result := &RBACUseCase{ + adapter: param.RBACRedisAdapter, + permissionRepo: param.permissionRepo, + roleRepo: param.roleRepo, + rolePermission: param.rolePermission, + } + + // 1. 讀取 RBAC 模型 -> + m, err := model.NewModelFromFile(param.ModulePath) + if err != nil { + log.Fatalf("failed to load model: %v", err) + } + + // 3. 創建 Casbin Enforcer + enforcer, err := casbin.NewEnforcer(m, result.adapter) + if err != nil { + log.Fatalf("failed to init Enforcer: %v", err) + } + + result.instance = enforcer + + return result +} + +func (use *RBACUseCase) Check(ctx context.Context, role, path, method string) (usecase.CheckRolePermissionStatus, error) { + ok, p, err := use.instance.EnforceEx(role, path, method) + if err != nil { + e := errs.ForbiddenL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: fmt.Sprintf("role: %s, path: %s, method: %s", role, path, method)}, + {Key: "func", Value: "casbin.EnforceEx"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get permission") + + return usecase.CheckRolePermissionStatus{}, e + } + + status := usecase.CheckRolePermissionStatus{ + Allow: ok, + } + + // 檢查是否有明碼查詢權限 + if role == permission.AdminRoleUID { + status.Select.PlainCode = true + } else if ok && method == http.MethodGet { + policy, err := use.instance.GetModel().HasPolicy("p", "p", []string{ + role, path, method, p[3] + ".plain_code", + }) + if err != nil { + return usecase.CheckRolePermissionStatus{}, err + } + status.Select.PlainCode = policy + } + + limit := 4 + if len(p) >= limit { + status.Select.PermissionName = p[3] + } + + return status, nil +} + +func (use *RBACUseCase) LoadPolicy(ctx context.Context) error { + // 取得所有permission -> permission tree 拿到所有節點, + permissions, err := use.permissionRepo.GetAll(ctx, nil) + if err != nil { + return fmt.Errorf("permissionRepo.AllStatus error: %w", err) + } + + // 建立樹,我只要開啟的 Permission + tree := GeneratePermissionTree(permissions) + openMaps, err := tree.filterOpenNodes() + if err != nil { + return fmt.Errorf("GeneratePermissionTree.filterOpenNodes error: %w", err) + } + // 全部permission + permissionMap := make(map[string]entity.Permission, len(permissions)) + for k, v := range openMaps { + permissionMap[k] = v + } + + // 全部角色 + roles, err := use.roleRepo.All(ctx, nil) + if err != nil { + return fmt.Errorf("roleRepo.All error: %w", err) + } + + roleMap := make(map[string]entity.Role, len(roles)) + for _, v := range roles { + tmpValue := v + roleMap[v.ID.Hex()] = *tmpValue + } + + // 根據角色組合權限表 + for _, role := range roles { + rolePermission, err := use.rolePermission.Get(ctx, role.ID.Hex()) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToGetRolePermission, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: fmt.Sprintf("role: %s", role.ID.Hex())}, + {Key: "func", Value: "RolePermissionRepo.Get"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get rolePermission") + + return e + } + + for _, rp := range rolePermission { + r, ok := roleMap[rp.RoleID] + if !ok { + logx.Errorf(fmt.Sprintf("role_id: %s not found", rp.RoleID)) + + continue + } + + p, ok := permissionMap[rp.PermissionID] + if !ok { + logx.Errorf(fmt.Sprintf("permission_id: %s not found", rp.PermissionID)) + + continue + } + + if p.HTTPPath == "" || p.HTTPMethod == "" { + continue + } + + _, err = use.instance.AddPolicy(r.Name, p.HTTPPath, p.HTTPMethod, p.Name) + if err != nil { + return err + } + } + } + + err = use.instance.LoadPolicy() + if err != nil { + return err + } + + return nil +} + +func (use *RBACUseCase) SyncPolicy(ctx context.Context, cron time.Duration) { + t := time.NewTicker(cron) + + for { + select { + case <-t.C: + if err := use.LoadPolicy(ctx); err == nil { + logx.Info("LoadPolicy success") + } + case <-ctx.Done(): + t.Stop() + logx.Info("exit Policy success") + + return + } + } +} diff --git a/pkg/usecase/casbin_redis_rbac_test.go b/pkg/usecase/casbin_redis_rbac_test.go new file mode 100644 index 0000000..3d7d586 --- /dev/null +++ b/pkg/usecase/casbin_redis_rbac_test.go @@ -0,0 +1,119 @@ +package usecase + +import ( + "context" + "net/http" + "testing" + "time" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + mock "code.30cm.net/digimon/app-cloudep-permission-server/pkg/mock/repository" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/repository" + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/redis" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +func setupMiniRedis() (*miniredis.Miniredis, *redis.Redis) { + // 啟動 setupMiniRedis 作為模擬的 Redis 服務 + mr, err := miniredis.Run() + if err != nil { + panic("failed to start miniRedis: " + err.Error()) + } + + // 使用 setupMiniRedis 的地址配置 go-zero Redis 客戶端 + redisConf := redis.RedisConf{ + Host: mr.Addr(), + Type: "node", + } + r := redis.MustNewRedis(redisConf) + + return mr, r +} + +func TestNewRBACUseCase_LoadPolicy(t *testing.T) { + // 固定發行者設定,測試中會用來驗證 claims.Issuer + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + // 準備測試資料 + permID, _ := primitive.ObjectIDFromHex("507f1f77bcf86cd799439011") + permissionData := entity.Permission{ + ID: permID, + Parent: "", + Name: "read_perm", + HTTPMethod: http.MethodGet, + HTTPPath: "/resource", + Status: permission.Open, // 1: 啟用 + // 其他欄位可以隨便填上 + CreateAt: time.Now().Unix(), + UpdateAt: time.Now().Unix(), + } + + roleID, _ := primitive.ObjectIDFromHex("507f1f77bcf86cd799439012") + roleData := &entity.Role{ + ID: roleID, + Name: "test_role", + UID: "role1", + ClientID: "client1", + Status: permission.Open, + CreateAt: time.Now().Unix(), + UpdateAt: time.Now().Unix(), + } + + rolePermissionData := &entity.RolePermission{ + // 這裡 RoleID 與 PermissionID 皆以 Hex 字串儲存 + RoleID: roleID.Hex(), + PermissionID: permID.Hex(), + CreateAt: time.Now().Unix(), + UpdateAt: time.Now().Unix(), + } + + // 建立 mock 物件,並設定期望值 + permissionRepo := mock.NewMockPermissionRepository(mockCtrl) + roleRepo := mock.NewMockRoleRepository(mockCtrl) + rolePermissionRepo := mock.NewMockRolePermissionRepository(mockCtrl) + ctx := context.Background() + permissionRepo.EXPECT(). + GetAll(ctx, nil). + Return([]entity.Permission{permissionData}, nil) + + roleRepo.EXPECT(). + All(ctx, nil). + Return([]*entity.Role{roleData}, nil) + + rolePermissionRepo.EXPECT(). + Get(ctx, roleID.Hex()). + Return([]*entity.RolePermission{rolePermissionData}, nil) + + // 建立 miniRedis 與 adapter + mr, rdb := setupMiniRedis() + defer mr.Close() + + adapter, err := repository.NewRBACAdapter(repository.RBACAdapterParam{Redis: rdb}) + assert.NoError(t, err) + + // 建立 RBACUseCase 測試實例 + uc := NewRBACUseCase(RBACUseCaseParam{ + ModulePath: "../../etc/rbac.conf", // 注意:確認路徑下有對應的模型檔 + permissionRepo: permissionRepo, + roleRepo: roleRepo, + rolePermission: rolePermissionRepo, + RBACRedisAdapter: adapter, + }) + + // 載入 policy + err = uc.LoadPolicy(ctx) + assert.NoError(t, err) + + // 接下來測試 casbin 是否能正確檢查該權限 + status, err := uc.Check(ctx, roleData.Name, permissionData.HTTPPath, permissionData.HTTPMethod) + assert.NoError(t, err) + assert.True(t, status.Allow) + // 預期 PermissionName 為 "read_perm" + assert.Equal(t, "read_perm", status.Select.PermissionName) + +} diff --git a/pkg/usecase/permiission.go b/pkg/usecase/permiission.go new file mode 100644 index 0000000..80ff643 --- /dev/null +++ b/pkg/usecase/permiission.go @@ -0,0 +1,98 @@ +package usecase + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" +) + +type PermissionUseCaseParam struct { + permissionRepository repository.PermissionRepository +} + +type PermissionUseCase struct { + PermissionUseCaseParam +} + +func NewPermissionUseCase(param PermissionUseCaseParam) usecase.PermissionUseCase { + return &PermissionUseCase{ + PermissionUseCaseParam: param, + } +} + +func (use *PermissionUseCase) Insert(ctx context.Context, req usecase.CreatePermissionReq) error { + insert := entity.Permission{ + Name: req.Name, + HTTPMethod: req.HTTPMethod, + HTTPPath: req.HTTPPath, + Status: req.Status, + Type: req.Type, + } + if req.Parent != nil { + insert.Parent = *req.Parent + } + + err := use.permissionRepository.Insert(ctx, insert) + if err != nil { + return err + } + + return nil +} + +func (use *PermissionUseCase) Del(ctx context.Context, id string) error { + err := use.Del(ctx, id) + if err != nil { + return err + } + + return nil +} + +func (use *PermissionUseCase) Update(ctx context.Context, id string, req usecase.UpdatePermissionReq) error { + update := repository.UpdatePermission{ + Name: req.Name, + HTTPMethod: req.HTTPMethod, + HTTPPath: req.HTTPPath, + Status: req.Status, + Type: req.Type, + } + + err := use.permissionRepository.Update(ctx, id, update) + if err != nil { + return err + } + + return nil +} + +func (use *PermissionUseCase) All(ctx context.Context, status *permission.Status) ([]entity.Permission, error) { + all, err := use.permissionRepository.GetAll(ctx, status) + if err != nil { + return nil, err + } + + return all, nil +} + +func (use *PermissionUseCase) FilterAll(ctx context.Context) ([]entity.Permission, error) { + all, err := use.permissionRepository.GetAll(ctx, nil) + if err != nil { + return nil, err + } + + nodes, err := GeneratePermissionTree(all).filterOpenNodes() + if err != nil { + return nil, err + } + + result := make([]entity.Permission, 0, len(nodes)) + for _, item := range nodes { + result = append(result, item) + } + + return result, nil +} diff --git a/pkg/usecase/permission_tree.go b/pkg/usecase/permission_tree.go new file mode 100644 index 0000000..3b09cbb --- /dev/null +++ b/pkg/usecase/permission_tree.go @@ -0,0 +1,222 @@ +package usecase + +import ( + "sync" + + "code.30cm.net/digimon/library-go/errs" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PermissionTree 用來管理權限節點,包含快速查詢 map 與輔助 map(例如名稱對應的 ID 列表) +type PermissionTree struct { + // dummy root 節點 + root *PermissionNode + // permission id => node + nodes map[string]*PermissionNode + // permission name => permission id + names map[string][]string + mu sync.RWMutex // 保護樹的並發存取 +} + +type PermissionNode struct { + Data entity.Permission + Parent *PermissionNode + Children []*PermissionNode +} + +const rootName = "root" + +// GeneratePermissionTree 根據扁平權限資料建立樹,並掛在 dummy root 下 +func GeneratePermissionTree(permissions []entity.Permission) *PermissionTree { + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + } + + // 1. 建立所有節點 + for _, perm := range permissions { + node := &PermissionNode{ + Data: perm, + Children: []*PermissionNode{}, + } + tree.nodes[perm.ID.Hex()] = node + tree.names[perm.Name] = append(tree.names[perm.Name], perm.ID.Hex()) + } + + // 2. 建立 dummy root 節點 + tree.root = &PermissionNode{ + Data: entity.Permission{ID: primitive.NewObjectID(), Name: rootName}, + Children: []*PermissionNode{}, + } + + // 3. 建立父子連結:若找不到父節點或 Parent 為 0,則掛在 dummy root 下 + for _, node := range tree.nodes { + if node.Data.Parent == "" { + node.Parent = tree.root + tree.root.Children = append(tree.root.Children, node) + } else if parent, ok := tree.nodes[node.Data.Parent]; ok { + node.Parent = parent + parent.Children = append(parent.Children, node) + } else { + // 若父節點不存在,預設掛在 dummy root 下 + node.Parent = tree.root + tree.root.Children = append(tree.root.Children, node) + } + } + + return tree +} + +// getNode 輔助函數:根據 ID 從樹中查找節點 +func (tree *PermissionTree) getNode(id string) *PermissionNode { + tree.mu.RLock() + defer tree.mu.RUnlock() + + return tree.nodes[id] +} + +//nolint:unused +func (tree *PermissionTree) put(node entity.Permission) { + parentNode := tree.getNode(node.Parent) + if parentNode == nil { + parentNode = tree.root + } + thisNode := &PermissionNode{ + Data: node, + Parent: parentNode, + Children: make([]*PermissionNode, 0), + } + parentNode.Children = append(parentNode.Children, thisNode) + tree.names[node.Name] = append(tree.names[node.Name], node.ID.Hex()) + tree.nodes[node.ID.Hex()] = thisNode +} + +// filterOpenNodes 走訪整棵樹,列出有被打開的節點(父節點沒開,則底下的都不會開) +// 如果某個節點為非葉節點,則會檢查其子節點是否有啟用,否則該節點不會被展開。 +// [permissionID] entity.Permission +func (tree *PermissionTree) filterOpenNodes() (map[string]entity.Permission, error) { + tree.mu.RLock() + defer tree.mu.RUnlock() + + result := make(map[string]entity.Permission) + + // dfs 為內部閉包,可存取 result + // 返回值 bool 表示目前節點或其子孫中是否存在有效 open 節點 + var dfs func(node *PermissionNode) bool + dfs = func(node *PermissionNode) bool { + // 若本身狀態非 open,則整個分支不展開 + if node.Data.Status != permission.Open { + return false + } + + // 節點本身是 open,不論子節點狀態如何,先將該節點加入結果 + result[node.Data.ID.Hex()] = node.Data + + // 遞迴處理子節點 + for _, child := range node.Children { + dfs(child) + } + + return true + } + + // 從 dummy root 的 Children 開始走訪(dummy root 本身不納入結果) + for _, child := range tree.root.Children { + dfs(child) + } + + return result, nil +} + +// getFullParentPermission 根據 role permission 找出完整 path permission status +func (tree *PermissionTree) getFullParentPermission(rolePermissions []*entity.RolePermission) permission.Permissions { + status := permission.Permissions{} + + for _, v := range rolePermissions { + node := tree.getNode(v.PermissionID) + + if node == nil { + return nil + } + + if _, ok := status[node.Data.Name]; ok { + continue + } + + status[node.Data.Name] = permission.StatusCode(node.Data.Status.String()) + + // 往上找node(父) + for node.Parent != nil { + np := node.Parent + if np == nil || np.Data.Name == rootName { + break + } + parent := tree.getNode(np.Data.ID.Hex()) + if parent == nil { + continue + } + + status[parent.Data.Name] = permission.StatusCode(parent.Data.Status.String()) + } + } + + return status +} + +// getFullParentPermissionIDs 根據 permissions 找出所有完整 permission +// 比如B的父權限是A,給B權限就要回傳A與B權限id +func (tree *PermissionTree) getFullParentPermissionIDs(permissions permission.Permissions) ([]string, error) { + exist := make(map[string]bool) + ids := make([]string, 0) + + for name, status := range permissions { + if status != permission.OpenPermission { + continue + } + + pIDs, ok := tree.names[name] + if !ok { + return nil, errs.ResourceNotFound("failed to get node") + } + + for _, pID := range pIDs { + node := tree.getNode(pID) + if node == nil { + return nil, errs.ResourceNotFound("failed to get node") + } + + // 如果有子node代表不是最底層權限而是父node,就必須檢查此父node底下子node權限是否有開啟 + if len(node.Children) > 0 { + var can bool + for _, ch := range node.Children { + if v, ok := permissions[ch.Data.Name]; ok && v == permission.OpenPermission { + can = true + + break + } + } + + if !can { + continue + } + } + + for node.Parent != nil { + np := node.Parent + if np == nil || np.Data.Name == rootName { + break + } + if _, ok := exist[np.Data.ID.Hex()]; !ok { + ids = append(ids, np.Data.ID.Hex()) + exist[np.Data.ID.Hex()] = true + } + } + } + } + + return ids, nil +} diff --git a/pkg/usecase/permission_tree_test.go b/pkg/usecase/permission_tree_test.go new file mode 100644 index 0000000..cbfc9d5 --- /dev/null +++ b/pkg/usecase/permission_tree_test.go @@ -0,0 +1,359 @@ +package usecase + +import ( + "sync" + "testing" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// TestGeneratePermissionTree 測試 GeneratePermissionTree 函數的建立樹功能 +func TestGeneratePermissionTree(t *testing.T) { + // 準備測試資料 + // p1 為根節點(Parent 為空) + p1ID := primitive.NewObjectID() + p1 := entity.Permission{ + ID: p1ID, + Parent: "", + Name: "A", + } + + // p2 的父節點為 p1 + p2ID := primitive.NewObjectID() + p2 := entity.Permission{ + ID: p2ID, + Parent: p1ID.Hex(), + Name: "B", + } + + // p3 為另一個根節點(Parent 為空) + p3ID := primitive.NewObjectID() + p3 := entity.Permission{ + ID: p3ID, + Parent: "", + Name: "C", + } + + // p4 的 Parent 填寫一個不存在的 id,預期會掛在 dummy root 下 + p4ID := primitive.NewObjectID() + p4 := entity.Permission{ + ID: p4ID, + Parent: "nonexistent", + Name: "D", + } + + permissions := []entity.Permission{p1, p2, p3, p4} + + // 建立樹 + tree := GeneratePermissionTree(permissions) + + // 驗證 dummy root 下的子節點 + // 預期 p1、p3、p4 均掛在 dummy root 下 + if len(tree.root.Children) != 3 { + t.Errorf("expected 3 children under dummy root, got %d", len(tree.root.Children)) + } + + // 驗證 p1 的節點是否正確 + nodeP1, ok := tree.nodes[p1ID.Hex()] + if !ok { + t.Errorf("node for permission A (p1) not found") + } else { + // p1 應該有一個子節點 p2 + if len(nodeP1.Children) != 1 { + t.Errorf("expected node A to have 1 child, got %d", len(nodeP1.Children)) + } else { + if nodeP1.Children[0].Data.ID != p2ID { + t.Errorf("expected node A child to be permission B (p2), got %v", nodeP1.Children[0].Data.ID.Hex()) + } + } + } + + // 驗證 tree.names 的對應關係 + checkNames := []struct { + name string + expectedID string + }{ + {"A", p1ID.Hex()}, + {"B", p2ID.Hex()}, + {"C", p3ID.Hex()}, + {"D", p4ID.Hex()}, + } + for _, cn := range checkNames { + ids, ok := tree.names[cn.name] + if !ok { + t.Errorf("name mapping for %s not found", cn.name) + continue + } + if len(ids) != 1 || ids[0] != cn.expectedID { + t.Errorf("expected name mapping for %s to be [%s], got %v", cn.name, cn.expectedID, ids) + } + } +} + +func TestGetNode(t *testing.T) { + // 建立一個測試用的 PermissionTree + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + } + id := primitive.NewObjectID() + // 建立一個測試節點,ID 為 "testID" + perm := entity.Permission{ + ID: id, + Name: "Test Permission", + } + node := &PermissionNode{ + Data: perm, + } + + // 將測試節點插入 tree 的 nodes map + tree.mu.Lock() + tree.nodes["testID"] = node + tree.mu.Unlock() + + // 測試 getNode 返回存在的節點 + got := tree.getNode("testID") + if got == nil { + t.Error("Expected to find node with id 'testID', but got nil") + } else if got.Data.ID.Hex() != id.Hex() { + t.Errorf("Expected node ID 'testID', got '%s'", got.Data.ID) + } + + // 測試對不存在的 id,應回傳 nil + gotNil := tree.getNode("nonexistent") + if gotNil != nil { + t.Errorf("Expected nil for non-existent node, got %+v", gotNil) + } +} + +func TestPut(t *testing.T) { + // 建立一個 PermissionTree,並初始化 dummy root + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + } + // 建立 dummy root 節點,其 ID 為一個隨機 ObjectID,但不參與 mapping + dummyRootPerm := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "root", + } + tree.root = &PermissionNode{ + Data: dummyRootPerm, + Children: make([]*PermissionNode, 0), + } + + // 測試 1:放入一筆 Parent 為空的節點,預期掛在 dummy root 下 + permA := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 無父節點 + Name: "A", + } + tree.put(permA) + nodeA := tree.getNode(permA.ID.Hex()) + if nodeA == nil { + t.Errorf("Expected to find node A in tree.nodes") + } + if nodeA.Parent != tree.root { + t.Errorf("Expected node A's parent to be dummy root, got %v", nodeA.Parent.Data.Name) + } + if ids, ok := tree.names["A"]; !ok || len(ids) != 1 || ids[0] != permA.ID.Hex() { + t.Errorf("Expected tree.names for 'A' to contain %s, got %v", permA.ID.Hex(), tree.names["A"]) + } + + // 測試 2:放入一筆 Parent 為存在節點的節點 + // 先放入父節點 permB + permB := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 掛在 dummy root 下 + Name: "B", + } + tree.put(permB) + nodeB := tree.getNode(permB.ID.Hex()) + if nodeB == nil { + t.Errorf("Expected to find node B in tree.nodes") + } + + // 再放入子節點 permC,其 Parent 為 permB.ID.Hex() + permC := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permB.ID.Hex(), + Name: "C", + } + tree.put(permC) + nodeC := tree.getNode(permC.ID.Hex()) + if nodeC == nil { + t.Errorf("Expected to find node C in tree.nodes") + } + if nodeC.Parent != nodeB { + t.Errorf("Expected node C's parent to be node B") + } + // 驗證 nodeB 的 Children 是否包含 nodeC + found := false + for _, child := range nodeB.Children { + if child.Data.ID == permC.ID { + found = true + break + } + } + if !found { + t.Errorf("Expected node B's children to contain node C") + } +} + +func TestFilterOpenNodes(t *testing.T) { + // 建立一個 PermissionTree,初始化 nodes 與 names,並建立 dummy root 節點 + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + mu: sync.RWMutex{}, + } + // 建立 dummy root 節點 + dummyRootPerm := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "root", + Status: permission.Open, // dummy root 狀態不影響結果 + } + tree.root = &PermissionNode{ + Data: dummyRootPerm, + Children: make([]*PermissionNode, 0), + } + + // 建立測試節點 + // Node A:Open, leaf + permA := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 無父節點 → 掛在 dummy root 下 + Name: "A", + Status: permission.Open, + } + tree.put(permA) + + // Node B:Open, non-leaf + permB := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 掛在 dummy root 下 + Name: "B", + Status: permission.Open, + } + tree.put(permB) + + // Node B1:Open, leaf, Parent = B + permB1 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permB.ID.Hex(), + Name: "B1", + Status: permission.Open, + } + tree.put(permB1) + + // Node B2:Closed, leaf, Parent = B + permB2 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permB.ID.Hex(), + Name: "B2", + Status: permission.Close, + } + tree.put(permB2) + + // Node C:Open, non-leaf,但其子節點皆 Closed → C 不會展開 + permC := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 掛在 dummy root 下 + Name: "C", + Status: permission.Close, + } + tree.put(permC) + + // Node C1:Closed, leaf, Parent = C + permC1 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permC.ID.Hex(), + Name: "C1", + Status: permission.Open, + } + tree.put(permC1) + + // Node C2:Closed, leaf, Parent = C + permC2 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permC.ID.Hex(), + Name: "C2", + Status: permission.Open, + } + tree.put(permC2) + + // Node D:Closed, leaf, 無父節點 + permD := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "D", + Status: permission.Close, + } + tree.put(permD) + + // Node E:Closed, leaf, 無父節點 + permE := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "E", + Status: permission.Open, + } + tree.put(permE) + + // Node E1:Closed, leaf, Parent = E + permE1 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permE.ID.Hex(), + Name: "E1", + Status: permission.Close, + } + tree.put(permE1) + + // Node E2:Closed, leaf, Parent = E + permE2 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permE.ID.Hex(), + Name: "E2", + Status: permission.Close, + } + tree.put(permE2) + + // 執行 filterOpenNodes + openNodes, err := tree.filterOpenNodes() + if err != nil { + t.Fatalf("filterOpenNodes returned error: %v", err) + } + + // 預期結果: + // - Node A 應該包含(open 且為葉節點) + // - Node B 應該包含(open 且其子節點 B1 為 open) + // - Node B1 應該包含(open, leaf) + // - Node B2 不包含(closed) + // - Node C 不包含(closed) + // - Node C1, C2 不包含(C Node Close) + // - Node D 不包含(closed) + // - Node E 包含(open) + // - Node E1, E2 不包含(本身Close) + expectedIDs := map[string]bool{ + permA.ID.Hex(): true, + permB.ID.Hex(): true, + permB1.ID.Hex(): true, + permE.ID.Hex(): true, + } + + // 檢查結果是否只包含預期的節點 + for id, perm := range openNodes { + if !expectedIDs[id] { + t.Errorf("Unexpected node in openNodes: id=%s, name=%s", id, perm.Name) + } + delete(expectedIDs, id) + } + + if len(expectedIDs) != 0 { + t.Errorf("Expected nodes not found in openNodes: %v", expectedIDs) + } +} diff --git a/pkg/usecase/role.go b/pkg/usecase/role.go new file mode 100644 index 0000000..73a29bc --- /dev/null +++ b/pkg/usecase/role.go @@ -0,0 +1,234 @@ +package usecase + +import ( + "context" + + "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" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/zeromicro/go-zero/core/logx" +) + +type RoleUseCaseParam struct { + roleRepository repository.RoleRepository +} + +type RoleUseCase struct { + RoleUseCaseParam +} + +func NewRoleUseCase(param RoleUseCaseParam) usecase.RoleUseCase { + return &RoleUseCase{ + RoleUseCaseParam: param, + } +} + +func (use *RoleUseCase) List(ctx context.Context, param usecase.ListQuery) ([]usecase.Role, int64, error) { + query := repository.ListQuery{ + PageSize: param.PageSize, + PageIndex: param.PageIndex, + ClientID: param.ClientID, + Name: param.Name, + UID: param.UID, + Status: param.Status, + } + if param.UID != nil { + query.UID = param.UID + } + + list, i, err := use.roleRepository.List(ctx, query) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToListRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "param", Value: param}, + {Key: "func", Value: "roleRepository.List"}, + {Key: "err", Value: err.Error()}, + }, + "failed to list role") + + return nil, 0, e + } + + result := make([]usecase.Role, 0, len(list)) + + for _, item := range list { + result = append(result, usecase.Role{ + ID: item.ID.Hex(), + UID: item.UID, + Name: item.Name, + Status: item.Status, + ClientID: item.ClientID, + CreateAt: item.CreateAt, + UpdateAt: item.UpdateAt, + }) + } + + return result, i, nil +} + +func (use *RoleUseCase) All(ctx context.Context, clientID *string) ([]usecase.Role, error) { + all, err := use.roleRepository.All(ctx, clientID) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToListRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "clientID", Value: clientID}, + {Key: "func", Value: "roleRepository.All"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get all role") + + return nil, e + } + + result := make([]usecase.Role, 0, len(all)) + + for _, item := range all { + result = append(result, usecase.Role{ + ID: item.ID.Hex(), + UID: item.UID, + Name: item.Name, + Status: item.Status, + ClientID: item.ClientID, + CreateAt: item.CreateAt, + UpdateAt: item.UpdateAt, + }) + } + + return result, nil +} + +func (use *RoleUseCase) GetByID(ctx context.Context, id string) (*usecase.Role, error) { + byID, err := use.roleRepository.GetByID(ctx, id) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToGetRoleByID, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "func", Value: "roleRepository.GetByID"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get role by id") + + return nil, e + } + + return &usecase.Role{ + ID: byID.ID.Hex(), + UID: byID.UID, + Name: byID.Name, + Status: byID.Status, + ClientID: byID.ClientID, + CreateAt: byID.CreateAt, + UpdateAt: byID.UpdateAt, + }, nil +} + +func (use *RoleUseCase) GetByUID(ctx context.Context, uid string) (*usecase.Role, error) { + byUID, err := use.roleRepository.GetByUID(ctx, uid) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToGetRoleByUID, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "uid", Value: uid}, + {Key: "func", Value: "roleRepository.GetByUID"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get role by uid") + + return nil, e + } + + return &usecase.Role{ + ID: byUID.ID.Hex(), + UID: byUID.UID, + Name: byUID.Name, + Status: byUID.Status, + ClientID: byUID.ClientID, + CreateAt: byUID.CreateAt, + UpdateAt: byUID.UpdateAt, + }, nil +} + +func (use *RoleUseCase) Create(ctx context.Context, role usecase.CreateRoleReq) error { + err := use.roleRepository.Create(ctx, &entity.Role{ + UID: role.UID, + Name: role.Name, + Status: role.Status, + ClientID: role.ClientID, + }) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToCreateRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "param", Value: role}, + {Key: "func", Value: "roleRepository.Create"}, + {Key: "err", Value: err.Error()}, + }, + "failed to create role") + + return e + } + + return nil +} + +func (use *RoleUseCase) Update(ctx context.Context, id string, data usecase.CreateRoleReq) error { + err := use.roleRepository.Update(ctx, repository.UpdateReq{ + ID: id, + Name: &data.Name, + ClientID: &data.ClientID, + Status: &data.Status, + }) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToUpdateRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "param", Value: data}, + {Key: "func", Value: "roleRepository.Update"}, + {Key: "err", Value: err.Error()}, + }, + "failed to update role") + + return e + } + + return nil +} + +func (use *RoleUseCase) Delete(ctx context.Context, id string) error { + err := use.roleRepository.Delete(ctx, id) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToDelRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "func", Value: "roleRepository.Delete"}, + {Key: "err", Value: err.Error()}, + }, + "failed to delete role") + + return e + } + + return nil +} diff --git a/pkg/usecase/role_permission.go b/pkg/usecase/role_permission.go new file mode 100644 index 0000000..b91cae0 --- /dev/null +++ b/pkg/usecase/role_permission.go @@ -0,0 +1,119 @@ +package usecase + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" +) + +type RolePermissionUseCaseParam struct { + rolePermissionRepository repository.RolePermissionRepository + permissionRepository repository.PermissionRepository + roleRepository repository.RoleRepository + // userRoleRepository repository.UserRoleRepository +} + +type RolePermissionUseCase struct { + RolePermissionUseCaseParam +} + +func NewRolePermissionUseCase(param RolePermissionUseCaseParam) usecase.RolePermissionUseCase { + return &RolePermissionUseCase{ + RolePermissionUseCaseParam: param, + } +} + +// Get 拿到這個 Role ID 底下有哪些權限是開的 +func (use *RolePermissionUseCase) Get(ctx context.Context, roleID string) (permission.Permissions, error) { + // 拿到這個 role 底下有哪些 Permission + rolePermission, err := use.rolePermissionRepository.Get(ctx, roleID) + if err != nil { + return nil, err + } + p, err := use.permissionRepository.GetAll(ctx, nil) // -> 開的關的 permission 都會拿到 + if err != nil { + return nil, err + } + + return GeneratePermissionTree(p).getFullParentPermission(rolePermission), nil +} + +func (use *RolePermissionUseCase) Create(ctx context.Context, roleID string, permissions permission.Permissions) error { + // 如果加了一個,要把上面的節點都加入才可以 + permissionIDs, err := use.getPermissionIDs(ctx, permissions) + if err != nil { + return err + } + insert := make([]entity.RolePermission, 0, len(permissions)) + + for _, permissionID := range permissionIDs { + insert = append(insert, entity.RolePermission{ + RoleID: roleID, + PermissionID: permissionID, + }) + } + + err = use.rolePermissionRepository.Create(ctx, insert) + if err != nil { + return err + } + + return nil +} + +func (use *RolePermissionUseCase) getPermissionIDs(ctx context.Context, setPermissions permission.Permissions) ([]string, error) { + p, err := use.permissionRepository.GetAll(ctx, nil) + if err != nil { + return nil, err + } + + return GeneratePermissionTree(p).getFullParentPermissionIDs(setPermissions) +} + +func (use *RolePermissionUseCase) Delete(ctx context.Context, roleID string, permissions permission.Permissions) error { + // 如果加了一個,要把上面的節點都加入才可以 + permissionIDs, err := use.getPermissionIDs(ctx, permissions) + if err != nil { + return err + } + + err = use.rolePermissionRepository.Delete(ctx, roleID, permissionIDs) + if err != nil { + return err + } + + return nil +} + +func (use *RolePermissionUseCase) List(ctx context.Context, req usecase.ListQuery) (usecase.RoleResp, error) { + roles, total, err := use.roleRepository.List(ctx, repository.ListQuery{ + PageIndex: req.PageIndex, + PageSize: req.PageSize, + ClientID: req.ClientID, + UID: req.UID, + Name: req.Name, + Status: req.Status, + }) + if err != nil { + return usecase.RoleResp{}, err + } + + result := make([]usecase.Role, 0, len(roles)) + for _, item := range roles { + result = append(result, usecase.Role{ + ID: item.ID.Hex(), + UID: item.UID, + Name: item.Name, + Status: item.Status, + ClientID: item.ClientID, + }) + } + + return usecase.RoleResp{ + Total: total, + Roles: result, + }, nil +} diff --git a/pkg/usecase/token.go b/pkg/usecase/token.go new file mode 100644 index 0000000..c4c581c --- /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 // refresh token 過期時間(比較長) + Expired time.Duration // token 過期時間(比較短) + Secret string // 加密Token +} + +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..5cf8165 --- /dev/null +++ b/pkg/usecase/token_test.go @@ -0,0 +1,612 @@ +package usecase + +import ( + "context" + "fmt" + "testing" + "time" + + "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" + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// 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) + }) + } +} diff --git a/pkg/usecase/user_role.go b/pkg/usecase/user_role.go new file mode 100644 index 0000000..34248a4 --- /dev/null +++ b/pkg/usecase/user_role.go @@ -0,0 +1,132 @@ +package usecase + +import ( + "context" + "fmt" + + "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" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/zeromicro/go-zero/core/logx" +) + +type UserRoleUseCaseParam struct { + userRoleRepository repository.UserRoleRepository +} + +type UserRoleUseCase struct { + UserRoleUseCaseParam +} + +func NewUserRoleUseCase(param UserRoleUseCaseParam) usecase.UserRoleUseCase { + return &UserRoleUseCase{ + UserRoleUseCaseParam: param, + } +} + +func (use *UserRoleUseCase) Select(ctx context.Context, filter usecase.UserRoleFilter) ([]usecase.UserRole, error) { + id, err := use.userRoleRepository.GetUsersByRoleID(ctx, filter.RoleID) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToGetUserRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: filter}, + {Key: "func", Value: "userRoleRepository.GetUsersByRoleID"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get users by role id") + + return nil, e + } + + result := make([]usecase.UserRole, 0, len(id)) + for _, item := range result { + result = append(result, usecase.UserRole{ + UID: item.UID, + RoleID: filter.RoleID, + CreateAt: item.CreateAt, + UpdateAt: item.UpdateAt, + }) + } + + return result, nil +} + +func (use *UserRoleUseCase) Get(ctx context.Context, uid string) (usecase.UserRole, error) { + id, err := use.userRoleRepository.GetByUserID(ctx, uid) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToGetUserRoleByUID, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "uid", Value: uid}, + {Key: "func", Value: "userRoleRepository.GetByUserID"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get users by role id") + + return usecase.UserRole{}, e + } + + return usecase.UserRole{ + UID: id.UID, + RoleID: id.RoleID, + CreateAt: id.CreateAt, + UpdateAt: id.UpdateAt, + }, nil +} + +func (use *UserRoleUseCase) Create(ctx context.Context, uid, roleID string) (usecase.UserRole, error) { + ins := entity.UserRole{ + UID: uid, + RoleID: roleID, + } + err := use.userRoleRepository.CreateUserRole(ctx, ins) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToCreateUserRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "uid-role", Value: fmt.Sprintf("%s-%s", uid, roleID)}, + {Key: "func", Value: "userRoleRepository.CreateUserRole"}, + {Key: "err", Value: err.Error()}, + }, + "failed to create user role") + + return usecase.UserRole{}, e + } + + return usecase.UserRole{ + UID: ins.UID, + RoleID: ins.RoleID, + CreateAt: ins.CreateAt, + UpdateAt: ins.UpdateAt, + }, nil +} + +func (use *UserRoleUseCase) Update(ctx context.Context, uid, roleID string) error { + _, err := use.userRoleRepository.UpdateUserRole(ctx, uid, roleID) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToUpdateUserRole, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "uid-role", Value: fmt.Sprintf("%s-%s", uid, roleID)}, + {Key: "func", Value: "userRoleRepository.UpdateUserRole"}, + {Key: "err", Value: err.Error()}, + }, + "failed to update user role") + + return e + } + + return nil +}