diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8fc1271 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**仅限中文** + +在提之前请先查找[已有 issues](https://github.com/ecodeclub/ginx/issues),避免重复上报。 + +并且确保自己已经: +- [ ] 阅读过文档 +- [ ] 阅读过代码注释 +- [ ] 阅读过相关测试 + +### 问题简要描述 + +### 复现步骤 +> 通过编写单元、集成及e2e测试来复现Bug + +### 错误日志或者截图 + +### 你期望的结果 + +### 你排查的结果,或者你觉得可行的修复方案 +> 可选。希望你能够尽量先排查问题,这对于你个人能力提升很有帮助。 + +### 你设置的的 Go 环境? +> 上传 `go env` 的结果 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2247949 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +**仅限中文** + +### 使用场景 + +### 行业分析 +> 如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子 + +### 可行方案 +> 如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择 + +### 其它 +> 任何你觉得有利于解决问题的补充说明 + +### 你设置的的 Go 环境? +> 上传 `go env` 的结果 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..1ee3dac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,20 @@ +--- +name: Question +about: Want to ask some questions +title: '' +labels: question +--- + +**仅限中文** + +在提问之前请先查找[已有 issues](https://github.com/ecodeclub/ginx/issues),避免重复提问。 + +并且确保自己已经: +- [ ] 阅读过文档 +- [ ] 阅读过代码注释 +- [ ] 阅读过相关测试 + +### 你的问题 + +### 你设置的的 Go 环境? +> 上传 `go env` 的结果 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/refactor.md b/.github/ISSUE_TEMPLATE/refactor.md new file mode 100644 index 0000000..f94e212 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.md @@ -0,0 +1,18 @@ +--- +name: Refactor request +about: Refactor existing code +title: '' +labels: refactor +assignees: '' + +--- + +**仅限中文** + +### 当前实现缺陷 + +### 重构方案 +> 描述可以如何重构,以及重构之后带来的效果,如可读性、性能等方面的提升 + +### 其它 +> 任何你觉得有利于解决问题的补充说明 \ No newline at end of file diff --git a/.github/workflows/go-fmt.yml b/.github/workflows/go-fmt.yml new file mode 100644 index 0000000..230c442 --- /dev/null +++ b/.github/workflows/go-fmt.yml @@ -0,0 +1,42 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Format Go code + +on: + push: + branches: [ main,dev ] + pull_request: + branches: [ main,dev ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ">=1.20.0" + + - name: Install goimports + run: go install golang.org/x/tools/cmd/goimports@latest + + - name: Check + run: | + make check + if [ -n "$(git status --porcelain)" ]; then + echo >&2 "错误: 请在本地运行命令'make check'后再提交." + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..1e00067 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,40 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Go + +on: + push: + branches: [ dev,main ] + pull_request: + branches: [ dev,main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.20.0 + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -race -coverprofile=cover.out -v ./... + + - name: Post Coverage + uses: codecov/codecov-action@v2 \ No newline at end of file diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..7e4c788 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,61 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: golangci-lint +on: + push: + branches: + - dev + - main + pull_request: + branches: + - dev + - main +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.20.0 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + args: -c .golangci.yml + + # Optional: show only new issues if it's a pull request. The default value is `false`. + only-new-issues: true + + # Optional: if set to true then the all caching functionality will be complete disabled, + # takes precedence over all other caching options. + # skip-cache: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true \ No newline at end of file diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 0000000..0ae64fc --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,29 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Check License Lines +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + branches: + - develop + - main + - dev +jobs: + check-license-lines: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Check License Lines + uses: kt3k/license_checker@v1.0.6 \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..1761c9e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,33 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is inactive for a long time.' + stale-pr-message: 'This PR is inactive for a long time' + stale-issue-label: 'inactive-issue' + stale-pr-label: 'inactive-pr' diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..00126cc --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,18 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +run: + go: '1.21' + skip-dirs: + - .idea \ No newline at end of file diff --git a/.licenserc.json b/.licenserc.json new file mode 100644 index 0000000..4709c9b --- /dev/null +++ b/.licenserc.json @@ -0,0 +1,8 @@ +{ + "**/*.go": "// Copyright 2023 ecodeclub", + "**/*.{yml,toml}": "# Copyright 2023 ecodeclub", + "**/*.sh": "# Copyright 2023 ecodeclub", + "ignore": [ + "*.mock.go" + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index c3c37d9..c7e6056 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,28 @@ -.PHONY: mock -mock: - @mockgen -source=internal/ratelimit/types.go -package=limitmocks -destination=internal/ratelimit/mocks/ratelimit.mock.go - @go mod tidy \ No newline at end of file +.PHONY: bench +bench: + @go test -bench=. -benchmem ./... + +.PHONY: ut +ut: + @go test -tags=goexperiment.arenas -race ./... + +.PHONY: setup +setup: + @sh ./script/setup.sh + +.PHONY: fmt +fmt: + @sh ./script/goimports.sh + +.PHONY: lint +lint: + @golangci-lint run -c .golangci.yml + +.PHONY: tidy +tidy: + @go mod tidy -v + +.PHONY: check +check: + @$(MAKE) fmt + @$(MAKE) tidy \ No newline at end of file diff --git a/internal/integration/activelimit_test.go b/internal/integration/activelimit_test.go index 3edf207..c64aa37 100644 --- a/internal/integration/activelimit_test.go +++ b/internal/integration/activelimit_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + //go:build e2e package integration diff --git a/internal/integration/ratelimit_test.go b/internal/integration/ratelimit_test.go index a68f286..b36d1a6 100644 --- a/internal/integration/ratelimit_test.go +++ b/internal/integration/ratelimit_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + //go:build e2e package integration diff --git a/internal/ratelimit/mocks/ratelimit.mock.go b/internal/ratelimit/mocks/ratelimit.mock.go index 5a34ce8..01883ae 100644 --- a/internal/ratelimit/mocks/ratelimit.mock.go +++ b/internal/ratelimit/mocks/ratelimit.mock.go @@ -1,14 +1,14 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: internal/ratelimit/types.go +// Source: types.go // Package limitmocks is a generated GoMock package. package limitmocks import ( - "context" - "reflect" + context "context" + reflect "reflect" - "go.uber.org/mock/gomock" + gomock "go.uber.org/mock/gomock" ) // MockLimiter is a mock of Limiter interface. diff --git a/internal/ratelimit/redis_slide_window.go b/internal/ratelimit/redis_slide_window.go index cd00441..a383c9d 100644 --- a/internal/ratelimit/redis_slide_window.go +++ b/internal/ratelimit/redis_slide_window.go @@ -1,3 +1,17 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package ratelimit import ( diff --git a/internal/ratelimit/redis_slide_window_test.go b/internal/ratelimit/redis_slide_window_test.go index 1c4fb98..0e5be10 100644 --- a/internal/ratelimit/redis_slide_window_test.go +++ b/internal/ratelimit/redis_slide_window_test.go @@ -1,3 +1,17 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package ratelimit import ( diff --git a/internal/ratelimit/types.go b/internal/ratelimit/types.go index 8713bba..e603695 100644 --- a/internal/ratelimit/types.go +++ b/internal/ratelimit/types.go @@ -2,6 +2,7 @@ package ratelimit import "context" +//go:generate mockgen -source=types.go -package=limitmocks -destination=./mocks/ratelimit.mock.go type Limiter interface { // Limit 有没有触发限流。key 就是限流对象 // bool 代表是否限流,true 就是要限流 diff --git a/middlewares/jwt/claims.go b/jwt/claims_option.go similarity index 91% rename from middlewares/jwt/claims.go rename to jwt/claims_option.go index 44ae07a..0dc7f28 100644 --- a/middlewares/jwt/claims.go +++ b/jwt/claims_option.go @@ -7,11 +7,6 @@ import ( "github.com/golang-jwt/jwt/v5" ) -type RegisteredClaims[T any] struct { - Data T `json:"data"` - jwt.RegisteredClaims -} - type Options struct { Expire time.Duration // 有效期 EncryptionKey string // 加密密钥 @@ -25,7 +20,7 @@ type Options struct { // DecryptKey: 默认与 EncryptionKey 相同. // Method: 默认使用 jwt.SigningMethodHS256 签名方式. func NewOptions(expire time.Duration, encryptionKey string, - opts ...option.Option[Options]) *Options { + opts ...option.Option[Options]) Options { dOpts := Options{ Expire: expire, EncryptionKey: encryptionKey, @@ -36,7 +31,7 @@ func NewOptions(expire time.Duration, encryptionKey string, option.Apply[Options](&dOpts, opts...) - return &dOpts + return dOpts } // WithDecryptKey 设置解密密钥. diff --git a/middlewares/jwt/claims_test.go b/jwt/claims_option_test.go similarity index 98% rename from middlewares/jwt/claims_test.go rename to jwt/claims_option_test.go index 7fbccfd..df6d7a0 100644 --- a/middlewares/jwt/claims_test.go +++ b/jwt/claims_option_test.go @@ -15,13 +15,13 @@ func TestNewOptions(t *testing.T) { name string expire time.Duration encryptionKey string - want *Options + want Options }{ { name: "normal", expire: 10 * time.Minute, encryptionKey: "sign key", - want: &Options{ + want: Options{ Expire: 10 * time.Minute, EncryptionKey: "sign key", DecryptKey: "sign key", diff --git a/middlewares/jwt/jwt.go b/jwt/management.go similarity index 72% rename from middlewares/jwt/jwt.go rename to jwt/management.go index 9bf6272..4e1f464 100644 --- a/middlewares/jwt/jwt.go +++ b/jwt/management.go @@ -9,7 +9,6 @@ import ( "time" "github.com/ecodeclub/ekit/bean/option" - "github.com/ecodeclub/ekit/set" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) @@ -17,24 +16,21 @@ import ( const bearerPrefix = "Bearer" var ( - ErrEmptyRefreshOpts = errors.New("refreshJWTOptions are nil") + errEmptyRefreshOpts = errors.New("refreshJWTOptions are nil") ) type Management[T any] struct { - ignorePath func(path string) bool // Middleware 方法中忽略认证的路径 - allowTokenHeader string // 认证的请求头(存放 token 的请求头 key) - exposeAccessHeader string // 暴露到外部的资源请求头 - exposeRefreshHeader string // 暴露到外部的刷新请求头 + allowTokenHeader string // 认证的请求头(存放 token 的请求头 key) + exposeAccessHeader string // 暴露到外部的资源请求头 + exposeRefreshHeader string // 暴露到外部的刷新请求头 - accessJWTOptions *Options // 资源 token 选项 - refreshJWTOptions *Options // 刷新 token 选项 - rotateRefreshToken bool // 轮换刷新令牌 - - nowFunc func() time.Time // 控制 jwt 的时间 + accessJWTOptions Options // 资源 token 选项 + refreshJWTOptions *Options // 刷新 token 选项 + rotateRefreshToken bool // 轮换刷新令牌 + nowFunc func() time.Time // 控制 jwt 的时间 } // NewManagement 定义一个 Management. -// ignorePath: 默认使用 func(path string) bool { return false } 也就是全部不忽略. // allowTokenHeader: 默认使用 authorization 为认证请求头. // exposeAccessHeader: 默认使用 x-access-token 为暴露外部的资源请求头. // exposeRefreshHeader: 默认使用 x-refresh-token 为暴露外部的刷新请求头. @@ -42,12 +38,8 @@ type Management[T any] struct { // 如要使用 refresh 相关功能则需要使用 WithRefreshJWTOptions 添加相关配置. // rotateRefreshToken: 默认不轮换刷新令牌. // 该配置需要设置 refreshJWTOptions 才有效. -func NewManagement[T any](accessJWTOptions *Options, +func NewManagement[T any](accessJWTOptions Options, opts ...option.Option[Management[T]]) *Management[T] { - - if accessJWTOptions == nil { - panic("accessJWTOptions 不允许为 nil") - } dOpts := defaultManagementOptions[T]() dOpts.accessJWTOptions = accessJWTOptions option.Apply[Management[T]](&dOpts, opts...) @@ -57,7 +49,6 @@ func NewManagement[T any](accessJWTOptions *Options, func defaultManagementOptions[T any]() Management[T] { return Management[T]{ - ignorePath: func(path string) bool { return false }, allowTokenHeader: "authorization", exposeAccessHeader: "x-access-token", exposeRefreshHeader: "x-refresh-token", @@ -66,27 +57,6 @@ func defaultManagementOptions[T any]() Management[T] { } } -// WithIgnorePath 设置忽略资源令牌认证的路径. -func WithIgnorePath[T any](fn func(path string) bool) option.Option[Management[T]] { - return func(m *Management[T]) { - m.ignorePath = fn - } -} - -// StaticIgnorePaths 设置静态忽略的路径. -func StaticIgnorePaths(paths ...string) func(path string) bool { - s := set.NewMapSet[string](len(paths)) - for _, path := range paths { - s.Add(path) - } - return func(path string) bool { - if s.Exist(path) { - return true - } - return false - } -} - // WithAllowTokenHeader 设置允许 token 的请求头. func WithAllowTokenHeader[T any](header string) option.Option[Management[T]] { return func(m *Management[T]) { @@ -109,9 +79,9 @@ func WithExposeRefreshHeader[T any](header string) option.Option[Management[T]] } // WithRefreshJWTOptions 设置刷新令牌相关的配置. -func WithRefreshJWTOptions[T any](refreshOpts *Options) option.Option[Management[T]] { +func WithRefreshJWTOptions[T any](refreshOpts Options) option.Option[Management[T]] { return func(m *Management[T]) { - m.refreshJWTOptions = refreshOpts + m.refreshJWTOptions = &refreshOpts } } @@ -167,34 +137,9 @@ func (m *Management[T]) Refresh(ctx *gin.Context) { ctx.Status(http.StatusNoContent) } -// Middleware 登录认证的中间件. -func (m *Management[T]) Middleware() gin.HandlerFunc { - return func(ctx *gin.Context) { - // 不需要校验 - if m.ignorePath(ctx.Request.URL.Path) { - return - } - - // 提取 token - tokenStr := m.extractTokenString(ctx) - if tokenStr == "" { - slog.Debug("failed to extract token") - ctx.AbortWithStatus(http.StatusUnauthorized) - return - } - - // 校验 token - clm, err := m.VerifyAccessToken(tokenStr, - jwt.WithTimeFunc(m.nowFunc)) - if err != nil { - slog.Debug("access token verification failed") - ctx.AbortWithStatus(http.StatusUnauthorized) - return - } - - // 设置 claims - m.SetClaims(ctx, clm) - } +// MiddlewareBuilder 登录认证的中间件. +func (m *Management[T]) MiddlewareBuilder() *MiddlewareBuilder[T] { + return newMiddlewareBuilder[T](m) } // extractTokenString 提取 token 字符串. @@ -246,10 +191,10 @@ func (m *Management[T]) VerifyAccessToken(token string, opts ...jwt.ParserOption } // GenerateRefreshToken 生成刷新 token. -// 需要设置 refreshJWTOptions 否则返回 ErrEmptyRefreshOpts 错误. +// 需要设置 refreshJWTOptions 否则返回 errEmptyRefreshOpts 错误. func (m *Management[T]) GenerateRefreshToken(data T) (string, error) { if m.refreshJWTOptions == nil { - return "", ErrEmptyRefreshOpts + return "", errEmptyRefreshOpts } nowTime := m.nowFunc() @@ -268,10 +213,10 @@ func (m *Management[T]) GenerateRefreshToken(data T) (string, error) { } // VerifyRefreshToken 校验刷新 token. -// 需要设置 refreshJWTOptions 否则返回 ErrEmptyRefreshOpts 错误. +// 需要设置 refreshJWTOptions 否则返回 errEmptyRefreshOpts 错误. func (m *Management[T]) VerifyRefreshToken(token string, opts ...jwt.ParserOption) (RegisteredClaims[T], error) { if m.refreshJWTOptions == nil { - return RegisteredClaims[T]{}, ErrEmptyRefreshOpts + return RegisteredClaims[T]{}, errEmptyRefreshOpts } t, err := jwt.ParseWithClaims(token, &RegisteredClaims[T]{}, func(*jwt.Token) (interface{}, error) { diff --git a/middlewares/jwt/jwt_test.go b/jwt/management_test.go similarity index 80% rename from middlewares/jwt/jwt_test.go rename to jwt/management_test.go index 9f01891..638f4cf 100644 --- a/middlewares/jwt/jwt_test.go +++ b/jwt/management_test.go @@ -45,93 +45,6 @@ var ( ) ) -func TestManagement_Middleware(t *testing.T) { - type testCase[T any] struct { - name string - m *Management[T] - reqBuilder func(t *testing.T) *http.Request - wantCode int - } - tests := []testCase[data]{ - { - // 验证失败 - name: "verify_failed", - m: NewManagement[data](defaultOption, - WithIgnorePath[data](StaticIgnorePaths("/login"))), - reqBuilder: func(t *testing.T) *http.Request { - req, err := http.NewRequest(http.MethodGet, "/", nil) - if err != nil { - t.Fatal(err) - } - req.Header.Add("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImZvbyI6IjEifSwiZXhwIjoxNjk1NTcxODAwLCJpYXQiOjE2OTU1NzEyMDB9.RMpM5YNgxl9OtCy4lt_JRxv6k8s6plCkthnAV-vbXEQ") - return req - }, - wantCode: http.StatusUnauthorized, - }, - { - // 提取 token 失败 - name: "extract_token_failed", - m: NewManagement[data](defaultOption, - WithIgnorePath[data](StaticIgnorePaths("/login"))), - reqBuilder: func(t *testing.T) *http.Request { - req, err := http.NewRequest(http.MethodGet, "/", nil) - if err != nil { - t.Fatal(err) - } - req.Header.Add("authorization", "Bearer ") - return req - }, - wantCode: http.StatusUnauthorized, - }, - { - // 无需认证直接通过 - name: "pass_without_authentication", - m: NewManagement[data](defaultOption, - WithIgnorePath[data](StaticIgnorePaths("/login"))), - reqBuilder: func(t *testing.T) *http.Request { - req, err := http.NewRequest(http.MethodGet, "/login", nil) - if err != nil { - t.Fatal(err) - } - return req - }, - wantCode: http.StatusOK, - }, - { - // 验证通过 - name: "pass_the_verification", - m: NewManagement[data](defaultOption, - WithIgnorePath[data](StaticIgnorePaths("/login")), - WithNowFunc[data](func() time.Time { - return time.UnixMilli(1695571500000) - }), - ), - reqBuilder: func(t *testing.T) *http.Request { - req, err := http.NewRequest(http.MethodGet, "/", nil) - if err != nil { - t.Fatal(err) - } - req.Header.Add("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImZvbyI6IjEifSwiZXhwIjoxNjk1NTcxODAwLCJpYXQiOjE2OTU1NzEyMDB9.RMpM5YNgxl9OtCy4lt_JRxv6k8s6plCkthnAV-vbXEQ") - return req - }, - wantCode: http.StatusOK, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := gin.Default() - server.Use(tt.m.Middleware()) - tt.m.registerRoutes(server) - - req := tt.reqBuilder(t) - recorder := httptest.NewRecorder() - - server.ServeHTTP(recorder, req) - assert.Equal(t, tt.wantCode, recorder.Code) - }) - } -} - func TestManagement_Refresh(t *testing.T) { type testCase[T any] struct { name string @@ -218,7 +131,7 @@ func TestManagement_Refresh(t *testing.T) { // 生成资源令牌失败 name: "gen_access_token_failed", m: NewManagement[data]( - &Options{ + Options{ Expire: 10 * time.Minute, EncryptionKey: encryptionKey, DecryptKey: encryptionKey, @@ -401,16 +314,19 @@ func TestManagement_GenerateRefreshToken(t *testing.T) { } tests := []testCase[data]{ { - name: "normal", - refreshJWTOptions: NewOptions(24*60*time.Minute, "refresh sign key"), - data: data{Foo: "1"}, - want: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImZvbyI6IjEifSwiZXhwIjoxNjk1NjU3NjAwLCJpYXQiOjE2OTU1NzEyMDB9.y2AQ98i0le5AbmJFgYCAfCVAphd_9NecmHdhtehMSZE", + name: "normal", + refreshJWTOptions: func() *Options { + opt := NewOptions(24*60*time.Minute, "refresh sign key") + return &opt + }(), + data: data{Foo: "1"}, + want: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImZvbyI6IjEifSwiZXhwIjoxNjk1NjU3NjAwLCJpYXQiOjE2OTU1NzEyMDB9.y2AQ98i0le5AbmJFgYCAfCVAphd_9NecmHdhtehMSZE", }, { name: "mistake", data: data{Foo: "1"}, want: "", - wantErr: ErrEmptyRefreshOpts, + wantErr: errEmptyRefreshOpts, }, } for _, tt := range tests { @@ -424,7 +340,7 @@ func TestManagement_GenerateRefreshToken(t *testing.T) { } func TestManagement_VerifyRefreshToken(t *testing.T) { - defaultRefOpts := &Options{ + defaultRefOpts := Options{ Expire: 24 * 60 * time.Minute, EncryptionKey: "refresh sign key", DecryptKey: "refresh sign key", @@ -502,7 +418,7 @@ func TestManagement_VerifyRefreshToken(t *testing.T) { }), ), token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImZvbyI6IjEifSwiZXhwIjoxNjk1NjU3NjAwLCJpYXQiOjE2OTU1NzEyMDB9.y2AQ98i0le5AbmJFgYCAfCVAphd_9NecmHdhtehMSZE", - wantErr: ErrEmptyRefreshOpts, + wantErr: errEmptyRefreshOpts, }, } for _, tt := range tests { @@ -599,7 +515,7 @@ func TestManagement_extractTokenString(t *testing.T) { func TestNewManagement(t *testing.T) { type testCase[T any] struct { name string - accessJWTOptions *Options + accessJWTOptions Options wantPanic bool } tests := []testCase[data]{ @@ -608,11 +524,6 @@ func TestNewManagement(t *testing.T) { accessJWTOptions: defaultOption, wantPanic: false, }, - { - name: "accessJWTOptions_are_nil", - accessJWTOptions: nil, - wantPanic: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -628,87 +539,6 @@ func TestNewManagement(t *testing.T) { } } -func TestWithIgnorePath(t *testing.T) { - type testCase[T any] struct { - name string - fn func() option.Option[Management[T]] - paths []string - want []bool - } - tests := []testCase[data]{ - { - name: "default", - fn: func() option.Option[Management[data]] { - return nil - }, - paths: []string{"profile", "abc"}, - want: []bool{false, false}, - }, - { - name: "all_exists_paths", - fn: func() option.Option[Management[data]] { - return WithIgnorePath[data](defaultIgnorePaths) - }, - paths: []string{"/login", "/signup"}, - want: []bool{true, true}, - }, - { - name: "one_path_does_not_exist", - fn: func() option.Option[Management[data]] { - return WithIgnorePath[data](defaultIgnorePaths) - }, - paths: []string{"/login", "/profile", "/signup"}, - want: []bool{true, false, true}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var ignoreFn func(path string) bool - if tt.fn() == nil { - ignoreFn = NewManagement[data]( - defaultOption, - ).ignorePath - } else { - ignoreFn = NewManagement[data]( - defaultOption, - tt.fn(), - ).ignorePath - } - exists := make([]bool, 0, len(tt.paths)) - for _, path := range tt.paths { - exists = append(exists, ignoreFn(path)) - } - assert.Equal(t, tt.want, exists) - }) - } -} - -func TestStaticIgnorePaths(t *testing.T) { - tests := []struct { - name string - paths []string - requestPaths []string - want []bool - }{ - { - name: "normal", - paths: []string{"login", "signup"}, - requestPaths: []string{"profile", "login", "info", "signup"}, - want: []bool{false, true, false, true}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotBool := make([]bool, 0, len(tt.want)) - fn := StaticIgnorePaths(tt.paths...) - for _, path := range tt.requestPaths { - gotBool = append(gotBool, fn(path)) - } - assert.Equal(t, tt.want, gotBool) - }) - } -} - func TestWithAllowTokenHeader(t *testing.T) { type testCase[T any] struct { name string diff --git a/jwt/middleware_builder.go b/jwt/middleware_builder.go new file mode 100644 index 0000000..35995b8 --- /dev/null +++ b/jwt/middleware_builder.go @@ -0,0 +1,81 @@ +package jwt + +import ( + "github.com/ecodeclub/ekit/set" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "log/slog" + "net/http" + "time" +) + +// MiddlewareBuilder 创建一个校验登录的 middleware +// ignorePath: 默认使用 func(path string) bool { return false } 也就是全部不忽略. +type MiddlewareBuilder[T any] struct { + ignorePath func(path string) bool // Middleware 方法中忽略认证的路径 + manager *Management[T] + nowFunc func() time.Time // 控制 jwt 的时间 +} + +func newMiddlewareBuilder[T any](m *Management[T]) *MiddlewareBuilder[T] { + return &MiddlewareBuilder[T]{ + manager: m, + ignorePath: func(path string) bool { + return false + }, + nowFunc: m.nowFunc, + } +} + +func (m *MiddlewareBuilder[T]) IgnorePath(path ...string) *MiddlewareBuilder[T] { + return m.IgnorePathFunc(staticIgnorePaths(path...)) +} + +// IgnorePathFunc 设置忽略资源令牌认证的路径. +func (m *MiddlewareBuilder[T]) IgnorePathFunc(fn func(path string) bool) *MiddlewareBuilder[T] { + m.ignorePath = fn + return m +} + +func (m *MiddlewareBuilder[T]) Build() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 不需要校验 + if m.ignorePath(ctx.Request.URL.Path) { + return + } + + // 提取 token + tokenStr := m.manager.extractTokenString(ctx) + if tokenStr == "" { + slog.Debug("failed to extract token") + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + + // 校验 token + clm, err := m.manager.VerifyAccessToken(tokenStr, + jwt.WithTimeFunc(m.nowFunc)) + if err != nil { + slog.Debug("access token verification failed") + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + + // 设置 claims + m.manager.SetClaims(ctx, clm) + } +} + +// staticIgnorePaths 设置静态忽略的路径. +func staticIgnorePaths(paths ...string) func(path string) bool { + s := set.NewMapSet[string](len(paths)) + for _, path := range paths { + s.Add(path) + } + return func(path string) bool { + if s.Exist(path) { + return true + } + return false + } +} diff --git a/jwt/middleware_builder_test.go b/jwt/middleware_builder_test.go new file mode 100644 index 0000000..2e72151 --- /dev/null +++ b/jwt/middleware_builder_test.go @@ -0,0 +1,94 @@ +package jwt + +import ( + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestMiddlewareBuilder_Build(t *testing.T) { + type testCase[T any] struct { + name string + m *Management[T] + reqBuilder func(t *testing.T) *http.Request + wantCode int + } + tests := []testCase[data]{ + { + // 验证失败 + name: "verify_failed", + m: NewManagement[data](defaultOption), + reqBuilder: func(t *testing.T) *http.Request { + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Add("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImZvbyI6IjEifSwiZXhwIjoxNjk1NTcxODAwLCJpYXQiOjE2OTU1NzEyMDB9.RMpM5YNgxl9OtCy4lt_JRxv6k8s6plCkthnAV-vbXEQ") + return req + }, + wantCode: http.StatusUnauthorized, + }, + { + // 提取 token 失败 + name: "extract_token_failed", + m: NewManagement[data](defaultOption), + reqBuilder: func(t *testing.T) *http.Request { + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Add("authorization", "Bearer ") + return req + }, + wantCode: http.StatusUnauthorized, + }, + { + // 无需认证直接通过 + name: "pass_without_authentication", + m: NewManagement[data](defaultOption), + reqBuilder: func(t *testing.T) *http.Request { + req, err := http.NewRequest(http.MethodGet, "/login", nil) + if err != nil { + t.Fatal(err) + } + return req + }, + wantCode: http.StatusOK, + }, + { + // 验证通过 + name: "pass_the_verification", + m: NewManagement[data](defaultOption, + WithNowFunc[data](func() time.Time { + return time.UnixMilli(1695571500000) + }), + ), + reqBuilder: func(t *testing.T) *http.Request { + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Add("authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImZvbyI6IjEifSwiZXhwIjoxNjk1NTcxODAwLCJpYXQiOjE2OTU1NzEyMDB9.RMpM5YNgxl9OtCy4lt_JRxv6k8s6plCkthnAV-vbXEQ") + return req + }, + wantCode: http.StatusOK, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := gin.Default() + server.Use(tt.m.MiddlewareBuilder(). + IgnorePath("/login").Build()) + tt.m.registerRoutes(server) + + req := tt.reqBuilder(t) + recorder := httptest.NewRecorder() + + server.ServeHTTP(recorder, req) + assert.Equal(t, tt.wantCode, recorder.Code) + }) + } +} diff --git a/middlewares/jwt/types.go b/jwt/types.go similarity index 74% rename from middlewares/jwt/types.go rename to jwt/types.go index 6662d4f..d6f104e 100644 --- a/middlewares/jwt/types.go +++ b/jwt/types.go @@ -7,8 +7,8 @@ import ( // Manager jwt 管理器. type Manager[T any] interface { - // Middleware 登录认证的中间件. - Middleware() gin.HandlerFunc + // MiddlewareBuilder 创建登录认证的中间件. + MiddlewareBuilder() *MiddlewareBuilder[T] // Refresh 刷新 token 的 gin.HandlerFunc. // 需要设置 refreshJWTOptions 否则会出现 500 的 http 状态码. @@ -21,13 +21,18 @@ type Manager[T any] interface { VerifyAccessToken(token string, opts ...jwt.ParserOption) (RegisteredClaims[T], error) // GenerateRefreshToken 生成刷新 token. - // 需要设置 refreshJWTOptions 否则返回 ErrEmptyRefreshOpts 错误. + // 需要设置 refreshJWTOptions 否则返回 errEmptyRefreshOpts 错误. GenerateRefreshToken(data T) (string, error) // VerifyRefreshToken 校验刷新 token. - // 需要设置 refreshJWTOptions 否则返回 ErrEmptyRefreshOpts 错误. + // 需要设置 refreshJWTOptions 否则返回 errEmptyRefreshOpts 错误. VerifyRefreshToken(token string, opts ...jwt.ParserOption) (RegisteredClaims[T], error) // SetClaims 设置 claims 到 key=`claims` 的 gin.Context 中. SetClaims(ctx *gin.Context, claims RegisteredClaims[T]) } + +type RegisteredClaims[T any] struct { + Data T `json:"data"` + jwt.RegisteredClaims +} diff --git a/middlewares/accesslog/builder.go b/middlewares/accesslog/builder.go index c3ab0ac..d5e8434 100644 --- a/middlewares/accesslog/builder.go +++ b/middlewares/accesslog/builder.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package accesslog import ( diff --git a/middlewares/accesslog/builder_test.go b/middlewares/accesslog/builder_test.go index 30ecd4f..22003aa 100644 --- a/middlewares/accesslog/builder_test.go +++ b/middlewares/accesslog/builder_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package accesslog import ( diff --git a/middlewares/activelimit/locallimit/builder.go b/middlewares/activelimit/locallimit/builder.go index 9784d2d..b09d25d 100644 --- a/middlewares/activelimit/locallimit/builder.go +++ b/middlewares/activelimit/locallimit/builder.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package locallimit import ( diff --git a/middlewares/activelimit/locallimit/builder_test.go b/middlewares/activelimit/locallimit/builder_test.go index 0d6518b..af897be 100644 --- a/middlewares/activelimit/locallimit/builder_test.go +++ b/middlewares/activelimit/locallimit/builder_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package locallimit import ( diff --git a/middlewares/activelimit/redislimit/builder.go b/middlewares/activelimit/redislimit/builder.go index f3da2e2..9d295f8 100644 --- a/middlewares/activelimit/redislimit/builder.go +++ b/middlewares/activelimit/redislimit/builder.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package redislimit import ( diff --git a/middlewares/activelimit/redislimit/builder_test.go b/middlewares/activelimit/redislimit/builder_test.go index e6f8f13..4277521 100644 --- a/middlewares/activelimit/redislimit/builder_test.go +++ b/middlewares/activelimit/redislimit/builder_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package redislimit import ( diff --git a/middlewares/ratelimit/builder.go b/middlewares/ratelimit/builder.go index 3707016..fda2812 100644 --- a/middlewares/ratelimit/builder.go +++ b/middlewares/ratelimit/builder.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package ratelimit import ( diff --git a/middlewares/ratelimit/builder_test.go b/middlewares/ratelimit/builder_test.go index 471494f..c0fa288 100644 --- a/middlewares/ratelimit/builder_test.go +++ b/middlewares/ratelimit/builder_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package ratelimit import ( diff --git a/middlewares/ratelimit/redis_slide_window.go b/middlewares/ratelimit/redis_slide_window.go index b90f488..c3de352 100644 --- a/middlewares/ratelimit/redis_slide_window.go +++ b/middlewares/ratelimit/redis_slide_window.go @@ -1,3 +1,17 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package ratelimit import ( diff --git a/script/goimports.sh b/script/goimports.sh new file mode 100755 index 0000000..97c89e6 --- /dev/null +++ b/script/goimports.sh @@ -0,0 +1,15 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +goimports -l -w $(find . -type f -name '*.go' -not -path "./.idea/*") \ No newline at end of file diff --git a/script/setup.sh b/script/setup.sh new file mode 100755 index 0000000..5d1e4ad --- /dev/null +++ b/script/setup.sh @@ -0,0 +1,36 @@ +# Copyright 2023 ecodeclub +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SOURCE_COMMIT=.github/pre-commit +TARGET_COMMIT=.git/hooks/pre-commit +SOURCE_PUSH=.github/pre-push +TARGET_PUSH=.git/hooks/pre-push + +# copy pre-commit file if not exist. +echo "设置 git pre-commit hooks..." +cp $SOURCE_COMMIT $TARGET_COMMIT + +# copy pre-push file if not exist. +echo "设置 git pre-push hooks..." +cp $SOURCE_PUSH $TARGET_PUSH + +# add permission to TARGET_PUSH and TARGET_COMMIT file. +test -x $TARGET_PUSH || chmod +x $TARGET_PUSH +test -x $TARGET_COMMIT || chmod +x $TARGET_COMMIT + +echo "安装 golangci-lint..." +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +echo "安装 goimports..." +go install golang.org/x/tools/cmd/goimports@latest \ No newline at end of file