From 421727977045abb472000225795e20d67cfa3a20 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 09:41:58 -0500 Subject: [PATCH 01/98] fix: enhance import session response structure and update preview query conditions --- backend/internal/api/handlers/import_handler.go | 17 +++++++++++++++-- .../api/handlers/import_handler_test.go | 4 +++- frontend/src/hooks/useImport.ts | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/backend/internal/api/handlers/import_handler.go b/backend/internal/api/handlers/import_handler.go index b0a0da35..4bc98f0a 100644 --- a/backend/internal/api/handlers/import_handler.go +++ b/backend/internal/api/handlers/import_handler.go @@ -63,7 +63,12 @@ func (h *ImportHandler) GetStatus(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "has_pending": true, - "session": session, + "session": gin.H{ + "id": session.UUID, + "state": session.Status, + "created_at": session.CreatedAt, + "updated_at": session.UpdatedAt, + }, }) } @@ -89,7 +94,15 @@ func (h *ImportHandler) GetPreview(c *gin.Context) { session.Status = "reviewing" h.db.Save(&session) - c.JSON(http.StatusOK, result) + c.JSON(http.StatusOK, gin.H{ + "session": gin.H{ + "id": session.UUID, + "state": session.Status, + "created_at": session.CreatedAt, + "updated_at": session.UpdatedAt, + }, + "preview": result, + }) } // Upload handles manual Caddyfile upload or paste. diff --git a/backend/internal/api/handlers/import_handler_test.go b/backend/internal/api/handlers/import_handler_test.go index c7939d8e..c499ab9f 100644 --- a/backend/internal/api/handlers/import_handler_test.go +++ b/backend/internal/api/handlers/import_handler_test.go @@ -93,7 +93,9 @@ func TestImportHandler_GetPreview(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var result map[string]interface{} json.Unmarshal(w.Body.Bytes(), &result) - hosts := result["hosts"].([]interface{}) + + preview := result["preview"].(map[string]interface{}) + hosts := preview["hosts"].([]interface{}) assert.Len(t, hosts, 1) // Verify status changed to reviewing diff --git a/frontend/src/hooks/useImport.ts b/frontend/src/hooks/useImport.ts index 7742824e..d2a16f7b 100644 --- a/frontend/src/hooks/useImport.ts +++ b/frontend/src/hooks/useImport.ts @@ -31,7 +31,7 @@ export function useImport() { const previewQuery = useQuery({ queryKey: ['import-preview'], queryFn: getImportPreview, - enabled: !!statusQuery.data?.has_pending && statusQuery.data?.session?.state === 'reviewing', + enabled: !!statusQuery.data?.has_pending && (statusQuery.data?.session?.state === 'reviewing' || statusQuery.data?.session?.state === 'pending'), }); const uploadMutation = useMutation({ From 6ba87eb121f6aa89ecb88d8bcfc0b377f84cd220 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 09:55:46 -0500 Subject: [PATCH 02/98] fix: rename workflow to Docker Build, Publish & Test and enhance image testing steps --- .github/workflows/docker-build.yml | 277 --------------------------- .github/workflows/docker-publish.yml | 98 +++++++++- 2 files changed, 95 insertions(+), 280 deletions(-) delete mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index 20cf2ec1..00000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,277 +0,0 @@ -name: Build and Push Docker Images - -on: - push: - branches: - - main # Pushes to main → tags as "latest" - - development # Pushes to development → tags as "dev" - tags: - - 'v*.*.*' # Version tags (v1.0.0, v1.2.3, etc.) → tags as version number - workflow_dispatch: # Allows manual trigger from GitHub UI - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/cpmp - -jobs: - build-and-push: - name: Build and Push Docker Image - runs-on: ubuntu-latest - concurrency: - group: docker-build-${{ github.ref }} - cancel-in-progress: true - outputs: - skip_build: ${{ steps.skip.outputs.skip_build }} - permissions: - contents: read - packages: write - security-events: write - - steps: - # Step 1: Download the code - - name: 📥 Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 - - - name: 🧪 Determine skip condition - id: skip - env: - ACTOR: ${{ github.actor }} - EVENT: ${{ github.event_name }} - HEAD_MSG: ${{ github.event.head_commit.message }} - run: | - should_skip=false - pr_title="" - if [ "$EVENT" = "pull_request" ]; then - pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '') - fi - if [ "$ACTOR" = "renovate[bot]" ]; then should_skip=true; fi - if echo "$HEAD_MSG" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi - if echo "$HEAD_MSG" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi - if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi - if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi - echo "skip_build=$should_skip" >> $GITHUB_OUTPUT - if [ "$should_skip" = true ]; then - echo "Skipping heavy docker build for actor=$ACTOR event=$EVENT (message/title matched)" - else - echo "Proceeding with full docker build" - fi - - # Step 2: Set up QEMU for multi-platform builds (ARM, AMD64, etc.) - - name: 🔧 Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - - # Step 3: Set up Docker Buildx (advanced Docker builder) - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 - - # Resolve immutable digest for Caddy base - - name: 📦 Resolve Caddy base digest - id: caddy - run: | - docker pull caddy:2-alpine - DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine) - echo "image=$DIGEST" >> $GITHUB_OUTPUT - - # Step 4: Log in to GitHub Container Registry - - name: 🔐 Log in to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.PROJECT_TOKEN }} - - # Step 5: Figure out what tags to use - - name: 🏷️ Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - # Tag "latest" for main branch - type=raw,value=latest,enable={{is_default_branch}} - # Tag "dev" for development branch - type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} - # Tag version numbers from git tags (v1.0.0 → 1.0.0) - type=semver,pattern={{version}} - # Tag major.minor from git tags (v1.2.3 → 1.2) - type=semver,pattern={{major}}.{{minor}} - # Tag major from git tags (v1.2.3 → 1) - type=semver,pattern={{major}} - # Ephemeral tag for pull requests (derive number from GITHUB_REF if available) - type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }} - # Short SHA tag as fallback (for non-default non-dev push events) - type=sha,format=short,enable=${{ github.event_name != 'pull_request' }} - - # Step 6: Build the frontend first - - name: 🎨 Build frontend - if: steps.skip.outputs.skip_build != 'true' - run: | - cd frontend - npm ci - npm run build - - # Step 7: Build and push Docker image - - name: 🐳 Build and push Docker image - if: steps.skip.outputs.skip_build != 'true' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 - id: build - with: - context: . - file: ./Dockerfile - platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - CADDY_IMAGE=${{ steps.caddy.outputs.image }} - - # Step 8: Run Trivy scan (table output first for visibility) - - name: 📋 Run Trivy scan (table output) - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' - id: trivy_table - continue-on-error: true - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} - format: 'table' - severity: 'CRITICAL,HIGH' - exit-code: '0' - - # Step 9: Run Trivy security scan (SARIF) - - name: 🔍 Run Trivy vulnerability scanner - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' - id: trivy - continue-on-error: true - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} - format: 'sarif' - output: 'trivy-results.sarif' - exit-code: '0' - severity: 'CRITICAL,HIGH' - - # Step 10: Upload Trivy results to GitHub Security tab - - name: 📤 Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4 - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && (steps.trivy.outcome == 'success' || steps.trivy.outcome == 'failure') - with: - sarif_file: 'trivy-results.sarif' - - # Step 11: Fail if vulnerabilities found - - name: ❌ Check for vulnerabilities - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy_table.outcome == 'failure' - run: | - echo "::error::CRITICAL or HIGH vulnerabilities found in image" - exit 1 - - # Step 11: Create a summary - - name: 📋 Create summary - if: steps.skip.outputs.skip_build != 'true' - run: | - echo "## 🎉 Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📦 Image Details" >> $GITHUB_STEP_SUMMARY - echo "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY - echo "- **Repository**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY - echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🚀 How to Use" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY - echo "docker run -d -p 8080:8080 -v caddy_data:/app/data ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: 📋 Create skip summary - if: steps.skip.outputs.skip_build == 'true' - run: | - echo "## 🚫 Docker Build Skipped" >> $GITHUB_STEP_SUMMARY - echo "Reason: renovate bot or chore(deps)/chore commit/PR title." >> $GITHUB_STEP_SUMMARY - echo "Actor: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY - echo "Event: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY - - test-image: - name: Test Docker Image - needs: build-and-push - runs-on: ubuntu-latest - if: needs.build-and-push.outputs.skip_build != 'true' - - steps: - # Ensure normalized lowercase IMAGE_NAME for this job as well - - name: 🔤 Normalize image name - run: | - raw="${{ github.repository_owner }}/${{ github.event.repository.name }}" - echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - # Step 1: Figure out which tag to test - - name: 🏷️ Determine image tag - id: tag - run: | - if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then - echo "tag=latest" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then - echo "tag=dev" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then - echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - else - echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT - fi - - # Step 1.5: Log in to GitHub Container Registry (Required for private/internal images) - - name: 🔐 Log in to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.PROJECT_TOKEN }} # yamllint disable-line rule:line-length - - # Step 2: Pull the image we just built - - name: 📥 Pull Docker image - run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - # Step 3: Start the container - - name: 🚀 Run container - run: | - docker run -d \ - --name test-container \ - -p 8080:8080 \ - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - # Step 4/5: Wait and check health with retries - - name: 🏥 Test health endpoint (retries) - run: | - set +e - for i in $(seq 1 30); do - code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000") - if [ "$code" = "200" ]; then - echo "✅ Health check passed on attempt $i" - exit 0 - fi - echo "Attempt $i/30: health not ready (code=$code); waiting..." - sleep 2 - done - echo "❌ Health check failed after retries" - docker logs test-container || true - exit 1 - - # Step 6: Check the logs for errors - - name: 📋 Check container logs - if: always() - run: docker logs test-container - - # Step 7: Clean up - - name: 🧹 Stop container - if: always() - run: docker stop test-container && docker rm test-container - - # Step 8: Summary - - name: 📋 Create test summary - if: always() - run: | - echo "## 🧪 Docker Image Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY - echo "- **Health Check**: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 5212dfdc..ed4c59ae 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,4 +1,4 @@ -name: Docker Build & Publish +name: Docker Build, Publish & Test on: push: @@ -11,6 +11,7 @@ on: branches: - main - development + workflow_dispatch: workflow_call: env: @@ -26,6 +27,10 @@ jobs: packages: write security-events: write + outputs: + skip_build: ${{ steps.skip.outputs.skip_build }} + digest: ${{ steps.build-and-push.outputs.digest }} + steps: - name: Checkout repository uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 @@ -85,6 +90,8 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} + type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }} + type=sha,format=short,enable=${{ github.event_name != 'pull_request' }} - name: Build and push Docker image if: steps.skip.outputs.skip_build != 'true' @@ -92,7 +99,6 @@ jobs: uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . - # PRs: amd64 only, no push. Pushes: amd64+arm64, push. platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} @@ -105,7 +111,6 @@ jobs: VCS_REF=${{ github.sha }} CADDY_IMAGE=${{ steps.caddy.outputs.image }} - # Trivy steps only on push - name: Run Trivy scan (table output) if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 @@ -132,3 +137,90 @@ jobs: uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4 with: sarif_file: 'trivy-results.sarif' + + - name: Create summary + if: steps.skip.outputs.skip_build != 'true' + run: | + echo "## 🎉 Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📦 Image Details" >> $GITHUB_STEP_SUMMARY + echo "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY + echo "- **Repository**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY + echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + test-image: + name: Test Docker Image + needs: build-and-push + runs-on: ubuntu-latest + if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request' + + steps: + - name: Normalize image name + run: | + raw="${{ github.repository_owner }}/${{ github.event.repository.name }}" + echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Determine image tag + id: tag + run: | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "tag=latest" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then + echo "tag=dev" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then + echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + else + echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT + fi + + - name: Log in to GitHub Container Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.PROJECT_TOKEN }} + + - name: Pull Docker image + run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} + + - name: Run container + run: | + docker run -d \ + --name test-container \ + -p 8080:8080 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} + + - name: Test health endpoint (retries) + run: | + set +e + for i in $(seq 1 30); do + code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000") + if [ "$code" = "200" ]; then + echo "✅ Health check passed on attempt $i" + exit 0 + fi + echo "Attempt $i/30: health not ready (code=$code); waiting..." + sleep 2 + done + echo "❌ Health check failed after retries" + docker logs test-container || true + exit 1 + + - name: Check container logs + if: always() + run: docker logs test-container + + - name: Stop container + if: always() + run: docker stop test-container && docker rm test-container + + - name: Create test summary + if: always() + run: | + echo "## 🧪 Docker Image Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY + echo "- **Health Check**: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY From cdc80485907a676138a8d043f9a6435fb9ed348a Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 10:22:54 -0500 Subject: [PATCH 03/98] fix: enhance Dockerfile for cross-compilation support and update Caddy build process --- Dockerfile | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 52fb68b8..2f6bda97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,9 @@ ARG VCS_REF # Using caddy:2.9.1-alpine to fix CVE-2025-59530 and stdlib vulnerabilities ARG CADDY_IMAGE=caddy:2.9.1-alpine +# ---- Cross-Compilation Helpers ---- +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx + # ---- Frontend Builder ---- # Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues FROM --platform=$BUILDPLATFORM node:24.11.1-alpine AS frontend-builder @@ -29,14 +32,20 @@ COPY frontend/ ./ RUN npm run build # ---- Backend Builder ---- -FROM golang:alpine AS backend-builder +FROM --platform=$BUILDPLATFORM golang:alpine AS backend-builder +# Copy xx helpers for cross-compilation +COPY --from=xx / / + WORKDIR /app/backend # Install build dependencies -RUN apk add --no-cache gcc musl-dev sqlite-dev +# xx-apk installs packages for the TARGET architecture +ARG TARGETPLATFORM +RUN apk add --no-cache clang lld +RUN xx-apk add --no-cache gcc musl-dev sqlite-dev -# Install Delve so we can attach during debugging -RUN go install github.com/go-delve/delve/cmd/dlv@latest +# Install Delve (cross-compile for target) +RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@latest # Copy Go module files COPY backend/go.mod backend/go.sum ./ @@ -52,7 +61,8 @@ ARG BUILD_DATE=unknown # Build the Go binary with version information injected via ldflags # -gcflags "all=-N -l" disables optimizations and inlining for better debugging -RUN CGO_ENABLED=1 GOOS=linux go build \ +# xx-go handles CGO and cross-compilation flags automatically +RUN CGO_ENABLED=1 xx-go build \ -gcflags "all=-N -l" \ -a -installsuffix cgo \ -ldflags "-X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.Version=${VERSION} \ @@ -63,10 +73,15 @@ RUN CGO_ENABLED=1 GOOS=linux go build \ # ---- Caddy Builder ---- # Build Caddy from source to ensure we use the latest Go version and dependencies # This fixes vulnerabilities found in the pre-built Caddy images (e.g. CVE-2025-59530, stdlib issues) -FROM golang:alpine AS caddy-builder +FROM --platform=$BUILDPLATFORM golang:alpine AS caddy-builder +ARG TARGETOS +ARG TARGETARCH + RUN apk add --no-cache git RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest -RUN xcaddy build v2.9.1 \ + +# Build Caddy for the target architecture +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v2.9.1 \ --replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.49.1 \ --replace golang.org/x/crypto=golang.org/x/crypto@v0.35.0 \ --output /usr/bin/caddy From 4dcab99ecf0df2a69f3c960beec52218a41a09fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:24:09 +0000 Subject: [PATCH 04/98] chore(deps): update golangci/golangci-lint-action action to v9.1.0 --- .github/workflows/quality-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 713f37bf..ec51014a 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -32,7 +32,7 @@ jobs: fail_ci_if_error: true - name: Run golangci-lint - uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # v9.0.0 + uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 with: version: latest working-directory: backend From 766075298c2742a44485ad8c6d6d3b9b3a82de4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:24:14 +0000 Subject: [PATCH 05/98] chore(deps): update tonistiigi/xx docker tag to v1.8.0 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2f6bda97..17fb147c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ ARG VCS_REF ARG CADDY_IMAGE=caddy:2.9.1-alpine # ---- Cross-Compilation Helpers ---- -FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.8.0 AS xx # ---- Frontend Builder ---- # Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues From 2ec7adab43ab92886dcd11088655dc615a8878bf Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 10:49:42 -0500 Subject: [PATCH 06/98] feat: add PasswordStrengthMeter component and integrate it into Security and Setup pages --- .../src/components/PasswordStrengthMeter.tsx | 57 +++++++++++++ frontend/src/pages/Security.tsx | 18 +++-- frontend/src/pages/Setup.tsx | 25 +++--- frontend/src/utils/passwordStrength.ts | 80 +++++++++++++++++++ 4 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/PasswordStrengthMeter.tsx create mode 100644 frontend/src/utils/passwordStrength.ts diff --git a/frontend/src/components/PasswordStrengthMeter.tsx b/frontend/src/components/PasswordStrengthMeter.tsx new file mode 100644 index 00000000..a018b03b --- /dev/null +++ b/frontend/src/components/PasswordStrengthMeter.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { calculatePasswordStrength } from '../utils/passwordStrength'; + +interface Props { + password: string; +} + +export const PasswordStrengthMeter: React.FC = ({ password }) => { + const { score, label, color, feedback } = calculatePasswordStrength(password); + + // Calculate width percentage based on score (0-4) + // 0: 5%, 1: 25%, 2: 50%, 3: 75%, 4: 100% + const width = Math.max(5, (score / 4) * 100); + + // Map color name to Tailwind classes + const getColorClass = (c: string) => { + switch (c) { + case 'red': return 'bg-red-500'; + case 'yellow': return 'bg-yellow-500'; + case 'green': return 'bg-green-500'; + default: return 'bg-gray-300'; + } + }; + + const getTextColorClass = (c: string) => { + switch (c) { + case 'red': return 'text-red-500'; + case 'yellow': return 'text-yellow-600'; + case 'green': return 'text-green-600'; + default: return 'text-gray-500'; + } + }; + + if (!password) return null; + + return ( +
+
+ + {label} + + {feedback.length > 0 && ( + + {feedback[0]} + + )} +
+ +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx index 87aae42d..00551e1f 100644 --- a/frontend/src/pages/Security.tsx +++ b/frontend/src/pages/Security.tsx @@ -7,6 +7,7 @@ import { toast } from '../utils/toast' import client from '../api/client' import { getProfile, regenerateApiKey } from '../api/user' import { Copy, RefreshCw, Shield } from 'lucide-react' +import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter' export default function Security() { const [oldPassword, setOldPassword] = useState('') @@ -80,13 +81,16 @@ export default function Security() { onChange={e => setOldPassword(e.target.value)} required /> - setNewPassword(e.target.value)} - required - /> +
+ setNewPassword(e.target.value)} + required + /> + +
{ const navigate = useNavigate(); @@ -104,17 +105,19 @@ const Setup: React.FC = () => { onChange={(e) => setFormData({ ...formData, email: e.target.value })} helperText="This email will be used for Let's Encrypt certificate notifications and recovery." /> - setFormData({ ...formData, password: e.target.value })} - /> +
+ setFormData({ ...formData, password: e.target.value })} + /> + +
{error && ( diff --git a/frontend/src/utils/passwordStrength.ts b/frontend/src/utils/passwordStrength.ts new file mode 100644 index 00000000..768f8c2a --- /dev/null +++ b/frontend/src/utils/passwordStrength.ts @@ -0,0 +1,80 @@ +export interface PasswordStrength { + score: number; // 0-4 + label: string; + color: string; // Tailwind color class prefix (e.g., 'red', 'yellow', 'green') + feedback: string[]; +} + +export function calculatePasswordStrength(password: string): PasswordStrength { + let score = 0; + const feedback: string[] = []; + + if (!password) { + return { + score: 0, + label: 'Empty', + color: 'gray', + feedback: [], + }; + } + + // Length check + if (password.length < 8) { + feedback.push('Too short (min 8 chars)'); + } else { + score += 1; + } + + if (password.length >= 12) { + score += 1; + } + + // Complexity checks + const hasLower = /[a-z]/.test(password); + const hasUpper = /[A-Z]/.test(password); + const hasNumber = /\d/.test(password); + const hasSpecial = /[^A-Za-z0-9]/.test(password); + + const varietyCount = [hasLower, hasUpper, hasNumber, hasSpecial].filter(Boolean).length; + + if (varietyCount >= 3) { + score += 1; + } + if (varietyCount === 4) { + score += 1; + } + + // Penalties + if (varietyCount < 2 && password.length >= 8) { + feedback.push('Add more variety (uppercase, numbers, symbols)'); + } + + // Cap score at 4 + score = Math.min(score, 4); + + // Determine label and color + let label = 'Very Weak'; + let color = 'red'; + + switch (score) { + case 0: + case 1: + label = 'Weak'; + color = 'red'; + break; + case 2: + label = 'Fair'; + color = 'yellow'; + break; + case 3: + label = 'Good'; + color = 'green'; + break; + case 4: + label = 'Strong'; + color = 'green'; + break; + } + + return { score, label, color, feedback }; +} From 9914e20817479e459ebab74895f7a508b598d7aa Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 10:54:03 -0500 Subject: [PATCH 07/98] feat: optimize Dockerfile build process with cache mounts for frontend and backend --- Dockerfile | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 17fb147c..6e731806 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ RUN npm ci # Copy frontend source and build COPY frontend/ ./ -RUN npm run build +RUN --mount=type=cache,target=/app/frontend/node_modules/.cache \ + npm run build # ---- Backend Builder ---- FROM --platform=$BUILDPLATFORM golang:alpine AS backend-builder @@ -49,7 +50,7 @@ RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@latest # Copy Go module files COPY backend/go.mod backend/go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod go mod download # Copy backend source COPY backend/ ./ @@ -62,7 +63,9 @@ ARG BUILD_DATE=unknown # Build the Go binary with version information injected via ldflags # -gcflags "all=-N -l" disables optimizations and inlining for better debugging # xx-go handles CGO and cross-compilation flags automatically -RUN CGO_ENABLED=1 xx-go build \ +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + CGO_ENABLED=1 xx-go build \ -gcflags "all=-N -l" \ -a -installsuffix cgo \ -ldflags "-X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.Version=${VERSION} \ @@ -78,10 +81,13 @@ ARG TARGETOS ARG TARGETARCH RUN apk add --no-cache git -RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest +RUN --mount=type=cache,target=/go/pkg/mod \ + go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest # Build Caddy for the target architecture -RUN GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v2.9.1 \ +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v2.9.1 \ --replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.49.1 \ --replace golang.org/x/crypto=golang.org/x/crypto@v0.35.0 \ --output /usr/bin/caddy From 8a0d7952a9dd29b21e5651e725f419a86900b5c0 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 11:25:58 -0500 Subject: [PATCH 08/98] feat: add profile update functionality and integrate it into the Security page --- backend/internal/api/handlers/user_handler.go | 43 +++++ backend/internal/api/routes/routes.go | 1 + frontend/src/api/user.ts | 5 + frontend/src/pages/Security.tsx | 158 ++++++++++++++++-- frontend/src/pages/SystemSettings.tsx | 19 +-- 5 files changed, 200 insertions(+), 26 deletions(-) diff --git a/backend/internal/api/handlers/user_handler.go b/backend/internal/api/handlers/user_handler.go index 1c2ccda8..d5761277 100644 --- a/backend/internal/api/handlers/user_handler.go +++ b/backend/internal/api/handlers/user_handler.go @@ -23,6 +23,7 @@ func (h *UserHandler) RegisterRoutes(r *gin.RouterGroup) { r.POST("/setup", h.Setup) r.GET("/profile", h.GetProfile) r.POST("/regenerate-api-key", h.RegenerateAPIKey) + r.PUT("/profile", h.UpdateProfile) } // GetSetupStatus checks if the application needs initial setup (i.e., no users exist). @@ -154,3 +155,45 @@ func (h *UserHandler) GetProfile(c *gin.Context) { "api_key": user.APIKey, }) } + +type UpdateProfileRequest struct { + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` +} + +// UpdateProfile updates the authenticated user's profile. +func (h *UserHandler) UpdateProfile(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var req UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Check if email is already taken by another user + var count int64 + if err := h.DB.Model(&models.User{}).Where("email = ? AND id != ?", req.Email, userID).Count(&count).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check email availability"}) + return + } + + if count > 0 { + c.JSON(http.StatusConflict, gin.H{"error": "Email already in use"}) + return + } + + if err := h.DB.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ + "name": req.Name, + "email": req.Email, + }).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index c4c8d7b2..13875218 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -79,6 +79,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { // User Profile & API Key userHandler := handlers.NewUserHandler(db) protected.GET("/user/profile", userHandler.GetProfile) + protected.POST("/user/profile", userHandler.UpdateProfile) protected.POST("/user/api-key", userHandler.RegenerateAPIKey) // Updates diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index b27e5d75..faf1b934 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -17,3 +17,8 @@ export const regenerateApiKey = async (): Promise<{ api_key: string }> => { const response = await client.post('/user/api-key') return response.data } + +export const updateProfile = async (data: { name: string; email: string }): Promise<{ message: string }> => { + const response = await client.post('/user/profile', data) + return response.data +} diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx index 00551e1f..6a85ad0d 100644 --- a/frontend/src/pages/Security.tsx +++ b/frontend/src/pages/Security.tsx @@ -1,12 +1,13 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Card } from '../components/ui/Card' import { Input } from '../components/ui/Input' import { Button } from '../components/ui/Button' import { toast } from '../utils/toast' import client from '../api/client' -import { getProfile, regenerateApiKey } from '../api/user' -import { Copy, RefreshCw, Shield } from 'lucide-react' +import { getProfile, regenerateApiKey, updateProfile } from '../api/user' +import { getSettings, updateSetting } from '../api/settings' +import { Copy, RefreshCw, Shield, Mail, User } from 'lucide-react' import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter' export default function Security() { @@ -15,6 +16,14 @@ export default function Security() { const [confirmPassword, setConfirmPassword] = useState('') const [loading, setLoading] = useState(false) + // Profile State + const [name, setName] = useState('') + const [email, setEmail] = useState('') + + // Certificate Email State + const [certEmail, setCertEmail] = useState('') + const [useUserEmail, setUseUserEmail] = useState(true) + const queryClient = useQueryClient() const { data: profile, isLoading: isLoadingProfile } = useQuery({ @@ -22,6 +31,44 @@ export default function Security() { queryFn: getProfile, }) + const { data: settings } = useQuery({ + queryKey: ['settings'], + queryFn: getSettings, + }) + + // Initialize profile state + useEffect(() => { + if (profile) { + setName(profile.name) + setEmail(profile.email) + } + }, [profile]) + + // Initialize cert email state + useEffect(() => { + if (settings && profile) { + const savedEmail = settings['caddy.email'] + if (savedEmail && savedEmail !== profile.email) { + setCertEmail(savedEmail) + setUseUserEmail(false) + } else { + setCertEmail(profile.email) + setUseUserEmail(true) + } + } + }, [settings, profile]) + + const updateProfileMutation = useMutation({ + mutationFn: updateProfile, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['profile'] }) + toast.success('Profile updated successfully') + }, + onError: (error: any) => { + toast.error(`Failed to update profile: ${error.message}`) + }, + }) + const regenerateMutation = useMutation({ mutationFn: regenerateApiKey, onSuccess: () => { @@ -33,6 +80,21 @@ export default function Security() { }, }) + const saveCertEmailMutation = useMutation({ + mutationFn: async () => { + const emailToSave = useUserEmail ? profile?.email : certEmail + if (!emailToSave) return + await updateSetting('caddy.email', emailToSave, 'caddy', 'string') + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['settings'] }) + toast.success('Certificate email updated') + }, + onError: (error: any) => { + toast.error(`Failed to update certificate email: ${error.message}`) + }, + }) + const handleChangePassword = async (e: React.FormEvent) => { e.preventDefault() if (newPassword !== confirmPassword) { @@ -70,6 +132,33 @@ export default function Security() {
+ {/* Profile Settings */} + +
+ +

Profile Settings

+
+
+ setName(e.target.value)} + /> + setEmail(e.target.value)} + /> + +
+
+ {/* Change Password */}

Change Password

@@ -91,19 +180,68 @@ export default function Security() { />
- setConfirmPassword(e.target.value)} - required - /> +
+ setConfirmPassword(e.target.value)} + required + className={confirmPassword && newPassword !== confirmPassword ? 'border-red-500 focus:ring-red-500' : ''} + /> + {confirmPassword && newPassword !== confirmPassword && ( +

Passwords do not match

+ )} +
+ {/* Certificate Email Configuration */} + +
+ +

Certificate Email

+
+

+ This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications. +

+ +
+
+ setUseUserEmail(e.target.checked)} + className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + +
+ + {!useUserEmail && ( + setCertEmail(e.target.value)} + placeholder="certs@example.com" + /> + )} + + +
+
+ {/* API Key */}

API Key

diff --git a/frontend/src/pages/SystemSettings.tsx b/frontend/src/pages/SystemSettings.tsx index 7b267fce..ed977b02 100644 --- a/frontend/src/pages/SystemSettings.tsx +++ b/frontend/src/pages/SystemSettings.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Card } from '../components/ui/Card' import { Button } from '../components/ui/Button' @@ -25,7 +25,6 @@ interface UpdateInfo { export default function SystemSettings() { const queryClient = useQueryClient() - const [caddyEmail, setCaddyEmail] = useState('') const [caddyAdminAPI, setCaddyAdminAPI] = useState('http://localhost:2019') // Fetch Settings @@ -35,12 +34,11 @@ export default function SystemSettings() { }) // Update local state when settings load - useState(() => { + useEffect(() => { if (settings) { - if (settings['caddy.email']) setCaddyEmail(settings['caddy.email']) if (settings['caddy.admin_api']) setCaddyAdminAPI(settings['caddy.admin_api']) } - }) + }, [settings]) // Fetch Health/System Status const { data: health, isLoading: isLoadingHealth } = useQuery({ @@ -67,7 +65,6 @@ export default function SystemSettings() { const saveSettingsMutation = useMutation({ mutationFn: async () => { - await updateSetting('caddy.email', caddyEmail, 'caddy', 'string') await updateSetting('caddy.admin_api', caddyAdminAPI, 'caddy', 'string') }, onSuccess: () => { @@ -90,16 +87,6 @@ export default function SystemSettings() {

General Configuration

- setCaddyEmail(e.target.value)} - placeholder="admin@example.com" - /> -

- Email address for Let's Encrypt certificate notifications -

Date: Fri, 21 Nov 2025 11:46:09 -0500 Subject: [PATCH 09/98] feat: enhance email validation in Setup and Security pages, add sidebar collapse functionality in Layout --- frontend/src/components/Layout.tsx | 72 ++++++++++---- frontend/src/pages/Security.tsx | 131 ++++++++++++++++---------- frontend/src/pages/SettingsLayout.tsx | 2 +- frontend/src/pages/Setup.tsx | 39 +++++--- frontend/src/utils/validation.ts | 4 + 5 files changed, 170 insertions(+), 78 deletions(-) create mode 100644 frontend/src/utils/validation.ts diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 67af7d6d..aeb5f41a 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from 'react' +import { ReactNode, useState, useEffect } from 'react' import { Link, useLocation } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { ThemeToggle } from './ThemeToggle' @@ -7,6 +7,7 @@ import { useAuth } from '../hooks/useAuth' import { checkHealth } from '../api/health' import NotificationCenter from './NotificationCenter' import SystemStatus from './SystemStatus' +import { ChevronLeft, ChevronRight } from 'lucide-react' interface LayoutProps { children: ReactNode @@ -14,9 +15,17 @@ interface LayoutProps { export default function Layout({ children }: LayoutProps) { const location = useLocation() - const [sidebarOpen, setSidebarOpen] = useState(false) + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) + const [isCollapsed, setIsCollapsed] = useState(() => { + const saved = localStorage.getItem('sidebarCollapsed') + return saved ? JSON.parse(saved) : false + }) const { logout } = useAuth() + useEffect(() => { + localStorage.setItem('sidebarCollapsed', JSON.stringify(isCollapsed)) + }, [isCollapsed]) + const { data: health } = useQuery({ queryKey: ['health'], queryFn: checkHealth, @@ -40,20 +49,21 @@ export default function Layout({ children }: LayoutProps) {
-
{/* Sidebar */} {/* Overlay for mobile */} - {sidebarOpen && ( + {/* Mobile Overlay */} + {mobileSidebarOpen && (
setSidebarOpen(false)} + className="fixed inset-0 bg-gray-900/50 z-20 lg:hidden" + onClick={() => setMobileSidebarOpen(false)} /> )} diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx index 6a85ad0d..8447ebca 100644 --- a/frontend/src/pages/Security.tsx +++ b/frontend/src/pages/Security.tsx @@ -9,6 +9,7 @@ import { getProfile, regenerateApiKey, updateProfile } from '../api/user' import { getSettings, updateSetting } from '../api/settings' import { Copy, RefreshCw, Shield, Mail, User } from 'lucide-react' import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter' +import { isValidEmail } from '../utils/validation' export default function Security() { const [oldPassword, setOldPassword] = useState('') @@ -19,9 +20,11 @@ export default function Security() { // Profile State const [name, setName] = useState('') const [email, setEmail] = useState('') + const [emailValid, setEmailValid] = useState(null) // Certificate Email State const [certEmail, setCertEmail] = useState('') + const [certEmailValid, setCertEmailValid] = useState(null) const [useUserEmail, setUseUserEmail] = useState(true) const queryClient = useQueryClient() @@ -44,6 +47,15 @@ export default function Security() { } }, [profile]) + // Validate profile email + useEffect(() => { + if (email) { + setEmailValid(isValidEmail(email)) + } else { + setEmailValid(null) + } + }, [email]) + // Initialize cert email state useEffect(() => { if (settings && profile) { @@ -58,6 +70,15 @@ export default function Security() { } }, [settings, profile]) + // Validate cert email + useEffect(() => { + if (certEmail && !useUserEmail) { + setCertEmailValid(isValidEmail(certEmail)) + } else { + setCertEmailValid(null) + } + }, [certEmail, useUserEmail]) + const updateProfileMutation = useMutation({ mutationFn: updateProfile, onSuccess: () => { @@ -144,12 +165,18 @@ export default function Security() { value={name} onChange={(e) => setName(e.target.value)} /> - setEmail(e.target.value)} - /> +
+ setEmail(e.target.value)} + className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''} + /> + {emailValid === false && ( +

Please enter a valid email address

+ )} +
+ {/* Certificate Email Configuration */} + +
+ +

Certificate Email

+
+

+ This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications. +

+ +
+
+ setUseUserEmail(e.target.checked)} + className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + +
+ + {!useUserEmail && ( +
+ setCertEmail(e.target.value)} + placeholder="certs@example.com" + className={certEmailValid === false ? 'border-red-500 focus:ring-red-500' : certEmailValid === true ? 'border-green-500 focus:ring-green-500' : ''} + /> + {certEmailValid === false && ( +

Please enter a valid email address

+ )} +
+ )} + + +
+
+ {/* Change Password */}

Change Password

@@ -199,49 +275,6 @@ export default function Security() {
- {/* Certificate Email Configuration */} - -
- -

Certificate Email

-
-

- This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications. -

- -
-
- setUseUserEmail(e.target.checked)} - className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" - /> - -
- - {!useUserEmail && ( - setCertEmail(e.target.value)} - placeholder="certs@example.com" - /> - )} - - -
-
- {/* API Key */}

API Key

diff --git a/frontend/src/pages/SettingsLayout.tsx b/frontend/src/pages/SettingsLayout.tsx index 929b3948..5d78eb94 100644 --- a/frontend/src/pages/SettingsLayout.tsx +++ b/frontend/src/pages/SettingsLayout.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' export default function SettingsLayout() { const location = useLocation() - const [tasksOpen, setTasksOpen] = useState(true) + const [tasksOpen, setTasksOpen] = useState(false) const isActive = (path: string) => location.pathname === path diff --git a/frontend/src/pages/Setup.tsx b/frontend/src/pages/Setup.tsx index 36542a9c..f5d51cc8 100644 --- a/frontend/src/pages/Setup.tsx +++ b/frontend/src/pages/Setup.tsx @@ -7,6 +7,7 @@ import { useAuth } from '../hooks/useAuth'; import { Input } from '../components/ui/Input'; import { Button } from '../components/ui/Button'; import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter'; +import { isValidEmail } from '../utils/validation'; const Setup: React.FC = () => { const navigate = useNavigate(); @@ -18,6 +19,7 @@ const Setup: React.FC = () => { password: '', }); const [error, setError] = useState(null); + const [emailValid, setEmailValid] = useState(null); const { data: status, isLoading: statusLoading } = useQuery({ queryKey: ['setupStatus'], @@ -25,6 +27,14 @@ const Setup: React.FC = () => { retry: false, }); + useEffect(() => { + if (formData.email) { + setEmailValid(isValidEmail(formData.email)); + } else { + setEmailValid(null); + } + }, [formData.email]); + useEffect(() => { if (isAuthenticated) { navigate('/'); @@ -94,18 +104,23 @@ const Setup: React.FC = () => { value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} /> - setFormData({ ...formData, email: e.target.value })} - helperText="This email will be used for Let's Encrypt certificate notifications and recovery." - /> -
+
+ setFormData({ ...formData, email: e.target.value })} + className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''} + /> + {emailValid === false && ( +

Please enter a valid email address

+ )} +
+
{ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) +} From b7aff5a944276b6adb4da9d5377601210c588a4b Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 11:50:48 -0500 Subject: [PATCH 10/98] feat: refactor release workflow to enhance frontend and backend builds, add Caddy build step, and streamline artifact handling --- .github/workflows/release.yml | 124 ++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d83c3f2..5f9f12cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,38 +10,120 @@ permissions: packages: write jobs: - create-release: + build-frontend: + name: Build Frontend runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - fetch-depth: 0 + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json - - name: Generate changelog - id: changelog + - name: Install Dependencies + working-directory: frontend + run: npm ci + + - name: Build + working-directory: frontend + run: npm run build + + - name: Archive Frontend + working-directory: frontend + run: tar -czf ../frontend-dist.tar.gz dist/ + + - uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: frontend-dist.tar.gz + + build-backend: + name: Build Backend + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Build + working-directory: backend + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 1 run: | - # Get previous tag - PREV_TAG=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null || echo "") - - if [ -z "$PREV_TAG" ]; then - echo "First release - generating full changelog" - CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) - else - echo "Generating changelog since $PREV_TAG" - CHANGELOG=$(git log $PREV_TAG..HEAD --pretty=format:"- %s (%h)" --no-merges) + # Install dependencies for CGO (sqlite) + if [ "${{ matrix.goarch }}" = "arm64" ]; then + sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu + export CC=aarch64-linux-gnu-gcc fi - # Save to file for GitHub release - echo "$CHANGELOG" > CHANGELOG.txt - echo "Generated changelog with $(echo "$CHANGELOG" | wc -l) commits" + go build -ldflags "-s -w -X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.Version=${{ github.ref_name }}" -o ../cpmp-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/api + + - uses: actions/upload-artifact@v4 + with: + name: backend-${{ matrix.goos }}-${{ matrix.goarch }} + path: cpmp-${{ matrix.goos }}-${{ matrix.goarch }} + + build-caddy: + name: Build Caddy + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Install xcaddy + run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + + - name: Build Caddy + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + xcaddy build v2.9.1 \ + --replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.49.1 \ + --replace golang.org/x/crypto=golang.org/x/crypto@v0.35.0 \ + --output caddy-${{ matrix.goos }}-${{ matrix.goarch }} + + - uses: actions/upload-artifact@v4 + with: + name: caddy-${{ matrix.goos }}-${{ matrix.goarch }} + path: caddy-${{ matrix.goos }}-${{ matrix.goarch }} + + create-release: + name: Create Release + needs: [build-frontend, build-backend, build-caddy] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display structure of downloaded files + run: ls -R artifacts - name: Create GitHub Release - uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2 + uses: softprops/action-gh-release@v2 with: - body_path: CHANGELOG.txt + files: | + artifacts/frontend-dist/frontend-dist.tar.gz + artifacts/backend-linux-amd64/cpmp-linux-amd64 + artifacts/backend-linux-arm64/cpmp-linux-arm64 + artifacts/caddy-linux-amd64/caddy-linux-amd64 + artifacts/caddy-linux-arm64/caddy-linux-arm64 generate_release_notes: true - draft: false prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} env: GITHUB_TOKEN: ${{ secrets.PROJECT_TOKEN }} From 5db59291f4375bd57859fba3b29a5e9508153c09 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 11:58:25 -0500 Subject: [PATCH 11/98] feat: improve setup page navigation logic to handle loading state and redirect based on authentication --- frontend/src/pages/Setup.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Setup.tsx b/frontend/src/pages/Setup.tsx index f5d51cc8..b7d7dcb4 100644 --- a/frontend/src/pages/Setup.tsx +++ b/frontend/src/pages/Setup.tsx @@ -36,14 +36,21 @@ const Setup: React.FC = () => { }, [formData.email]); useEffect(() => { - if (isAuthenticated) { - navigate('/'); + // Wait for setup status to load + if (statusLoading) return; + + // If setup is required, stay on this page (ignore stale auth) + if (status?.setupRequired) { return; } - if (status && !status.setupRequired) { + + // If setup is NOT required, redirect based on auth + if (isAuthenticated) { + navigate('/'); + } else { navigate('/login'); } - }, [status, isAuthenticated, navigate]); + }, [status, statusLoading, isAuthenticated, navigate]); const mutation = useMutation({ mutationFn: async (data: SetupRequest) => { From c8822f61efafc0365ea3bb9cb0c45d5c846437b6 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Nov 2025 12:15:18 -0500 Subject: [PATCH 12/98] feat: enhance sidebar collapse functionality and improve layout header structure --- frontend/src/components/Layout.tsx | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index aeb5f41a..7117bd7f 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -62,9 +62,15 @@ export default function Layout({ children }: LayoutProps) { ${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'} ${isCollapsed ? 'w-20' : 'w-64'} `}> -