Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 47 additions & 30 deletions .github/workflows/gcp_admin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
env:
SERVICE: omi-admin-dashboard
REGION: us-central1
GCP_PROJECT: based-hardware

jobs:
deploy:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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 }}
70 changes: 6 additions & 64 deletions web/admin/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,18 @@ 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
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 . .
Expand All @@ -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
Expand Down
25 changes: 15 additions & 10 deletions web/admin/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ 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 });
}

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 });
}
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 };
return { uid: decodedToken.uid };
Comment on lines +19 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Server-side admin authorization removed — any Firebase user can call admin APIs

The adminData Firestore check has been moved entirely to the client-side auth-provider.tsx. After this change, verifyAdmin only confirms the bearer token belongs to any valid Firebase user, not specifically an admin.

Because the Next.js API routes (e.g. GET /api/omi/apps, POST /api/omi/apps/[app_id]/review) are publicly reachable HTTP endpoints, an attacker only needs a valid Firebase ID token — obtainable simply by signing into any Omi account — to call them directly, bypassing the client-side guard entirely.

The previous two-step check (valid token + document in adminData collection) is the correct pattern for protecting server-side routes. The client-side check in auth-provider.tsx is useful for UI gating, but it cannot substitute for server-side authorization.

Consider restoring the Firestore check inside verifyAdmin, independently of the try-catch fix (which is correct and valuable on its own):

try {
  const decodedToken = await verifyFirebaseToken(token);
  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);
  return NextResponse.json({ error: 'Internal server error during auth' }, { status: 500 });
}

Once the three Firebase secrets are set in the prod environment, getDb() will work correctly and this check can be safely restored.

} catch (error) {
console.error('Error verifying admin:', error);
return NextResponse.json({ error: 'Internal server error during auth' }, { status: 500 });
}
}
Loading