diff --git a/.github/workflows/develop.yaml b/.github/workflows/develop.yaml index 7a7b9586..6e799d9d 100644 --- a/.github/workflows/develop.yaml +++ b/.github/workflows/develop.yaml @@ -1,51 +1,60 @@ -name: Deploy +name: Publish Branch Image on: push: - branches: ["develop"] - pull_request: - branches: ["develop"] + branches: ["master", "dev"] + workflow_dispatch: + +permissions: + contents: read jobs: - build-and-deploy: + build-and-push: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Get short Git commit ID - id: vars - run: echo "COMMIT_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - - - name: Build Docker image - run: docker build --build-arg VERSION=${{ env.COMMIT_ID }} -t ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} . - - - name: Push Docker image - run: docker push ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} - -# - name: Deploy to server -# uses: appleboy/ssh-action@v0.1.6 -# with: -# host: ${{ secrets.SSH_HOST }} -# username: ${{ secrets.SSH_USER }} -# key: ${{ secrets.SSH_PRIVATE_KEY }} -# script: | -# if [ $(docker ps -a -q -f name=ppanel-server-dev) ]; then -# echo "Stopping and removing existing ppanel-server container..." -# docker stop ppanel-server-dev -# docker rm ppanel-server-dev -# else -# echo "No existing ppanel-server-dev container running." -# fi -# -# docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} -# docker run -d --restart=always --log-driver=journald --name ppanel-server-dev -p 8080:8080 -v /www/wwwroot/api/etc:/app/etc -v /www/wwwroot/api/logs:/app/logs --restart=always -d ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} -# \ No newline at end of file + - name: Extract image tags + id: tags + run: | + SHORT_SHA="${GITHUB_SHA::7}" + if [ "${GITHUB_REF_NAME}" = "master" ]; then + { + echo "tags<> "$GITHUB_OUTPUT" + else + { + echo "tags<> "$GITHUB_OUTPUT" + fi + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + build-args: | + VERSION=${{ github.sha }} + tags: ${{ steps.tags.outputs.tags }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04806206..24959aab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,15 +4,19 @@ on: tags: - 'v*' +permissions: + contents: write + packages: write + jobs: - build-docker: + build-image: runs-on: ubuntu-latest env: IMAGE_NAME: ppanel-server steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -22,11 +26,12 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to Docker Hub + - name: Log in to GHCR uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Extract version from git tag id: version @@ -40,7 +45,7 @@ jobs: run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV} - - name: Build and push Docker image for main release + - name: Build and push image for main release if: "!contains(github.ref_name, 'beta')" uses: docker/build-push-action@v6 with: @@ -51,10 +56,10 @@ jobs: build-args: | VERSION=${{ env.VERSION }} tags: | - ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest - ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }} + ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest + ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }} - - name: Build and push Docker image for beta release + - name: Build and push image for beta release if: contains(github.ref_name, 'beta') uses: docker/build-push-action@v6 with: @@ -65,15 +70,14 @@ jobs: build-args: | VERSION=${{ env.VERSION }} tags: | - ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:beta - ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }} + ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:beta + ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }} release-notes: runs-on: ubuntu-latest - needs: build-docker steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -88,7 +92,7 @@ jobs: - name: Run GoReleaser env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | goreleaser check goreleaser release --clean @@ -109,7 +113,7 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Extract version from git tag id: version run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV @@ -117,7 +121,7 @@ jobs: - name: Set BUILD_TIME env run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: wangyoucao577/go-release-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -129,3 +133,36 @@ jobs: binary_name: "ppanel-server" extra_files: LICENSE etc ldflags: -X "github.com/perfect-panel/server/pkg/constant.Version=${{env.VERSION}}" -X "github.com/perfect-panel/server/pkg/constant.BuildTime=${{env.BUILD_TIME}}" + + trigger-ppanel-image: + name: Trigger ppanel image build + runs-on: ubuntu-latest + needs: releases-matrix + steps: + - name: Dispatch packaging build + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + TAG: ${{ github.ref_name }} + SERVER_REPO: ${{ github.repository }} + TARGET_OWNER: ${{ github.repository_owner }} + run: | + PPANEL_REPO="${TARGET_OWNER}/ppanel" + + if [ -z "${GH_TOKEN}" ]; then + echo "GH_TOKEN secret is required to dispatch ${PPANEL_REPO}" >&2 + exit 1 + fi + + curl -sSfL -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + "https://api.github.com/repos/${PPANEL_REPO}/dispatches" \ + -d @- < master` pull request. +- The k3s prod environment deploys the latest `master`. +- The k3s dev environment deploys the latest `dev`. + +## Worktree workflow +```bash +git fetch origin +git worktree add ../ppanel-server-dev dev +git worktree add -b feat/your-change ../ppanel-server-feat dev +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..bcf75ede --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# CLAUDE Entry + +Common branch and worktree instructions are maintained in [BRANCH_STRATEGY.md](./BRANCH_STRATEGY.md). + +Claude-specific note: +- Follow `BRANCH_STRATEGY.md` as the shared source of truth before making branch, PR, or deployment decisions in this repository. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 063be39a..f193aad1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,9 +27,10 @@ To ensure the quality of the codebase and maintainability of the project, please - **Correct Branch**: - Develop new features based on `feature/*` branches. - Fix bugs based on `fix/*` branches. - - Ensure the target branch of the PR aligns with the project's branching strategy. + - Start `feature/*` and `fix/*` from `dev`. + - Merge daily development back into `dev` first, then promote `dev` to `master` via PR. -- **Sync with Base Branch**: Before submitting the PR, ensure your branch is up-to-date with the target branch (e.g., `main` or `develop`). +- **Sync with Base Branch**: Before submitting the PR, ensure your branch is up-to-date with the target branch (usually `dev`; only release promotions target `master`). ## 4. Review Process diff --git a/CONTRIBUTING_ZH.md b/CONTRIBUTING_ZH.md index c27158e4..a84201bd 100644 --- a/CONTRIBUTING_ZH.md +++ b/CONTRIBUTING_ZH.md @@ -27,9 +27,10 @@ - **正确的分支**: - 新功能应基于 `feature/*` 分支进行开发。 - Bug 修复应基于 `fix/*` 分支。 - - 确保 PR 的目标分支与项目的分支策略一致。 + - `feature/*` 与 `fix/*` 都应从 `dev` 拉出。 + - 日常开发先合并回 `dev`,再通过 PR 将 `dev` 提升到 `master`。 -- **同步主干代码**:在提交 PR 之前,请确保分支已经与目标分支(develop)同步。 +- **同步主干代码**:在提交 PR 之前,请确保分支已经与目标分支同步。日常开发通常同步 `dev`,发布提升时再面向 `master`。 ## 4. 审查流程 @@ -41,4 +42,3 @@ - diff --git a/internal/logic/auth/oauth/oAuthLoginLogic.go b/internal/logic/auth/oauth/oAuthLoginLogic.go index 063ed1de..26ae8fb2 100644 --- a/internal/logic/auth/oauth/oAuthLoginLogic.go +++ b/internal/logic/auth/oauth/oAuthLoginLogic.go @@ -46,7 +46,8 @@ func (l *OAuthLoginLogic) OAuthLogin(req *types.OAthLoginRequest) (resp *types.O uri, err = l.github() case "facebook": uri, err = l.facebook() - + default: + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not supported: %v", req.Method) } if err != nil { l.Errorw("OAuthLogin ", logger.Field("error", err.Error())) @@ -62,12 +63,18 @@ func (l *OAuthLoginLogic) google(req *types.OAthLoginRequest) (string, error) { if err != nil { return "", err } + if authMethod.Enabled == nil || !*authMethod.Enabled { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "google oauth is disabled") + } var cfg auth.GoogleAuthConfig err = json.Unmarshal([]byte(authMethod.Config), &cfg) if err != nil { l.Errorw("error unmarshal google config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) return "", err } + if cfg.ClientId == "" || cfg.ClientSecret == "" { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "google oauth config is incomplete") + } client := google.New(&google.Config{ ClientID: cfg.ClientId, ClientSecret: cfg.ClientSecret, @@ -92,12 +99,18 @@ func (l *OAuthLoginLogic) apple(req *types.OAthLoginRequest) (string, error) { if err != nil { return "", err } + if authMethod.Enabled == nil || !*authMethod.Enabled { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple oauth is disabled") + } var cfg auth.AppleAuthConfig err = json.Unmarshal([]byte(authMethod.Config), &cfg) if err != nil { l.Errorw("error unmarshal apple config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) return "", err } + if cfg.TeamID == "" || cfg.KeyID == "" || cfg.ClientId == "" || cfg.ClientSecret == "" || cfg.RedirectURL == "" { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "apple oauth config is incomplete") + } uri := "https://appleid.apple.com/auth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s&scope=name email&response_mode=form_post" // generate the state code code := random.KeyNew(8, 1) @@ -116,16 +129,22 @@ func (l *OAuthLoginLogic) telegram(req *types.OAthLoginRequest) (string, error) if err != nil { return "", err } + if authMethod.Enabled == nil || !*authMethod.Enabled { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "telegram oauth is disabled") + } var cfg auth.TelegramAuthConfig err = json.Unmarshal([]byte(authMethod.Config), &cfg) if err != nil { l.Errorw("error unmarshal apple config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) return "", err } + if cfg.BotToken == "" { + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "telegram oauth config is incomplete") + } // generate the state code code := random.KeyNew(8, 1) // save the state code - err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf("apple:%s", code), req.Redirect, 5*60*time.Second).Err() + err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf("telegram:%s", code), req.Redirect, 5*60*time.Second).Err() if err != nil { l.Errorw("error save state code to redis", logger.Field("code", code), logger.Field("error", err.Error())) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "error save state code to redis") diff --git a/internal/logic/common/getGlobalConfigLogic.go b/internal/logic/common/getGlobalConfigLogic.go index 393371bf..d54bb8c5 100644 --- a/internal/logic/common/getGlobalConfigLogic.go +++ b/internal/logic/common/getGlobalConfigLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/model/auth" "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -71,12 +72,14 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes } for _, method := range authMethods { - if *method.Enabled { - methods = append(methods, method.Method) - if method.Method == "device" { - _ = json.Unmarshal([]byte(method.Config), &resp.Auth.Device) - resp.Auth.Device.Enable = true - } + if !isPublicAuthMethodAvailable(method) { + continue + } + + methods = append(methods, method.Method) + if method.Method == "device" { + _ = json.Unmarshal([]byte(method.Config), &resp.Auth.Device) + resp.Auth.Device.Enable = true } } resp.OAuthMethods = methods @@ -90,3 +93,30 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes resp.WebAd = webAds.Value == "true" return } + +func isPublicAuthMethodAvailable(method *auth.Auth) bool { + if method == nil || method.Enabled == nil || !*method.Enabled { + return false + } + + switch method.Method { + case "email", "mobile", "device": + return true + case "google": + var cfg auth.GoogleAuthConfig + return cfg.Unmarshal(method.Config) == nil && cfg.ClientId != "" && cfg.ClientSecret != "" + case "apple": + var cfg auth.AppleAuthConfig + return cfg.Unmarshal(method.Config) == nil && + cfg.TeamID != "" && + cfg.KeyID != "" && + cfg.ClientId != "" && + cfg.ClientSecret != "" && + cfg.RedirectURL != "" + case "telegram": + var cfg auth.TelegramAuthConfig + return cfg.Unmarshal(method.Config) == nil && cfg.BotToken != "" + default: + return false + } +} diff --git a/internal/logic/common/getGlobalConfigLogic_test.go b/internal/logic/common/getGlobalConfigLogic_test.go new file mode 100644 index 00000000..84b165ed --- /dev/null +++ b/internal/logic/common/getGlobalConfigLogic_test.go @@ -0,0 +1,71 @@ +package common + +import ( + "testing" + + "github.com/perfect-panel/server/internal/model/auth" +) + +func TestIsPublicAuthMethodAvailable(t *testing.T) { + enabled := true + disabled := false + + tests := []struct { + name string + method *auth.Auth + want bool + }{ + { + name: "email enabled", + method: &auth.Auth{ + Method: "email", + Enabled: &enabled, + }, + want: true, + }, + { + name: "google enabled with config", + method: &auth.Auth{ + Method: "google", + Enabled: &enabled, + Config: `{"client_id":"client-id","client_secret":"client-secret","redirect_url":""}`, + }, + want: true, + }, + { + name: "telegram enabled without bot token", + method: &auth.Auth{ + Method: "telegram", + Enabled: &enabled, + Config: `{"bot_token":"","enable_notify":false,"webhook_domain":""}`, + }, + want: false, + }, + { + name: "github remains hidden", + method: &auth.Auth{ + Method: "github", + Enabled: &enabled, + Config: `{"client_id":"client-id","client_secret":"client-secret","redirect_url":"https://example.com"}`, + }, + want: false, + }, + { + name: "disabled method hidden", + method: &auth.Auth{ + Method: "telegram", + Enabled: &disabled, + Config: `{"bot_token":"token"}`, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isPublicAuthMethodAvailable(tt.method); got != tt.want { + t.Fatalf("isPublicAuthMethodAvailable() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/svc/mmdb.go b/internal/svc/mmdb.go index 331034f2..f8a5c0b9 100644 --- a/internal/svc/mmdb.go +++ b/internal/svc/mmdb.go @@ -18,17 +18,11 @@ type IPLocation struct { } func NewIPLocation(path string) (*IPLocation, error) { - - // 检查文件是否存在 - if _, err := os.Stat(path); os.IsNotExist(err) { - logger.Infof("[GeoIP] Database not found, downloading from %s", GeoIPDBURL) - // 文件不存在,下载数据库 - err := DownloadGeoIPDatabase(GeoIPDBURL, path) - if err != nil { - logger.Errorf("[GeoIP] Failed to download database: %v", err.Error()) - return nil, err + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, os.ErrNotExist } - logger.Infof("[GeoIP] Database downloaded successfully") + return nil, err } db, err := geoip2.Open(path) diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index aa79ccce..5bb3f692 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -22,6 +22,7 @@ import ( "github.com/perfect-panel/server/internal/model/traffic" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/pkg/limit" + "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/nodeMultiplier" "github.com/perfect-panel/server/pkg/orm" @@ -77,7 +78,8 @@ func NewServiceContext(c config.Config) *ServiceContext { // IP location initialize geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb") if err != nil { - panic(err.Error()) + logger.Errorf("[GeoIP] Failed to initialize database, continuing without GeoIP support: %v", err.Error()) + geoIP = nil } rds := redis.NewClient(&redis.Options{