From dda19ce679314ac2ba4bfce1d4960f802c28e832 Mon Sep 17 00:00:00 2001 From: beastoin Date: Wed, 1 Apr 2026 11:36:03 +0000 Subject: [PATCH 1/4] fix(admin): remove server-side adminData check, add error handling to verifyAdmin The original omi-admin routes only verified the Firebase token server-side. The adminData collection check was client-side only (auth-provider.tsx). Adding the server-side Firestore check caused 500s when the Admin SDK couldn't initialize. Revert to original behavior: token-only server auth. Co-Authored-By: Claude Opus 4.6 --- web/admin/lib/auth.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/web/admin/lib/auth.ts b/web/admin/lib/auth.ts index d0add823df2..4f76f520f88 100644 --- a/web/admin/lib/auth.ts +++ b/web/admin/lib/auth.ts @@ -1,10 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; -import { verifyFirebaseToken, getDb } from '@/lib/firebase/admin'; +import { verifyFirebaseToken } from '@/lib/firebase/admin'; /** * Verify that the request comes from an authenticated admin user. - * Checks: (1) valid Firebase ID token in Authorization header, - * (2) user's UID exists in the adminData collection. + * Checks valid Firebase ID token in Authorization header. + * The adminData collection check is done client-side by auth-provider. * Returns the decoded token on success, or a NextResponse error on failure. */ export async function verifyAdmin(request: NextRequest): Promise< @@ -16,16 +16,14 @@ export async function verifyAdmin(request: NextRequest): Promise< } const token = authorization.split('Bearer ')[1]; - const decodedToken = await verifyFirebaseToken(token); - if (!decodedToken) { - return NextResponse.json({ error: 'Unauthorized: Invalid token' }, { status: 401 }); + try { + const decodedToken = await verifyFirebaseToken(token); + if (!decodedToken) { + return NextResponse.json({ error: 'Unauthorized: Invalid token' }, { status: 401 }); + } + return { uid: decodedToken.uid }; + } catch (error) { + console.error('Error verifying admin token:', error); + return NextResponse.json({ error: 'Internal server error during auth' }, { status: 500 }); } - - const db = getDb(); - const adminDoc = await db.collection('adminData').doc(decodedToken.uid).get(); - if (!adminDoc.exists) { - return NextResponse.json({ error: 'Forbidden: Not an admin' }, { status: 403 }); - } - - return { uid: decodedToken.uid }; } From eb09fe976839c23c22f3054ffc37fc725593ada6 Mon Sep 17 00:00:00 2001 From: beastoin Date: Wed, 1 Apr 2026 12:15:37 +0000 Subject: [PATCH 2/4] feat(admin): switch to GCP Secret Manager for deploy secrets - NEXT_PUBLIC_ vars: fetched from Secret Manager at build time via gcloud secrets versions access, passed as Docker build-args - Server-side vars: injected at Cloud Run runtime via --set-secrets, no longer baked into Docker image (more secure) - All secrets use WEB_ADMIN_ prefix in Secret Manager Co-Authored-By: Claude Opus 4.6 --- .github/workflows/gcp_admin.yml | 77 ++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/.github/workflows/gcp_admin.yml b/.github/workflows/gcp_admin.yml index 0f187bdb5c5..1f38378a7eb 100644 --- a/.github/workflows/gcp_admin.yml +++ b/.github/workflows/gcp_admin.yml @@ -10,6 +10,7 @@ on: env: SERVICE: omi-admin-dashboard REGION: us-central1 + GCP_PROJECT: based-hardware jobs: deploy: @@ -31,6 +32,22 @@ jobs: credentials_json: ${{ secrets.GCP_CREDENTIALS }} - run: gcloud auth configure-docker + - name: Fetch build-time secrets from GCP Secret Manager + id: secrets + run: | + echo "Fetching NEXT_PUBLIC_ build-time secrets from GCP Secret Manager..." + { + echo "NEXT_PUBLIC_FIREBASE_API_KEY=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_FIREBASE_API_KEY --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_FIREBASE_PROJECT_ID=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_FIREBASE_PROJECT_ID --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_FIREBASE_APP_ID=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_FIREBASE_APP_ID --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_FIREBASE_VAPID_KEY=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_FIREBASE_VAPID_KEY --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_PLUGINS_APP_ID=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_PLUGINS_APP_ID --project=${{ env.GCP_PROJECT }})" + echo "NEXT_PUBLIC_OMI_API_URL=$(gcloud secrets versions access latest --secret=WEB_ADMIN_NEXT_PUBLIC_OMI_API_URL --project=${{ env.GCP_PROJECT }})" + } >> "$GITHUB_OUTPUT" + - name: Build and Push Docker image uses: docker/build-push-action@v2 with: @@ -39,36 +56,15 @@ jobs: platforms: linux/amd64 tags: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }} build-args: | - NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} - NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} - NEXT_PUBLIC_FIREBASE_PROJECT_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} - NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} - NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} - NEXT_PUBLIC_FIREBASE_APP_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} - NEXT_PUBLIC_FIREBASE_VAPID_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_VAPID_KEY }} - NEXT_PUBLIC_PLUGINS_APP_ID=${{ secrets.NEXT_PUBLIC_PLUGINS_APP_ID }} - OMI_API_SECRET_KEY=${{ secrets.OMI_API_SECRET_KEY }} - NEXT_PUBLIC_OMI_API_URL=${{ secrets.NEXT_PUBLIC_OMI_API_URL }} - FIREBASE_PROJECT_ID=${{ secrets.FIREBASE_PROJECT_ID }} - FIREBASE_CLIENT_EMAIL=${{ secrets.FIREBASE_CLIENT_EMAIL }} - FIREBASE_PRIVATE_KEY=${{ secrets.FIREBASE_PRIVATE_KEY }} - TYPESENSE_HOST=${{ secrets.TYPESENSE_HOST }} - TYPESENSE_API_KEY=${{ secrets.TYPESENSE_API_KEY }} - STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }} - STRIPE_UNLIMITED_MONTHLY_PRICE_ID=${{ secrets.STRIPE_UNLIMITED_MONTHLY_PRICE_ID }} - STRIPE_UNLIMITED_ANNUAL_PRICE_ID=${{ secrets.STRIPE_UNLIMITED_ANNUAL_PRICE_ID }} - REDIS_HOST=${{ secrets.REDIS_HOST }} - REDIS_PORT=${{ secrets.REDIS_PORT }} - REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} - POSTGRES_URL=${{ secrets.POSTGRES_URL }} - SHIPBOB_API_KEY=${{ secrets.SHIPBOB_API_KEY }} - SHOPIFY_STORE=${{ secrets.SHOPIFY_STORE }} - SHOPIFY_ACCESS_TOKEN=${{ secrets.SHOPIFY_ACCESS_TOKEN }} - MIXPANEL_SECRET=${{ secrets.MIXPANEL_SECRET }} - OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} - POSTHOG_PERSONAL_API_KEY=${{ secrets.POSTHOG_PERSONAL_API_KEY }} - POSTHOG_PROJECT_ID=${{ secrets.POSTHOG_PROJECT_ID }} - POSTHOG_HOST=${{ secrets.POSTHOG_HOST }} + NEXT_PUBLIC_FIREBASE_API_KEY=${{ steps.secrets.outputs.NEXT_PUBLIC_FIREBASE_API_KEY }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${{ steps.secrets.outputs.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID=${{ steps.secrets.outputs.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${{ steps.secrets.outputs.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${{ steps.secrets.outputs.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} + NEXT_PUBLIC_FIREBASE_APP_ID=${{ steps.secrets.outputs.NEXT_PUBLIC_FIREBASE_APP_ID }} + NEXT_PUBLIC_FIREBASE_VAPID_KEY=${{ steps.secrets.outputs.NEXT_PUBLIC_FIREBASE_VAPID_KEY }} + NEXT_PUBLIC_PLUGINS_APP_ID=${{ steps.secrets.outputs.NEXT_PUBLIC_PLUGINS_APP_ID }} + NEXT_PUBLIC_OMI_API_URL=${{ steps.secrets.outputs.NEXT_PUBLIC_OMI_API_URL }} - name: Deploy to Cloud Run id: deploy @@ -77,6 +73,27 @@ jobs: service: ${{ env.SERVICE }} region: ${{ env.REGION }} image: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }} + secrets: | + FIREBASE_PROJECT_ID=WEB_ADMIN_FIREBASE_PROJECT_ID:latest + FIREBASE_CLIENT_EMAIL=WEB_ADMIN_FIREBASE_CLIENT_EMAIL:latest + FIREBASE_PRIVATE_KEY=WEB_ADMIN_FIREBASE_PRIVATE_KEY:latest + OMI_API_SECRET_KEY=WEB_ADMIN_OMI_API_SECRET_KEY:latest + TYPESENSE_HOST=WEB_ADMIN_TYPESENSE_HOST:latest + TYPESENSE_API_KEY=WEB_ADMIN_TYPESENSE_API_KEY:latest + STRIPE_SECRET_KEY=WEB_ADMIN_STRIPE_SECRET_KEY:latest + STRIPE_UNLIMITED_MONTHLY_PRICE_ID=WEB_ADMIN_STRIPE_UNLIMITED_MONTHLY_PRICE_ID:latest + STRIPE_UNLIMITED_ANNUAL_PRICE_ID=WEB_ADMIN_STRIPE_UNLIMITED_ANNUAL_PRICE_ID:latest + REDIS_HOST=WEB_ADMIN_REDIS_HOST:latest + REDIS_PORT=WEB_ADMIN_REDIS_PORT:latest + REDIS_PASSWORD=WEB_ADMIN_REDIS_PASSWORD:latest + SHIPBOB_API_KEY=WEB_ADMIN_SHIPBOB_API_KEY:latest + SHOPIFY_STORE=WEB_ADMIN_SHOPIFY_STORE:latest + SHOPIFY_ACCESS_TOKEN=WEB_ADMIN_SHOPIFY_ACCESS_TOKEN:latest + MIXPANEL_SECRET=WEB_ADMIN_MIXPANEL_SECRET:latest + OPENAI_API_KEY=WEB_ADMIN_OPENAI_API_KEY:latest + POSTHOG_PERSONAL_API_KEY=WEB_ADMIN_POSTHOG_PERSONAL_API_KEY:latest + POSTHOG_PROJECT_ID=WEB_ADMIN_POSTHOG_PROJECT_ID:latest + POSTHOG_HOST=WEB_ADMIN_POSTHOG_HOST:latest - name: Show Output run: echo ${{ steps.deploy.outputs.url }} From 3e2dc3196756942a91a36fde07bc3fe266585223 Mon Sep 17 00:00:00 2001 From: beastoin Date: Wed, 1 Apr 2026 12:15:42 +0000 Subject: [PATCH 3/4] feat(admin): remove server-side secrets from Dockerfile Server-side env vars are now injected at runtime via Cloud Run Secret Manager instead of being baked into the Docker image. Only NEXT_PUBLIC_ build-time vars remain as ARGs. Co-Authored-By: Claude Opus 4.6 --- web/admin/Dockerfile | 70 ++++---------------------------------------- 1 file changed, 6 insertions(+), 64 deletions(-) diff --git a/web/admin/Dockerfile b/web/admin/Dockerfile index 16f3858007f..695690ed5d0 100644 --- a/web/admin/Dockerfile +++ b/web/admin/Dockerfile @@ -18,7 +18,7 @@ RUN \ FROM node:18-alpine AS builder WORKDIR /app -# Add ARG declarations for variables passed via --build-arg +# Build-time ARGs for NEXT_PUBLIC_ vars (baked into JS bundle by Next.js) ARG NEXT_PUBLIC_FIREBASE_API_KEY ARG NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ARG NEXT_PUBLIC_FIREBASE_PROJECT_ID @@ -26,27 +26,10 @@ ARG NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET ARG NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID ARG NEXT_PUBLIC_FIREBASE_APP_ID ARG NEXT_PUBLIC_FIREBASE_VAPID_KEY -ARG OMI_API_SECRET_KEY +ARG NEXT_PUBLIC_PLUGINS_APP_ID ARG NEXT_PUBLIC_OMI_API_URL -ARG FIREBASE_PROJECT_ID -ARG FIREBASE_CLIENT_EMAIL -ARG FIREBASE_PRIVATE_KEY -ARG TYPESENSE_HOST -ARG TYPESENSE_API_KEY -ARG STRIPE_SECRET_KEY -ARG STRIPE_UNLIMITED_MONTHLY_PRICE_ID -ARG STRIPE_UNLIMITED_ANNUAL_PRICE_ID -ARG REDIS_HOST -ARG REDIS_PORT -ARG REDIS_PASSWORD -ARG SHIPBOB_API_KEY -ARG SHOPIFY_STORE -ARG SHOPIFY_ACCESS_TOKEN -ARG MIXPANEL_SECRET -ARG OPENAI_API_KEY -ARG POSTHOG_PERSONAL_API_KEY -ARG POSTHOG_PROJECT_ID -ARG POSTHOG_HOST +# Server-side secrets are injected at runtime via Cloud Run Secret Manager +# — no need to pass them as build-args COPY --from=deps /app/node_modules ./node_modules COPY . . @@ -67,50 +50,9 @@ RUN \ FROM node:18-alpine AS runner WORKDIR /app -# Re-declare ARG for runtime ENV setting (only needed for non-NEXT_PUBLIC_ vars) -ARG OMI_API_SECRET_KEY -ARG FIREBASE_PROJECT_ID -ARG FIREBASE_CLIENT_EMAIL -ARG FIREBASE_PRIVATE_KEY -ARG TYPESENSE_HOST -ARG TYPESENSE_API_KEY -ARG STRIPE_SECRET_KEY -ARG STRIPE_UNLIMITED_MONTHLY_PRICE_ID -ARG STRIPE_UNLIMITED_ANNUAL_PRICE_ID -ARG REDIS_HOST -ARG REDIS_PORT -ARG REDIS_PASSWORD -ARG SHIPBOB_API_KEY -ARG SHOPIFY_STORE -ARG SHOPIFY_ACCESS_TOKEN -ARG MIXPANEL_SECRET -ARG OPENAI_API_KEY -ARG POSTHOG_PERSONAL_API_KEY -ARG POSTHOG_PROJECT_ID -ARG POSTHOG_HOST - ENV NODE_ENV production -# Set the non-NEXT_PUBLIC_ variable as a runtime ENV -ENV FIREBASE_PROJECT_ID=$FIREBASE_PROJECT_ID -ENV FIREBASE_CLIENT_EMAIL=$FIREBASE_CLIENT_EMAIL -ENV FIREBASE_PRIVATE_KEY=$FIREBASE_PRIVATE_KEY -ENV OMI_API_SECRET_KEY=$OMI_API_SECRET_KEY -ENV TYPESENSE_HOST=$TYPESENSE_HOST -ENV TYPESENSE_API_KEY=$TYPESENSE_API_KEY -ENV STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY -ENV STRIPE_UNLIMITED_MONTHLY_PRICE_ID=$STRIPE_UNLIMITED_MONTHLY_PRICE_ID -ENV STRIPE_UNLIMITED_ANNUAL_PRICE_ID=$STRIPE_UNLIMITED_ANNUAL_PRICE_ID -ENV REDIS_HOST=$REDIS_HOST -ENV REDIS_PORT=$REDIS_PORT -ENV REDIS_PASSWORD=$REDIS_PASSWORD -ENV SHIPBOB_API_KEY=$SHIPBOB_API_KEY -ENV SHOPIFY_STORE=$SHOPIFY_STORE -ENV SHOPIFY_ACCESS_TOKEN=$SHOPIFY_ACCESS_TOKEN -ENV MIXPANEL_SECRET=$MIXPANEL_SECRET -ENV OPENAI_API_KEY=$OPENAI_API_KEY -ENV POSTHOG_PERSONAL_API_KEY=$POSTHOG_PERSONAL_API_KEY -ENV POSTHOG_PROJECT_ID=$POSTHOG_PROJECT_ID -ENV POSTHOG_HOST=$POSTHOG_HOST +# Server-side secrets are injected at runtime via Cloud Run Secret Manager +# — they are NOT baked into the Docker image (more secure) # Uncomment the following line in case you want to disable telemetry during runtime. # ENV NEXT_TELEMETRY_DISABLED 1 From d50df421b763b6a02717e91f13ed3506e22c7fc4 Mon Sep 17 00:00:00 2001 From: beastoin Date: Wed, 1 Apr 2026 12:18:43 +0000 Subject: [PATCH 4/4] fix(admin): restore server-side adminData check with proper error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adminData Firestore check is good security — it was only causing 500s due to missing env vars. Now wrapped in try-catch so it returns proper error responses instead of unhandled throws. Co-Authored-By: Claude Opus 4.6 --- web/admin/lib/auth.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/web/admin/lib/auth.ts b/web/admin/lib/auth.ts index 4f76f520f88..e51bc15846e 100644 --- a/web/admin/lib/auth.ts +++ b/web/admin/lib/auth.ts @@ -1,10 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; -import { verifyFirebaseToken } from '@/lib/firebase/admin'; +import { verifyFirebaseToken, getDb } from '@/lib/firebase/admin'; /** * Verify that the request comes from an authenticated admin user. - * Checks valid Firebase ID token in Authorization header. - * The adminData collection check is done client-side by auth-provider. + * Checks: (1) valid Firebase ID token in Authorization header, + * (2) user's UID exists in the adminData collection. * Returns the decoded token on success, or a NextResponse error on failure. */ export async function verifyAdmin(request: NextRequest): Promise< @@ -21,9 +21,16 @@ export async function verifyAdmin(request: NextRequest): Promise< if (!decodedToken) { return NextResponse.json({ error: 'Unauthorized: Invalid token' }, { status: 401 }); } + + const db = getDb(); + const adminDoc = await db.collection('adminData').doc(decodedToken.uid).get(); + if (!adminDoc.exists) { + return NextResponse.json({ error: 'Forbidden: Not an admin' }, { status: 403 }); + } + return { uid: decodedToken.uid }; } catch (error) { - console.error('Error verifying admin token:', error); + console.error('Error verifying admin:', error); return NextResponse.json({ error: 'Internal server error during auth' }, { status: 500 }); } }