diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 73a27dd..dc64792 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -75,19 +75,26 @@ jobs: destination: "/" storageZoneName: ${{ secrets.BUNNY_STORAGE_ZONE_NAME }} storagePassword: ${{ secrets.BUNNY_STORAGE_PASSWORD }} - accessKey: ${{ secrets.BUNNY_API_KEY }} + accessKey: ${{ secrets.BUNNYNET_API_KEY }} pullZoneId: ${{ secrets.BUNNY_PULL_ZONE_ID }} upload: "true" remove: "true" purgePullZone: "true" - # Admin Docker build/push/roll. Skipped for md/pen-only changes via - # paths-filter — note the leading '**' positive pattern: paths-filter - # treats negation-only filters as never matching. + # Admin Docker build/push to ghcr.io + Magic Container roll. Follows + # bunny.net's canonical pattern (see + # https://docs.bunny.net/magic-containers/deploy-with-github-actions.md): + # ghcr.io for image hosting (no extra credentials — GITHUB_TOKEN does it), + # BunnyWay/actions/container-update-image to roll the container. + # Gated on vars.APP_ID so the job is skipped before the admin Magic + # Container app exists. build-admin: needs: verify - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.APP_ID != '' runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - uses: actions/checkout@v4 @@ -108,13 +115,13 @@ jobs: if: steps.changes.outputs.app == 'true' uses: docker/setup-buildx-action@v3 - - name: Log in to bunny.net container registry + - name: Log in to GitHub Container Registry if: steps.changes.outputs.app == 'true' uses: docker/login-action@v3 with: - registry: ${{ secrets.BUNNY_REGISTRY }} - username: ${{ secrets.BUNNY_REGISTRY_USERNAME }} - password: ${{ secrets.BUNNY_REGISTRY_PASSWORD }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push admin image if: steps.changes.outputs.app == 'true' @@ -123,14 +130,15 @@ jobs: context: . file: apps/admin/Dockerfile push: true - tags: ${{ secrets.BUNNY_REGISTRY }}/${{ vars.ADMIN_APP_ID }}:${{ github.sha }},${{ secrets.BUNNY_REGISTRY }}/${{ vars.ADMIN_APP_ID }}:latest + tags: | + ghcr.io/${{ github.repository_owner }}/wardrobe-assistants-admin:${{ github.sha }} + ghcr.io/${{ github.repository_owner }}/wardrobe-assistants-admin:latest - - name: Roll Magic Container deployment + - name: Roll admin Magic Container to new image if: steps.changes.outputs.app == 'true' - env: - BUNNY_API_KEY: ${{ secrets.BUNNY_API_KEY }} - ADMIN_APP_ID: ${{ vars.ADMIN_APP_ID }} - run: | - curl -fsS -X POST \ - -H "AccessKey: $BUNNY_API_KEY" \ - "https://api.bunny.net/magic-containers/apps/$ADMIN_APP_ID/deploy" + uses: BunnyWay/actions/container-update-image@main + with: + app_id: ${{ vars.APP_ID }} + api_key: ${{ secrets.BUNNYNET_API_KEY }} + container: wardrobe-assistants-admin + image_tag: ${{ github.sha }} diff --git a/apps/admin/Dockerfile b/apps/admin/Dockerfile index a031276..c99f153 100644 --- a/apps/admin/Dockerfile +++ b/apps/admin/Dockerfile @@ -16,6 +16,14 @@ WORKDIR /repo COPY --from=deps /repo/node_modules ./node_modules COPY --from=deps /repo/apps/admin/node_modules ./apps/admin/node_modules COPY . . +# Placeholder env for Next's build-time page-data collection. Runtime +# values come from the container env, not from this image — these never +# leak into the runner stage because ENV doesn't cross FROM boundaries. +ENV DATABASE_URL=file:./build.db \ + BETTER_AUTH_SECRET=build-placeholder-secret-not-used-at-runtime-32b \ + BETTER_AUTH_URL=https://admin.wardrobe-assistants.ch \ + RESEND_API_KEY=build-placeholder \ + EMAIL_FROM=admin@wardrobe-assistants.ch RUN npm -w apps/admin run build FROM node:22-alpine AS runner diff --git a/apps/admin/scripts/seed-admin.ts b/apps/admin/scripts/seed-admin.ts index 3dc5ff8..353fc1f 100644 --- a/apps/admin/scripts/seed-admin.ts +++ b/apps/admin/scripts/seed-admin.ts @@ -35,8 +35,11 @@ if (!(await isSeedNeeded(db, email))) { } // Sign up via the auth API so password hashing matches Better Auth's runtime. +// Empty Headers satisfies Better Auth's server-API origin-check middleware +// when called outside an HTTP request context (e.g. from this CLI). const signUp = await auth.api.signUpEmail({ body: { email, password, name: email }, + headers: new Headers(), }); if (!signUp || !("user" in signUp)) { console.error("Failed to create admin user."); @@ -49,10 +52,22 @@ await db .set({ emailVerified: true, updatedAt: new Date() }) .where(eq(schema.user.id, signUp.user.id)); +// Sign in to obtain a session so the next call (enableTwoFactor) is +// authenticated. Better Auth's API requires an active session for +// account-mutating actions; enableTwoFactor cannot be called as the unauthed +// CLI would. +const signInRes = await auth.api.signInEmail({ + body: { email, password }, + asResponse: true, +}); +const setCookie = signInRes.headers.get("set-cookie"); +const sessionHeaders = new Headers(); +if (setCookie) sessionHeaders.set("cookie", setCookie); + // Generate TOTP secret + backup codes by enabling 2FA via the API. const enable = await auth.api.enableTwoFactor({ body: { password, issuer: "Wardrobe Assistants Admin" }, - headers: new Headers(), + headers: sessionHeaders, }); if (!enable || !("totpURI" in enable)) {