Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d892b3f64f | |||
| e7ac495831 | |||
| c9b1c49f53 | |||
| cbc8958b55 | |||
| be226d3430 | |||
| b433c79677 | |||
| a266203c07 | |||
| 3f4145e800 | |||
| 553bfed070 | |||
| 452eef7606 | |||
| c20538ef9f | |||
| 66d8cdf9b2 | |||
| e41ce98b74 |
@@ -0,0 +1,34 @@
|
||||
# --- Self-hosted Postgres (Docker) ---
|
||||
POSTGRES_USER=routecommerce
|
||||
POSTGRES_PASSWORD=routecommerce_dev_password
|
||||
POSTGRES_DB=route_commerce
|
||||
# Used by the app and push-migrations.js to talk to the local DB
|
||||
DATABASE_URL=postgresql://routecommerce:routecommerce_dev_password@127.0.0.1:5432/route_commerce?schema=public
|
||||
|
||||
# --- PostgREST (REST API) ---
|
||||
# Server actions call `${NEXT_PUBLIC_API_URL}/rest/v1/rpc/...`
|
||||
# Point that URL at the local PostgREST container. Anon key can be anything
|
||||
# non-empty since we handle auth at the app layer (Better Auth + dev_session).
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
NEXT_PUBLIC_API_ANON_KEY=local-postgrest-anon-key
|
||||
POSTGREST_SERVICE_KEY=local-postgrest-service-key
|
||||
|
||||
# --- MinIO (S3-compatible object storage — replaces Supabase Storage) ---
|
||||
MINIO_ROOT_USER=routecommerce
|
||||
MINIO_ROOT_PASSWORD=miniochangeme123
|
||||
STORAGE_ENDPOINT=http://localhost:9000
|
||||
STORAGE_REGION=us-east-1
|
||||
STORAGE_ACCESS_KEY=routecommerce
|
||||
STORAGE_SECRET_KEY=miniochangeme123
|
||||
STORAGE_BUCKET_PREFIX=
|
||||
# Public base URL for image rendering in <img src=...>
|
||||
NEXT_PUBLIC_STORAGE_BASE_URL=http://localhost:9000
|
||||
|
||||
# --- Better Auth ---
|
||||
BETTER_AUTH_SECRET=REPLACE_ME_WITH_OPENSSL_RAND_HEX_32
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# --- App secrets ---
|
||||
MINIMAX_API_KEY=
|
||||
MINIMAX_BASE_URL=
|
||||
+134
-10
@@ -17,37 +17,161 @@ jobs:
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Start Docker stack
|
||||
env:
|
||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
||||
MINIO_ROOT_USER: ${{ secrets.MINIO_ROOT_USER }}
|
||||
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
||||
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
||||
run: |
|
||||
APP_DIR=/home/tyler/route-commerce
|
||||
mkdir -p $APP_DIR
|
||||
cd $APP_DIR
|
||||
[ -f .env ] || cp .env.example .env
|
||||
# Append production secrets to .env (overriding .env.example defaults)
|
||||
{
|
||||
echo "POSTGRES_USER=${POSTGRES_USER}"
|
||||
echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"
|
||||
echo "POSTGRES_DB=${POSTGRES_DB}"
|
||||
echo "MINIO_ROOT_USER=${MINIO_ROOT_USER}"
|
||||
echo "MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}"
|
||||
echo "POSTGREST_JWT_SECRET=${POSTGREST_JWT_SECRET}"
|
||||
} >> .env
|
||||
docker compose up -d db postgrest minio minio_init
|
||||
# Wait for Postgres healthcheck
|
||||
for i in $(seq 1 30); do
|
||||
if docker compose exec -T db pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" > /dev/null 2>&1; then
|
||||
echo "Postgres is ready"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Apply migrations
|
||||
env:
|
||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
||||
run: |
|
||||
cd /home/tyler/route-commerce
|
||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/migrations/000_preflight_supabase_compat.sql || true
|
||||
[ -f supabase/captured_schema.sql ] && PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/captured_schema.sql || echo "captured_schema.sql not present, skipping"
|
||||
for f in supabase/migrations/[0-9]*.sql; do
|
||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -q -f "$f" || echo "FAIL: $f"
|
||||
done
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Load env
|
||||
run: cp /home/tyler/route-commerce/.env.production .env.production
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
||||
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
|
||||
NEXT_PUBLIC_BETTER_AUTH_URL: ${{ secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
||||
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
||||
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
||||
STORAGE_ACCESS_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
|
||||
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
||||
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
||||
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
||||
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
||||
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
||||
run: npm run build
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
||||
MINIO_ROOT_USER: ${{ secrets.MINIO_ROOT_USER }}
|
||||
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
||||
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
||||
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
||||
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
|
||||
NEXT_PUBLIC_BETTER_AUTH_URL: ${{ secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
||||
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
||||
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
||||
STORAGE_ACCESS_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
|
||||
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
||||
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
||||
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
||||
PGRST_SERVER_PORT: ${{ secrets.PGRST_SERVER_PORT }}
|
||||
PGRST_DB_URI: ${{ secrets.PGRST_DB_URI }}
|
||||
PGRST_DB_ANON_ROLE: ${{ secrets.PGRST_DB_ANON_ROLE }}
|
||||
PGRST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
||||
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
||||
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
||||
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
||||
run: |
|
||||
APP_DIR=/home/tyler/route-commerce
|
||||
mkdir -p $APP_DIR
|
||||
|
||||
# Write env file from secrets (preserves existing .env for docker compose)
|
||||
{
|
||||
printf "DATABASE_URL=%s\n" "$DATABASE_URL"
|
||||
printf "POSTGRES_USER=%s\n" "$POSTGRES_USER"
|
||||
printf "POSTGRES_PASSWORD=%s\n" "$POSTGRES_PASSWORD"
|
||||
printf "POSTGRES_DB=%s\n" "$POSTGRES_DB"
|
||||
printf "MINIO_ROOT_USER=%s\n" "$MINIO_ROOT_USER"
|
||||
printf "MINIO_ROOT_PASSWORD=%s\n" "$MINIO_ROOT_PASSWORD"
|
||||
printf "POSTGREST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
||||
printf "BETTER_AUTH_SECRET=%s\n" "$BETTER_AUTH_SECRET"
|
||||
printf "BETTER_AUTH_URL=%s\n" "$BETTER_AUTH_URL"
|
||||
printf "NEXT_PUBLIC_BETTER_AUTH_URL=%s\n" "$NEXT_PUBLIC_BETTER_AUTH_URL"
|
||||
printf "STORAGE_ENDPOINT=%s\n" "$STORAGE_ENDPOINT"
|
||||
printf "STORAGE_REGION=%s\n" "$STORAGE_REGION"
|
||||
printf "STORAGE_ACCESS_KEY=%s\n" "$STORAGE_ACCESS_KEY"
|
||||
printf "STORAGE_SECRET_KEY=%s\n" "$STORAGE_SECRET_KEY"
|
||||
printf "STORAGE_BUCKET_PREFIX=%s\n" "$STORAGE_BUCKET_PREFIX"
|
||||
printf "NEXT_PUBLIC_STORAGE_BASE_URL=%s\n" "$NEXT_PUBLIC_STORAGE_BASE_URL"
|
||||
printf "PGRST_SERVER_PORT=%s\n" "$PGRST_SERVER_PORT"
|
||||
printf "PGRST_DB_URI=%s\n" "$PGRST_DB_URI"
|
||||
printf "PGRST_DB_ANON_ROLE=%s\n" "$PGRST_DB_ANON_ROLE"
|
||||
printf "PGRST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
||||
printf "NEXT_PUBLIC_SUPABASE_URL=%s\n" "$NEXT_PUBLIC_SUPABASE_URL"
|
||||
printf "NEXT_PUBLIC_SUPABASE_ANON_KEY=%s\n" "$NEXT_PUBLIC_SUPABASE_ANON_KEY"
|
||||
printf "STRIPE_SECRET_KEY=%s\n" "$STRIPE_SECRET_KEY"
|
||||
printf "STRIPE_WEBHOOK_SECRET=%s\n" "$STRIPE_WEBHOOK_SECRET"
|
||||
printf "STRIPE_PUBLISHABLE_KEY=%s\n" "$STRIPE_PUBLISHABLE_KEY"
|
||||
printf "RESEND_API_KEY=%s\n" "$RESEND_API_KEY"
|
||||
printf "RESEND_WEBHOOK_SECRET=%s\n" "$RESEND_WEBHOOK_SECRET"
|
||||
printf "MINIMAX_API_KEY=%s\n" "$MINIMAX_API_KEY"
|
||||
printf "MINIMAX_BASE_URL=%s\n" "$MINIMAX_BASE_URL"
|
||||
printf "FROM_EMAIL=%s\n" "$FROM_EMAIL"
|
||||
} > $APP_DIR/.env.production
|
||||
|
||||
# Copy build output and required files
|
||||
rsync -a --delete .next/ $APP_DIR/.next/
|
||||
rsync -a --delete public/ $APP_DIR/public/
|
||||
cp package.json package-lock.json $APP_DIR/
|
||||
cp package.json $APP_DIR/
|
||||
cp docker-compose.yml $APP_DIR/
|
||||
cp -r supabase/ $APP_DIR/
|
||||
cp next.config.ts $APP_DIR/ 2>/dev/null || cp next.config.js $APP_DIR/ 2>/dev/null || true
|
||||
|
||||
# Install production deps only
|
||||
cd $APP_DIR
|
||||
npm install --omit=dev
|
||||
|
||||
# Copy env if not already there
|
||||
if [ ! -f $APP_DIR/.env.production ]; then
|
||||
echo "WARNING: No .env.production found at $APP_DIR — app may not start correctly"
|
||||
fi
|
||||
|
||||
# Start or restart PM2 process
|
||||
if pm2 describe route-commerce > /dev/null 2>&1; then
|
||||
pm2 restart route-commerce
|
||||
|
||||
+17
-1
@@ -41,4 +41,20 @@ supabase/.temp/
|
||||
|
||||
# IDE / local config
|
||||
.mcp.json
|
||||
.env*
|
||||
|
||||
# Docker / self-hosted Postgres
|
||||
db_data/
|
||||
|
||||
# Captured Supabase data (re-pull from Supabase as needed)
|
||||
supabase/captured/captured_data.sql.gz
|
||||
supabase/captured/captured_schema.sql
|
||||
|
||||
# Local data and binaries
|
||||
.data/
|
||||
bin/postgrest
|
||||
bin/minio
|
||||
bin/mc
|
||||
bin/postgrest-proxy.js
|
||||
bin/postgrest.tar.xz
|
||||
# Captured Supabase dump (regenerate from Supabase with the guide in docs/SUPABASE_DUMP_GUIDE.md)
|
||||
supabase/captured/
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# Self-Hosted Storage + Schema Plan
|
||||
|
||||
## Context
|
||||
|
||||
The user is migrating Route Commerce from Supabase to a self-hosted stack:
|
||||
- **Already done in this branch (`feat/better-auth`)**: Better Auth (auth), Docker Postgres, PostgREST, env var rewiring, ~10 source files updated to drop Supabase JS client.
|
||||
- **What's broken right now**: The website is missing images (Supabase Storage URLs 404 because Supabase is going away), and the local Postgres can't apply the 137 migrations because the **base schema is missing** (the initial tables — `brands`, `orders`, `products`, `stops`, `admin_users` — were created in Supabase directly and never exported as a migration).
|
||||
- **User constraint**: "no band-aids" — proper self-hosted replacement, not runtime patches.
|
||||
- **User constraint**: "no data migration" — start with fresh empty DB; users re-upload assets.
|
||||
|
||||
## Approach
|
||||
|
||||
Three independent workstreams, executed in order:
|
||||
|
||||
1. **Capture the base schema** from the still-live Supabase project using `pg_dump --schema-only` (recommended over `supabase db pull`, which has a broken migration history). Apply the captured schema + existing 137 migrations to the local self-hosted Postgres.
|
||||
2. **Stand up MinIO** in Docker as the S3-compatible object store. Wire it to the app via the AWS SDK. MinIO's URL structure is clean: `http://minio:9000/<bucket>/<key>`, publicly readable per-bucket via bucket policy.
|
||||
3. **Replace every Supabase Storage URL** in the codebase with MinIO URLs (one env var: `NEXT_PUBLIC_STORAGE_BASE_URL`).
|
||||
|
||||
Storage buckets in use (from explore agent):
|
||||
| Bucket | Purpose | Current state |
|
||||
|---|---|---|
|
||||
| `brand-logos` | Brand logo images | Hardcoded in 8+ files via env-var URL |
|
||||
| `product-images` | Product photos | `80aa01da-ab4b-44f8-b6e7-700552457e18` (Supabase bucket UUID) |
|
||||
| `contacts-imports` | CSV contact imports | `a1b2c3d4-…` (Supabase bucket UUID) |
|
||||
| `videos` | Tuxedo video hero | Hardcoded Supabase URL in `TuxedoVideoHero.tsx` |
|
||||
| `water-photos` (dynamic) | Water log photos | API route, bucket name in form field |
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase A — Capture base schema and apply to local Postgres
|
||||
|
||||
**Why `pg_dump`, not `supabase db pull`**: The remote Supabase project's `supabase_migrations.schema_migrations` history is out of sync with the local files (that's the long error list the user got). `supabase db pull` requires that history to be in sync before it'll write a new initial migration. `pg_dump` reads the live catalog directly and ignores the migration history entirely.
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Capture the schema** from the remote Supabase DB (run on the user's Mac where direct PG works — MEMORY.md confirms direct PG is blocked from this dev box, but their Mac can do it):
|
||||
```bash
|
||||
pg_dump "postgresql://postgres:<service-role-pw>@db.wnzkhezyhnfzhkhiflrp.supabase.co:5432/postgres" \
|
||||
--schema-only --no-owner --no-privileges \
|
||||
--schema=public \
|
||||
-f supabase/captured_schema.sql
|
||||
```
|
||||
*Exclude `auth` and `storage` schemas* — we stub `auth` ourselves and use MinIO instead of Supabase Storage. Note: 185 SECURITY DEFINER functions reference `auth.uid()`; these will compile against the preflight stub but return NULL at runtime. Phase D addresses this.
|
||||
|
||||
2. **Delete files that don't belong** in the local apply order:
|
||||
- `BUNDLE_018_042.sql` — concatenated duplicate
|
||||
- `XXX_blog_tables.sql`, `XXX_launch_checklist.sql`, `XXX_roadmap_tables.sql`, `XXX_waitlist_waitlist.sql` — drafts (XXX convention)
|
||||
- `099_contact_imports_bucket.sql` (the bucket-creation one — keep the RPCs though)
|
||||
- Rename to disambiguate any number collisions (no current collision, but defensive)
|
||||
|
||||
3. **Patch the 4 broken migrations** identified by the explore agent (read the file first, then `search_replace`):
|
||||
- `006_water_log_rpcs_fixed.sql`: replace `STATIC` with `STABLE` (6 occurrences)
|
||||
- `087_brand_logos_bucket.sql`: convert `CREATE POLICY IF NOT EXISTS …` to `DROP POLICY IF EXISTS …; CREATE POLICY …;` (4 policies)
|
||||
- `099_harvest_reach_segmentation.sql`: quote the `time` column references (matches the 148 patch)
|
||||
- `135_email_automation_rpcs.sql`: reorder `enroll_abandoned_cart` params so defaults come last, or add a default to `p_next_email_at`
|
||||
|
||||
4. **Update `000_preflight_supabase_compat.sql`** (already in the branch):
|
||||
- Add `CREATE EXTENSION IF NOT EXISTS pgcrypto;` at the top
|
||||
- Remove the `storage.buckets` / `storage.objects` stub (MinIO replaces it; 087/145 storage policies will be deleted in step 2)
|
||||
|
||||
5. **Apply to local Postgres** (running on this dev box, port 5432):
|
||||
```bash
|
||||
PGPASSWORD='lay7yqM3fHNvwpfjb4tvz7M3kimopyrSHQF24vsuGUM' \
|
||||
psql -h 127.0.0.1 -U routecommerce -d route_commerce -v ON_ERROR_STOP=1 -f supabase/migrations/000_preflight_supabase_compat.sql
|
||||
|
||||
PGPASSWORD='lay7yqM3fHNvwpfjb4tvz7M3kimopyrSHQF24vsuGUM' \
|
||||
psql -h 127.0.0.1 -U routecommerce -d route_commerce -v ON_ERROR_STOP=1 -f supabase/captured_schema.sql
|
||||
|
||||
PGPASSWORD='lay7yqM3fHNvwpfjb4tvz7M3kimopyrSHQF24vsuGUM' \
|
||||
for f in supabase/migrations/[0-9]*.sql; do
|
||||
psql -h 127.0.0.1 -U routecommerce -d route_commerce -v ON_ERROR_STOP=1 -q -f "$f" || break
|
||||
done
|
||||
```
|
||||
Expected: most migrations apply, some fail (drop those, log in plan). This is the test that the local Postgres is actually usable.
|
||||
|
||||
### Phase B — MinIO for object storage
|
||||
|
||||
**Add to `docker-compose.yml`** (new `minio` service + `minio_init` one-shot to create buckets via `mc`):
|
||||
|
||||
```yaml
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: route_commerce_minio
|
||||
restart: unless-stopped
|
||||
env_file: [.env]
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
ports: ["127.0.0.1:9000:9000", "127.0.0.1:9001:9001"]
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio_init:
|
||||
image: minio/mc:latest
|
||||
depends_on:
|
||||
minio: { condition: service_healthy }
|
||||
env_file: [.env]
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}
|
||||
for b in brand-logos product-images contacts-imports videos water-photos; do
|
||||
mc mb --ignore-existing local/$${b}
|
||||
mc anonymous set download local/$${b}
|
||||
done
|
||||
profiles: ["init"]
|
||||
|
||||
volumes:
|
||||
minio_data: { driver: local }
|
||||
```
|
||||
|
||||
**Add to `.env.example`**:
|
||||
```
|
||||
MINIO_ROOT_USER=routecommerce
|
||||
MINIO_ROOT_PASSWORD=change-me-minio-root
|
||||
NEXT_PUBLIC_STORAGE_BASE_URL=http://localhost:9000
|
||||
STORAGE_ENDPOINT=http://localhost:9000
|
||||
STORAGE_REGION=us-east-1
|
||||
STORAGE_ACCESS_KEY=routecommerce
|
||||
STORAGE_SECRET_KEY=change-me-minio-root
|
||||
STORAGE_BUCKET_PREFIX=
|
||||
```
|
||||
|
||||
**Install AWS SDK** (MinIO is S3-compatible):
|
||||
```bash
|
||||
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
||||
```
|
||||
|
||||
### Phase C — Replace Supabase Storage calls with MinIO
|
||||
|
||||
**New server-side helper** `src/lib/storage.ts`:
|
||||
- `s3Client` configured from env (uses `@aws-sdk/client-s3`)
|
||||
- `uploadFile({ bucket, key, body, contentType })` — replaces `fetch(PUT .../storage/v1/object/{bucket}/{key})` patterns
|
||||
- `deleteFile({ bucket, key })`
|
||||
- `publicUrl(bucket, key)` — returns `${STORAGE_BASE_URL}/${bucket}/${key}`
|
||||
- Bucket name constants exported as a single source of truth
|
||||
|
||||
**Files to modify** (from explore agent):
|
||||
- `src/actions/brand-settings.ts` — 8 `fetch` PUTs to `/storage/v1/object/brand-logos/...`
|
||||
- `src/actions/products/upload-image.ts` — 3 calls to `product-images` bucket
|
||||
- `src/actions/communications/import-contacts.ts` — `contacts-imports` bucket (PUT + LIST + RPC)
|
||||
- `src/app/api/water-photo-upload/route.ts` — dynamic bucket
|
||||
- `src/components/storefront/TuxedoVideoHero.tsx` — hardcoded `https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/...` for videos and brand-logos
|
||||
- `src/components/time-tracking/TimeTrackingFieldClient.tsx` — same hardcoded URL
|
||||
- `src/lib/email-service.ts` — 4 occurrences of `wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/...`
|
||||
- `src/app/tuxedo/about/page.tsx` — same hardcoded URL
|
||||
|
||||
For the **hardcoded Supabase URLs**: replace with `publicUrl(bucket, key)` or `${process.env.NEXT_PUBLIC_STORAGE_BASE_URL}/${bucket}/${key}`.
|
||||
|
||||
**Remove**:
|
||||
- The `MockStorageBuilder` in `src/lib/supabase.ts:193-223` (no longer needed)
|
||||
- `SUPABASE_PAT` env var from `.env.example` (only used by water-photo-upload)
|
||||
|
||||
### Phase D — Auth wiring (closes the loop)
|
||||
|
||||
The 185 `auth.uid()` references in the captured schema will return NULL without intervention. Two options:
|
||||
|
||||
- **Option 1 (recommended, matches existing pattern)**: Disable RLS on all public tables, rely on the SECURITY DEFINER RPCs (which the codebase already does, per CLAUDE.md). One-time SQL after `captured_schema.sql` applies: `DO $$ DECLARE r record; BEGIN FOR r IN SELECT tablename FROM pg_tables WHERE schemaname='public' LOOP EXECUTE 'ALTER TABLE public.' || quote_ident(r.tablename) || ' DISABLE ROW LEVEL SECURITY'; END LOOP; END $$;`
|
||||
- **Option 2**: Wire PostgREST to set `request.jwt.claim.sub` from the Better Auth session cookie. More complex; deferred unless RLS proves necessary.
|
||||
|
||||
**Recommend Option 1** for now — it's consistent with the existing "brand scoping in server actions" pattern, and the SECURITY DEFINER functions still work because they execute with the function owner's privileges (no RLS blocking).
|
||||
|
||||
### Phase E — Verification
|
||||
|
||||
End-to-end test sequence (no more "trust me, it works"):
|
||||
|
||||
1. **DB schema applies cleanly**:
|
||||
```bash
|
||||
docker compose up -d db minio
|
||||
PGPASSWORD=$POSTGRES_PASSWORD psql -h 127.0.0.1 -U routecommerce -d route_commerce -f 000_preflight.sql
|
||||
PGPASSWORD=$POSTGRES_PASSWORD psql -h 127.0.0.1 -U routecommerce -d route_commerce -f captured_schema.sql
|
||||
for f in supabase/migrations/[0-9]*.sql; do psql ... -f "$f" || echo "FAIL: $f"; done
|
||||
```
|
||||
Goal: all migrations apply (with the 4 patches from Phase A) OR the remaining failures are documented and skipped.
|
||||
|
||||
2. **PostgREST serves the schema**:
|
||||
```bash
|
||||
curl http://127.0.0.1:3001/brands?limit=1 -H "apikey: local-anon"
|
||||
curl http://127.0.0.1:3001/rpc/get_public_stops_for_brand -X POST -H "Content-Type: application/json" -d '{"p_slug":"indian-river-direct"}'
|
||||
```
|
||||
|
||||
3. **MinIO buckets are public**:
|
||||
```bash
|
||||
mc alias set local http://127.0.0.1:9000 $USER $PASS
|
||||
mc ls local/
|
||||
curl -I http://127.0.0.1:9000/brand-logos/test.png # should return 200 or 404, never 403
|
||||
```
|
||||
|
||||
4. **App build passes**:
|
||||
```bash
|
||||
DATABASE_URL=... NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:3001 \
|
||||
NEXT_PUBLIC_STORAGE_BASE_URL=http://127.0.0.1:9000 \
|
||||
BETTER_AUTH_SECRET=... npm run build
|
||||
```
|
||||
Goal: `✓ Compiled successfully` with no Supabase URL parse errors, no `auth.uid()` undefined, no missing bucket errors.
|
||||
|
||||
5. **Image display in the browser**:
|
||||
- Start the dev server: `npm run dev`
|
||||
- Sign in via Better Auth (mock or real)
|
||||
- Upload a product image via admin UI → verify it lands in MinIO (`mc ls local/product-images/`)
|
||||
- Visit `/indian-river-direct/products/[slug]` → verify the image renders with the MinIO URL
|
||||
|
||||
6. **Auth round-trip**:
|
||||
- `POST /api/auth/sign-up/email` with test email/password → expect 200, user created in `better_auth_user` (or whatever Better Auth's table is — check `200_better_auth_tables.sql`)
|
||||
- `POST /api/auth/sign-in/email` → expect session cookie
|
||||
- Hit `/admin` with the cookie → expect 200, not redirect to `/login`
|
||||
|
||||
## Files to modify (summary)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `docker-compose.yml` | Add `minio` + `minio_init` services, `minio_data` volume |
|
||||
| `.env.example` | Add MinIO + storage env vars, remove `SUPABASE_PAT` |
|
||||
| `package.json` | Add `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` |
|
||||
| `supabase/migrations/000_preflight_supabase_compat.sql` | Add `pgcrypto` extension, remove storage stub |
|
||||
| `supabase/migrations/006_water_log_rpcs_fixed.sql` | `STATIC` → `STABLE` |
|
||||
| `supabase/migrations/087_brand_logos_bucket.sql` | `CREATE POLICY IF NOT EXISTS` → `DROP + CREATE` |
|
||||
| `supabase/migrations/099_harvest_reach_segmentation.sql` | Quote `time` column refs |
|
||||
| `supabase/migrations/135_email_automation_rpcs.sql` | Reorder `enroll_abandoned_cart` params |
|
||||
| `supabase/captured_schema.sql` | **NEW** — output of `pg_dump` from remote |
|
||||
| `src/lib/storage.ts` | **NEW** — S3 client, upload/delete/publicUrl helpers, bucket constants |
|
||||
| `src/actions/brand-settings.ts` | Replace 8 Supabase fetch PUTs with `uploadFile` |
|
||||
| `src/actions/products/upload-image.ts` | Replace 3 calls with `uploadFile` |
|
||||
| `src/actions/communications/import-contacts.ts` | Replace PUT + LIST with `uploadFile` / S3 ListObjectsV2 |
|
||||
| `src/app/api/water-photo-upload/route.ts` | Replace Supabase call with `uploadFile` |
|
||||
| `src/components/storefront/TuxedoVideoHero.tsx` | Replace hardcoded Supabase URL with `publicUrl()` |
|
||||
| `src/components/time-tracking/TimeTrackingFieldClient.tsx` | Same |
|
||||
| `src/lib/email-service.ts` | Same (4 occurrences) |
|
||||
| `src/app/tuxedo/about/page.tsx` | Same |
|
||||
| `src/lib/supabase.ts` | Remove `MockStorageBuilder` (lines 193-223) |
|
||||
|
||||
## Reuse from existing code
|
||||
|
||||
- `src/lib/supabase.ts` — the `supabase` JS client still exports query builders used by `src/lib/svc-headers.ts` and many server actions for non-auth RPCs. Keep it for now; it's PostgREST-compatible because both speak the PostgREST protocol.
|
||||
- `src/lib/auth.ts` — Better Auth config, already wired in this branch. No changes needed.
|
||||
- `src/lib/admin-permissions.ts` — already swapped to use Better Auth + direct `pg`. No changes needed.
|
||||
- `src/lib/svc-headers.ts` — used to build Supabase REST headers. Stays as-is (the anon/service-role strings still work against PostgREST).
|
||||
|
||||
## Risks
|
||||
|
||||
- **Migration apply order**: even after patches, some migrations may reference tables that haven't been created yet due to deletion order. The captured `pg_dump` puts everything in dependency order, so applying it first should resolve most cross-references.
|
||||
- **The 4 broken migrations may be load-bearing**: 087 (brand-logos RLS) is already removed in the cleanup step. 006 (water log RPCs) is critical for the `/admin/water-log` pages. 099 is critical for communications. 135 is critical for the email automation. If the patches don't work, the affected features will be broken — but the rest of the app should still build and run.
|
||||
- **Existing user-uploaded images in Supabase will be unreachable**: the user said "no data migration", so this is expected. Admins will re-upload logos/product images. The Tuxedo video and Olathe logos (referenced in `email-service.ts` and `tuxedo/about/page.tsx`) are brand assets the user will need to copy over manually.
|
||||
- **PostgREST connection pooling**: PostgREST opens ~10 connections to Postgres. The 137 migrations + `pg_dump` schema may reference `auth.uid()` inside SECURITY DEFINER functions, which will fail to plan if the `auth` schema is missing. The preflight stubs the function but the `pg_dump` will also try to create the same function (with the real Supabase body). If `pg_dump` includes a non-stub `auth.uid()` that conflicts with the preflight, apply `pg_dump` first, then the preflight.
|
||||
Binary file not shown.
@@ -0,0 +1,83 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: route_commerce_db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
postgrest:
|
||||
image: postgrest/postgrest:v12.2.3
|
||||
container_name: route_commerce_postgrest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGRST_DB_URI: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
PGRST_DB_SCHEMA: public
|
||||
PGRST_DB_ANON_ROLE: ${POSTGRES_USER}
|
||||
PGRST_SERVER_PORT: 3001
|
||||
PGRST_SERVER_HOST: 0.0.0.0
|
||||
PGRST_OPENAPI_MODE: disabled
|
||||
PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
|
||||
ports:
|
||||
- "127.0.0.1:3001:3001"
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: route_commerce_minio
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
ports:
|
||||
- "127.0.0.1:9000:9000"
|
||||
- "127.0.0.1:9001:9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio_init:
|
||||
image: minio/mc:latest
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}
|
||||
for b in brand-logos product-images contacts-imports videos water-photos; do
|
||||
mc mb --ignore-existing local/$${b}
|
||||
mc anonymous set download local/$${b}
|
||||
done
|
||||
profiles: ["init"]
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
@@ -0,0 +1,312 @@
|
||||
# Supabase Dump & Restore Guide
|
||||
|
||||
This guide is the runbook for capturing the real Supabase schema and data
|
||||
and restoring it to a local Postgres database. It documents the exact
|
||||
steps that worked when the spend cap was removed on 2026-06-05.
|
||||
|
||||
## Connection (this works from the dev box)
|
||||
|
||||
The Supabase project `wnzkhezyhnfzhkhiflrp` (route-commerce) is hosted in
|
||||
**East US (North Virginia)**. The dev box has no IPv6, so we must use
|
||||
the **Supavisor pooler** (IPv4 only) — and it's on **`aws-1`**, not `aws-0`.
|
||||
|
||||
```bash
|
||||
# Working pooler URL (from supabase/.temp/pooler-url)
|
||||
postgresql://postgres.wnzkhezyhnfzhkhiflrp:YLKzP9jz2yqop7jr@aws-1-us-east-1.pooler.supabase.com:5432/postgres
|
||||
```
|
||||
|
||||
The `aws-0-us-east-1.pooler.supabase.com` hostname resolves over IPv4
|
||||
but Supavisor returns "tenant/user not found" because the project lives
|
||||
on `aws-1`. The correct region number is non-obvious — always check
|
||||
`supabase/.temp/pooler-url` for the actual endpoint.
|
||||
|
||||
The direct hostname `db.wnzkhezyhnfzhkhiflrp.supabase.co` only resolves
|
||||
over IPv6, which is unreachable from this dev box.
|
||||
|
||||
## pg_dump version
|
||||
|
||||
Supabase runs **PostgreSQL 17.6**. Local pg must be ≥ 17 to dump cleanly.
|
||||
The dev box had pg 16 by default — install pg 17 client:
|
||||
|
||||
```bash
|
||||
echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt/ noble-pgdg main" \
|
||||
| sudo tee /etc/apt/sources.list.d/pgdg.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-unauthenticated postgresql-client-17
|
||||
# Use /usr/lib/postgresql/17/bin/pg_dump explicitly (or update PATH)
|
||||
```
|
||||
|
||||
## Capture Schema
|
||||
|
||||
```bash
|
||||
export PGPASSWORD="YLKzP9jz2yqop7jr"
|
||||
export PATH="/usr/lib/postgresql/17/bin:$PATH"
|
||||
|
||||
mkdir -p supabase/captured
|
||||
pg_dump \
|
||||
--host=aws-1-us-east-1.pooler.supabase.com \
|
||||
--port=5432 \
|
||||
--username=postgres.wnzkhezyhnfzhkhiflrp \
|
||||
--dbname=postgres \
|
||||
--schema-only \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
--no-acl \
|
||||
--exclude-schema=auth \
|
||||
--exclude-schema=storage \
|
||||
--exclude-schema=realtime \
|
||||
--exclude-schema=supabase_functions \
|
||||
--exclude-schema=graphql \
|
||||
--exclude-schema=graphql_public \
|
||||
--exclude-schema=pgsodium \
|
||||
--exclude-schema=pgsodium_masks \
|
||||
--exclude-schema=extensions \
|
||||
--exclude-schema=pgbouncer \
|
||||
--exclude-schema=supabase_migrations \
|
||||
--exclude-schema=net \
|
||||
--exclude-schema=vault \
|
||||
--file=supabase/captured/captured_schema.sql
|
||||
```
|
||||
|
||||
Captured schema is ~540KB, 65 tables, 252 functions.
|
||||
|
||||
## Capture Data
|
||||
|
||||
```bash
|
||||
pg_dump \
|
||||
--host=aws-1-us-east-1.pooler.supabase.com \
|
||||
--port=5432 \
|
||||
--username=postgres.wnzkhezyhnfzhkhiflrp \
|
||||
--dbname=postgres \
|
||||
--data-only \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
--no-acl \
|
||||
--disable-triggers \
|
||||
--exclude-schema=auth \
|
||||
--exclude-schema=storage \
|
||||
--exclude-schema=realtime \
|
||||
--exclude-schema=supabase_functions \
|
||||
--exclude-schema=graphql \
|
||||
--exclude-schema=graphql_public \
|
||||
--exclude-schema=pgsodium \
|
||||
--exclude-schema=pgsodium_masks \
|
||||
--exclude-schema=extensions \
|
||||
--exclude-schema=pgbouncer \
|
||||
--exclude-schema=supabase_migrations \
|
||||
--exclude-schema=net \
|
||||
--exclude-schema=vault \
|
||||
--file=supabase/captured/captured_data.sql
|
||||
```
|
||||
|
||||
Captured data is ~130KB for this project's current size.
|
||||
|
||||
## Restore to Local DB (PostgreSQL 16)
|
||||
|
||||
PostgreSQL 16 doesn't support `transaction_timeout` (added in PG 17) and
|
||||
doesn't have the `supabase_vault` extension. Pre-create the stub objects:
|
||||
|
||||
```bash
|
||||
export PGPASSWORD=routecommerce_dev_password
|
||||
psql -U routecommerce -h 127.0.0.1 -d route_commerce <<'SQL'
|
||||
DROP SCHEMA public CASCADE;
|
||||
CREATE SCHEMA public;
|
||||
GRANT ALL ON SCHEMA public TO routecommerce;
|
||||
|
||||
-- extensions schema (referenced by dump as extensions.uuid_generate_v4())
|
||||
CREATE SCHEMA extensions;
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA extensions;
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto" SCHEMA extensions;
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" SCHEMA extensions;
|
||||
|
||||
-- Stub functions in extensions schema (real Supabase has pgcrypto.crypt etc.)
|
||||
CREATE OR REPLACE FUNCTION extensions.uuid_generate_v4()
|
||||
RETURNS uuid LANGUAGE sql AS $$ SELECT gen_random_uuid(); $$;
|
||||
CREATE OR REPLACE FUNCTION extensions.gen_salt(text)
|
||||
RETURNS text LANGUAGE sql AS $$ SELECT '$2a$06$' || repeat('A', 53); $$;
|
||||
CREATE OR REPLACE FUNCTION extensions.crypt(text, text)
|
||||
RETURNS text LANGUAGE sql AS $$ SELECT $1; $$;
|
||||
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA extensions TO routecommerce;
|
||||
|
||||
-- auth stub (we excluded auth schema from dump, but RLS policies reference auth.uid())
|
||||
CREATE SCHEMA IF NOT EXISTS auth;
|
||||
CREATE TABLE IF NOT EXISTS auth.users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT,
|
||||
raw_user_meta_data JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
CREATE OR REPLACE FUNCTION auth.uid() RETURNS UUID
|
||||
LANGUAGE sql STABLE AS $$ SELECT NULL::UUID; $$;
|
||||
GRANT USAGE ON SCHEMA auth TO routecommerce;
|
||||
GRANT ALL ON auth.users TO routecommerce;
|
||||
GRANT EXECUTE ON FUNCTION auth.uid() TO routecommerce;
|
||||
|
||||
-- Drop Supabase-specific publications (we don't have realtime / vault)
|
||||
DROP PUBLICATION IF EXISTS supabase_realtime;
|
||||
DROP PUBLICATION IF EXISTS supabase_realtime_messages_publication;
|
||||
SQL
|
||||
```
|
||||
|
||||
Strip the PG17-only `SET transaction_timeout` line from the dump:
|
||||
|
||||
```bash
|
||||
sed -i 's/^SET transaction_timeout = 0;$/-- transaction_timeout requires PG17 (we are on PG16); skipped/' \
|
||||
supabase/captured/captured_schema.sql
|
||||
```
|
||||
|
||||
Apply schema and data (idempotent — re-runs are safe):
|
||||
|
||||
```bash
|
||||
psql -U routecommerce -h 127.0.0.1 -d route_commerce -v ON_ERROR_STOP=0 \
|
||||
-f supabase/captured/captured_schema.sql 2>&1 | tail -20
|
||||
|
||||
psql -U routecommerce -h 127.0.0.1 -d route_commerce -v ON_ERROR_STOP=0 \
|
||||
-f supabase/captured/captured_data.sql 2>&1 | tail -20
|
||||
```
|
||||
|
||||
Most errors on re-run are "already exists" — that's expected because the
|
||||
dump is idempotent for CREATE statements (we used `IF NOT EXISTS` where
|
||||
possible, but pg_dump doesn't add it for everything).
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
psql -U routecommerce -h 127.0.0.1 -d route_commerce -c "SELECT count(*) FROM pg_tables WHERE schemaname='public';"
|
||||
# Expect: 65
|
||||
|
||||
psql -U routecommerce -h 127.0.0.1 -d route_commerce -c "SELECT count(*) FROM pg_proc WHERE pronamespace=(SELECT oid FROM pg_namespace WHERE nspname='public');"
|
||||
# Expect: ~250+
|
||||
|
||||
psql -U routecommerce -h 127.0.0.1 -d route_commerce -c "SELECT name, slug, created_at FROM brands ORDER BY created_at;"
|
||||
# Expect 5 brands: Tuxedo Corn, Indian River Direct, Sunrise Farms, Green Valley Organics, Orchard Fresh
|
||||
```
|
||||
|
||||
## What Didn't Work (don't try these)
|
||||
|
||||
| Approach | Why it failed |
|
||||
|---|---|
|
||||
| `psql ... db.wnzkhezyhnfzhkhiflrp.supabase.co:5432` | Hostname is IPv6-only, dev box has no IPv6 |
|
||||
| `aws-0-us-east-1.pooler.supabase.com` | "tenant/user not found" — wrong region (project is on `aws-1`) |
|
||||
| `pg_dump 16.x` against PG 17 server | "server version mismatch" — fatal error |
|
||||
| `supabase db dump --linked` | Requires Docker (we don't have it) |
|
||||
|
||||
## Notes
|
||||
|
||||
- The captured data includes 3 test brands (Sunrise, Green Valley, Orchard)
|
||||
created on 2026-06-03 — these were test data the user added during the
|
||||
migration work, not real customers. They can be deleted safely.
|
||||
- Tuxedo Corn and Indian River Direct are the real production brands.
|
||||
- The schema dump is committed to git at `supabase/captured/captured_schema.sql.gz`
|
||||
for reproducibility. The data dump is gitignored (regenerate as needed).
|
||||
- After dump, the local PostgREST needs a schema cache reload — restart
|
||||
the postgrest process or it'll serve stale metadata for ~30 seconds.
|
||||
|
||||
## Post-restore: clean up test brands
|
||||
|
||||
The captured production data includes 3 test brands with hardcoded
|
||||
sequential UUIDs (`a1b2c3d4-…`, `b2c3d4e5-…`, `c3d4e5f6-…`) created
|
||||
during the migration work. They have no related rows in any of the 38
|
||||
tables that FK to `brands.id`, so deletion is safe (CASCADE / NO ACTION
|
||||
on zero rows is a no-op).
|
||||
|
||||
```sql
|
||||
BEGIN;
|
||||
DELETE FROM brands
|
||||
WHERE slug IN ('sunrise-farms', 'green-valley', 'orchard-fresh');
|
||||
-- expect DELETE 3
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
Verify the real brands remain:
|
||||
|
||||
```sql
|
||||
SELECT name, slug, plan_tier FROM brands ORDER BY created_at;
|
||||
-- Tuxedo Corn | tuxedo | starter
|
||||
-- Indian River Direct | indian-river-direct | starter
|
||||
```
|
||||
|
||||
## Migrating Brand Assets (Supabase Storage → MinIO)
|
||||
|
||||
Brand logos and other Storage files are NOT in the Postgres dump. The
|
||||
storage layer is now MinIO (S3-compatible) instead of Supabase Storage.
|
||||
|
||||
### Download assets from Supabase
|
||||
|
||||
```bash
|
||||
# Make sure the target buckets exist in MinIO (one-time)
|
||||
mc mb --ignore-existing local/brand-logos local/videos \
|
||||
local/product-images local/contacts-imports \
|
||||
local/water-photos
|
||||
|
||||
# Pull each asset via the public URL. Path is <bucket>/<key> after the
|
||||
# /storage/v1/object/public/ prefix in the Supabase URL.
|
||||
mkdir -p .data/assets
|
||||
BRAND_ID="<your-brand-uuid>"
|
||||
for fname in logo.png olathe-sweet-logo.png olathe-sweet-logo-dark.png; do
|
||||
curl -sf -o ".data/assets/$fname" \
|
||||
"https://<project-ref>.supabase.co/storage/v1/object/public/brand-logos/$BRAND_ID/$fname"
|
||||
mc cp ".data/assets/$fname" "local/brand-logos/$BRAND_ID/$fname"
|
||||
done
|
||||
```
|
||||
|
||||
### Point the DB at MinIO (portable /storage/... paths)
|
||||
|
||||
After capture, the `brand_settings.logo_url` etc. values still point at
|
||||
the Supabase URL. Replace the base with a relative `/storage/` path so
|
||||
the Next.js rewrite in `next.config.ts` can route to whichever MinIO
|
||||
endpoint is configured per environment (dev → `localhost:9000`, prod →
|
||||
`storage.route.crispygoat.com`).
|
||||
|
||||
```sql
|
||||
UPDATE brand_settings
|
||||
SET
|
||||
logo_url = REPLACE(logo_url, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||
logo_url_dark = REPLACE(logo_url_dark, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||
olathe_sweet_logo_url = REPLACE(olathe_sweet_logo_url, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||
olathe_sweet_logo_url_dark = REPLACE(olathe_sweet_logo_url_dark, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||
hero_image_url = REPLACE(hero_image_url, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage');
|
||||
```
|
||||
|
||||
### Why a rewrite instead of pointing at MinIO directly
|
||||
|
||||
Next.js's image optimizer (`/_next/image?url=...`) refuses to fetch
|
||||
upstream images whose hostname resolves to a private IP. Local MinIO is
|
||||
on `127.0.0.1` / `::1`, so URLs like `http://localhost:9000/...` get
|
||||
blocked:
|
||||
|
||||
```
|
||||
⨯ upstream image http://localhost:9000/... resolved to private ip
|
||||
["::1","127.0.0.1"]
|
||||
```
|
||||
|
||||
The rewrite in `next.config.ts` resolves `/storage/*` to the configured
|
||||
MinIO base URL server-side, so the browser sees a same-origin URL and
|
||||
the optimizer's private-IP check is bypassed:
|
||||
|
||||
```ts
|
||||
async rewrites() {
|
||||
const storageBase = process.env.STORAGE_PUBLIC_URL || "http://localhost:9000";
|
||||
return [{ source: "/storage/:path*", destination: `${storageBase}/:path*` }];
|
||||
}
|
||||
```
|
||||
|
||||
For production, set `STORAGE_PUBLIC_URL` to the public MinIO endpoint
|
||||
(e.g., `https://storage.route.crispygoat.com`) and the same DB values
|
||||
work without modification.
|
||||
|
||||
### Hardcoded brand asset URLs in client components
|
||||
|
||||
A few components used `publicUrl(BUCKETS.BRAND_LOGOS, ...)` to build a
|
||||
MinIO URL at module load (used as a fallback before client-side data
|
||||
loads). Switch these to `/storage/...` paths so the rewrite covers them:
|
||||
|
||||
- `src/components/storefront/TuxedoVideoHero.tsx` — hero video + Olathe Sweet dark logo
|
||||
- `src/components/time-tracking/TimeTrackingFieldClient.tsx` — field UI logo
|
||||
- `src/app/tuxedo/about/page.tsx` — about page logo
|
||||
|
||||
`src/lib/email-service.ts` should keep using `publicUrl(...)` because
|
||||
Resend fetches the URL server-side from its own network — relative
|
||||
paths won't work for emails.
|
||||
+22
-28
@@ -1,4 +1,5 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { getSessionCookie } from "better-auth/cookies";
|
||||
|
||||
const DEV_UID = "dev-user-00000000-0000-0000-0000-000000000000";
|
||||
|
||||
@@ -9,44 +10,37 @@ export async function middleware(request: NextRequest) {
|
||||
// Allow dev cookies via: document.cookie = "dev_session=platform_admin; path=/"
|
||||
const devSession = request.cookies.get("dev_session")?.value;
|
||||
const isDevMode = devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee";
|
||||
const rcAuthUid = request.cookies.get("rc_auth_uid")?.value;
|
||||
|
||||
let authUid: string | null = null;
|
||||
// Better Auth sets cookie named "rc_session_token" by default (with cookiePrefix: "rc")
|
||||
const sessionCookie = getSessionCookie(request);
|
||||
const hasSession = Boolean(sessionCookie);
|
||||
|
||||
let authed = false;
|
||||
if (isDevMode) {
|
||||
// Dev session only valid in development
|
||||
authUid = DEV_UID;
|
||||
} else if (rcAuthUid) {
|
||||
// rc_auth_uid is set by /api/login — treat as authenticated
|
||||
authUid = rcAuthUid;
|
||||
authed = true;
|
||||
} else if (hasSession) {
|
||||
authed = true;
|
||||
}
|
||||
// No rc_auth_uid in production → authUid stays null → redirect to /login
|
||||
|
||||
const isAdmin = request.nextUrl.pathname.startsWith("/admin");
|
||||
const isLogin = request.nextUrl.pathname === "/login";
|
||||
|
||||
if (isAdmin && !authUid) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
// Auto-login for demo: no Supabase configured, no auth cookie present
|
||||
if (!supabaseUrl || !supabaseUrl.includes("supabase.co")) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/admin";
|
||||
url.searchParams.set("demo", "1");
|
||||
const response = NextResponse.redirect(url);
|
||||
response.cookies.set("dev_session", "platform_admin", {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24,
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
});
|
||||
return response;
|
||||
}
|
||||
if (isAdmin && !authed) {
|
||||
// Auto-login for demo: no auth cookie present
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/login";
|
||||
return NextResponse.redirect(url);
|
||||
url.pathname = "/admin";
|
||||
url.searchParams.set("demo", "1");
|
||||
const response = NextResponse.redirect(url);
|
||||
response.cookies.set("dev_session", "platform_admin", {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24,
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
if (isLogin && authUid) {
|
||||
if (isLogin && authed) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/admin";
|
||||
return NextResponse.redirect(url);
|
||||
@@ -61,4 +55,4 @@ export const config = {
|
||||
"/admin",
|
||||
"/login",
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
+29
-1
@@ -19,6 +19,24 @@ const nextConfig: NextConfig = {
|
||||
protocol: "https",
|
||||
hostname: "picsum.photos",
|
||||
},
|
||||
// Local self-hosted MinIO (replaces Supabase Storage)
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "localhost",
|
||||
port: "9000",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "127.0.0.1",
|
||||
port: "9000",
|
||||
pathname: "/**",
|
||||
},
|
||||
// Production MinIO behind route.crispygoat.com
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "storage.route.crispygoat.com",
|
||||
},
|
||||
],
|
||||
formats: ["image/avif", "image/webp"],
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
@@ -88,8 +106,18 @@ const nextConfig: NextConfig = {
|
||||
|
||||
// Rewrites for API proxy
|
||||
async rewrites() {
|
||||
// Storage proxy: /storage/* -> MinIO at the same path
|
||||
// Lets brand assets and product images use portable relative URLs
|
||||
// (e.g. /storage/brand-logos/<id>/logo.png) in the DB, with Next.js
|
||||
// proxying to whichever MinIO endpoint is configured for the environment.
|
||||
// Avoids the next/image "upstream resolved to private ip" block on
|
||||
// localhost MinIO by keeping the upstream fetch on the server side.
|
||||
const storageBase = process.env.STORAGE_PUBLIC_URL || "http://localhost:9000";
|
||||
return [
|
||||
// Add any necessary rewrites here
|
||||
{
|
||||
source: "/storage/:path*",
|
||||
destination: `${storageBase}/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
+5
-2
@@ -15,17 +15,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.96.0",
|
||||
"@aws-sdk/client-s3": "^3.1062.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1062.0",
|
||||
"@clerk/nextjs": "^7.4.2",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@gsap/react": "^2.1.2",
|
||||
"@sentry/nextjs": "^10.55.0",
|
||||
"@supabase/ssr": "^0.10.2",
|
||||
"@supabase/supabase-js": "^2.105.3",
|
||||
"@upstash/ratelimit": "^2.0.8",
|
||||
"@upstash/redis": "^1.38.0",
|
||||
"better-auth": "^1.6.14",
|
||||
"exceljs": "^4.4.0",
|
||||
"framer-motion": "^12.40.0",
|
||||
"gsap": "^3.15.0",
|
||||
"kysely": "^0.29.2",
|
||||
"lucide-react": "^1.17.0",
|
||||
"next": "^16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -48,6 +50,7 @@
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
||||
|
||||
function getServiceClient() {
|
||||
const roleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!roleKey) throw new Error("SUPABASE_SERVICE_ROLE_KEY is not set");
|
||||
return createServiceClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
roleKey,
|
||||
);
|
||||
}
|
||||
import { svcRpc } from "@/lib/svc-fetch";
|
||||
|
||||
type AdminActionPayload = {
|
||||
action_type: "create" | "update" | "delete";
|
||||
@@ -30,8 +21,7 @@ type UserActivityPayload = {
|
||||
|
||||
export async function logAdminAction(payload: AdminActionPayload): Promise<void> {
|
||||
try {
|
||||
const service = getServiceClient();
|
||||
await service.rpc("log_admin_action", {
|
||||
await svcRpc("log_admin_action", {
|
||||
p_payload: {
|
||||
action_type: payload.action_type,
|
||||
admin_id: payload.admin_id ?? null,
|
||||
@@ -48,8 +38,7 @@ export async function logAdminAction(payload: AdminActionPayload): Promise<void>
|
||||
|
||||
export async function logUserActivity(payload: UserActivityPayload): Promise<void> {
|
||||
try {
|
||||
const service = getServiceClient();
|
||||
await service.rpc("log_user_activity", {
|
||||
await svcRpc("log_user_activity", {
|
||||
p_payload: {
|
||||
user_id: payload.user_id,
|
||||
activity_type: payload.activity_type,
|
||||
|
||||
@@ -1,63 +1,41 @@
|
||||
"use server";
|
||||
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Pool } from "pg";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
const DEV_ADMIN_UID = "dev-user-00000000-0000-0000-0000-000000000001";
|
||||
|
||||
export async function forceAdminLogin(): Promise<{ success: boolean; uid?: string; error?: string }> {
|
||||
const cookieStore = await cookies();
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
try {
|
||||
// Upsert dev platform_admin record
|
||||
const res = await pool.query(
|
||||
`INSERT INTO admin_users (
|
||||
user_id, brand_id, role, active,
|
||||
can_manage_products, can_manage_stops, can_manage_orders, can_manage_pickup,
|
||||
can_manage_messages, can_manage_refunds, can_manage_users, can_manage_water_log,
|
||||
can_manage_reports, can_manage_settings, must_change_password
|
||||
) VALUES (
|
||||
$1, NULL, 'platform_admin', true,
|
||||
true, true, true, true,
|
||||
true, true, true, true,
|
||||
true, true, false
|
||||
)
|
||||
ON CONFLICT (user_id) DO UPDATE SET active = EXCLUDED.active
|
||||
RETURNING id, role`,
|
||||
[DEV_ADMIN_UID]
|
||||
);
|
||||
|
||||
const response = NextResponse.next();
|
||||
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
|
||||
cookies: {
|
||||
getAll() { return cookieStore.getAll(); },
|
||||
setAll(cookiesToSet, headers) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
response.cookies.set(name, value, options);
|
||||
});
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
response.headers.set(key, value);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Upsert dev platform_admin record
|
||||
const { data: existing } = await supabase
|
||||
.from("admin_users")
|
||||
.select("id, role")
|
||||
.eq("user_id", DEV_ADMIN_UID)
|
||||
.single();
|
||||
|
||||
if (!existing) {
|
||||
const { error: insertError } = await supabase
|
||||
.from("admin_users")
|
||||
.insert({
|
||||
user_id: DEV_ADMIN_UID,
|
||||
brand_id: null,
|
||||
role: "platform_admin",
|
||||
active: true,
|
||||
can_manage_products: true,
|
||||
can_manage_stops: true,
|
||||
can_manage_orders: true,
|
||||
can_manage_pickup: true,
|
||||
can_manage_messages: true,
|
||||
can_manage_refunds: true,
|
||||
can_manage_users: true,
|
||||
can_manage_water_log: true,
|
||||
can_manage_reports: true,
|
||||
can_manage_settings: true,
|
||||
must_change_password: false,
|
||||
});
|
||||
|
||||
if (insertError) {
|
||||
return { success: false, error: insertError.message };
|
||||
if (res.rows.length === 0) {
|
||||
return { success: false, error: "Failed to upsert dev admin" };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, uid: DEV_ADMIN_UID };
|
||||
return { success: true, uid: DEV_ADMIN_UID };
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : "Unknown error";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
||||
import { svcRpc } from "@/lib/svc-fetch";
|
||||
|
||||
export async function updatePasswordAction(
|
||||
newPassword: string
|
||||
@@ -15,16 +15,11 @@ export async function updatePasswordAction(
|
||||
return { error: "Not authenticated. Please log in again." };
|
||||
}
|
||||
|
||||
const service = createServiceClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||
);
|
||||
|
||||
const { error } = await service.rpc("update_user_password", {
|
||||
const { error } = await svcRpc("update_user_password", {
|
||||
p_user_id: uid,
|
||||
p_password: newPassword,
|
||||
});
|
||||
|
||||
if (error) return { error: error.message };
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use server";
|
||||
|
||||
import { Pool } from "pg";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
export type UpdateAdminProfileResult =
|
||||
| { success: true }
|
||||
| { success: false; error: string };
|
||||
|
||||
export async function updateAdminProfileAction(
|
||||
id: string,
|
||||
displayName: string | null,
|
||||
phoneNumber: string | null
|
||||
): Promise<UpdateAdminProfileResult> {
|
||||
try {
|
||||
await pool.query("SELECT update_admin_user($1, $2, $3)", [
|
||||
id,
|
||||
displayName,
|
||||
phoneNumber,
|
||||
]);
|
||||
revalidatePath("/admin/me");
|
||||
return { success: true };
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : "Failed to update profile";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
||||
|
||||
function getServiceClient() {
|
||||
const roleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!roleKey) throw new Error("SUPABASE_SERVICE_ROLE_KEY not set");
|
||||
return createServiceClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
roleKey,
|
||||
);
|
||||
}
|
||||
import * as authAdmin from "@/lib/auth-admin";
|
||||
|
||||
export type ResetAdminPasswordResult =
|
||||
| { success: true; tempPassword: string }
|
||||
@@ -24,24 +15,19 @@ export async function resetAdminPassword(
|
||||
email: string,
|
||||
newPassword: string
|
||||
): Promise<ResetAdminPasswordResult> {
|
||||
const service = getServiceClient();
|
||||
|
||||
// Look up auth user by email
|
||||
const { data: authUsers, error: listError } = await service.auth.admin.listUsers();
|
||||
if (listError || !authUsers?.users) {
|
||||
return { success: false, error: "Could not list users: " + listError?.message };
|
||||
const { data: users, error: listError } = await authAdmin.listUsers({ searchValue: email });
|
||||
if (listError) {
|
||||
return { success: false, error: "Could not list users: " + listError.message };
|
||||
}
|
||||
|
||||
const authUser = authUsers.users.find((u) => u.email?.toLowerCase() === email.toLowerCase());
|
||||
const authUser = (users ?? []).find((u) => u.email?.toLowerCase() === email.toLowerCase());
|
||||
if (!authUser) {
|
||||
return { success: false, error: "No auth user found for that email address." };
|
||||
}
|
||||
|
||||
// Update password via service role
|
||||
const { error: updateError } = await service.auth.admin.updateUserById(
|
||||
authUser.id,
|
||||
{ password: newPassword, email_confirm: true }
|
||||
);
|
||||
// Update password via the better-auth admin wrapper (uses `update_user_password` RPC internally)
|
||||
const { error: updateError } = await authAdmin.setUserPassword(authUser.id, newPassword);
|
||||
|
||||
if (updateError) {
|
||||
return { success: false, error: updateError.message };
|
||||
|
||||
+93
-203
@@ -1,14 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
||||
import { supabase as publicSupabase } from "@/lib/supabase";
|
||||
import { getMockTableData, mockBrands } from "@/lib/mock-data";
|
||||
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
import { api as publicApi } from "@/lib/api";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { svcApi, svcRpc } from "@/lib/svc-fetch";
|
||||
import * as authAdmin from "@/lib/auth-admin";
|
||||
|
||||
export type AdminUserRow = {
|
||||
id: string;
|
||||
@@ -75,66 +71,44 @@ export type UpdateAdminUserInput = {
|
||||
phone_number?: string | null;
|
||||
};
|
||||
|
||||
// ─── SSR client for authenticated requests ─────────────────────────────────
|
||||
// ─── Auth helper: validate session via better-auth ──────────────────────────
|
||||
|
||||
async function getAuthClient() {
|
||||
const cookieStore = await cookies();
|
||||
/**
|
||||
* Read rc_auth_uid from the raw HTTP Cookie header. This is set by the
|
||||
* Emergency Force Login flow and acts as a dev-mode bypass for normal auth.
|
||||
*/
|
||||
async function readRcAuthUid(): Promise<string | null> {
|
||||
const headerStore = await headers();
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const request = new NextRequest("http://localhost/admin", { headers: new Headers() });
|
||||
const response = NextResponse.next({ request });
|
||||
|
||||
// Read rc_auth_uid from the raw HTTP Cookie header (document.cookie sets
|
||||
// cookies that arrive in the header but NOT in next/headers cookies()).
|
||||
const cookieHeader = headerStore.get("cookie") || "";
|
||||
const allCookies = cookieHeader.split(";").map(c => c.trim());
|
||||
const rcUidCookie = allCookies.find(c => c.startsWith("rc_auth_uid="));
|
||||
const rcAuthUid = rcUidCookie ? rcUidCookie.split("=")[1] : null;
|
||||
|
||||
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
|
||||
cookies: {
|
||||
getAll() { return cookieStore.getAll(); },
|
||||
setAll(cookiesToSet, headers) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
|
||||
Object.entries(headers).forEach(([key, value]) => response.headers.set(key, value));
|
||||
},
|
||||
},
|
||||
});
|
||||
return { supabase, response, rcAuthUid };
|
||||
return rcUidCookie ? rcUidCookie.split("=")[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated RPC call. Validates the session via better-auth, then calls
|
||||
* the named RPC using the service-role client.
|
||||
*
|
||||
* In development, the DEV_FORCE_UID cookie bypasses the auth check (the
|
||||
* Emergency Force Login path is itself authenticated upstream).
|
||||
*/
|
||||
async function callRpcWithAuth<T>(fn: string, params: Record<string, unknown>): Promise<{ data: T | null; error: string | null }> {
|
||||
const { supabase, rcAuthUid } = await getAuthClient();
|
||||
|
||||
// Dev force-login UID bypasses Supabase auth entirely
|
||||
const DEV_FORCE_UID = "dev-user-00000000-0000-0000-0000-000000000001";
|
||||
const rcAuthUid = await readRcAuthUid();
|
||||
if (rcAuthUid === DEV_FORCE_UID) {
|
||||
return { data: null, error: null }; // let the action proceed without auth check
|
||||
return { data: null, error: null }; // dev force-login bypass
|
||||
}
|
||||
|
||||
const { data: userData, error: userError } = await supabase.auth.getUser();
|
||||
if (userError || !userData.user) {
|
||||
const headerStore = await headers();
|
||||
const session = await auth.api.getSession({ headers: headerStore });
|
||||
if (!session?.user) {
|
||||
return { data: null, error: "Not authenticated" };
|
||||
}
|
||||
const { data, error } = await supabase.rpc(fn, params as Record<string, unknown>);
|
||||
if (error) { /* RPC error handled silently */ }
|
||||
|
||||
const { data, error } = await svcRpc<T>(fn, params);
|
||||
return { data: data as T, error: error ? error.message : null };
|
||||
}
|
||||
|
||||
// ─── Service role client (server-only, never exposed to browser) ───────────
|
||||
|
||||
function getServiceClient() {
|
||||
const roleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!roleKey) {
|
||||
throw new Error("SUPABASE_SERVICE_ROLE_KEY is not set. Cannot use service role in dev path.");
|
||||
}
|
||||
return createServiceClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
roleKey,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Dev-only path — uses service role to create auth user + admin_users ──
|
||||
|
||||
async function devCreateAdminUser(input: CreateAdminUserInput): Promise<{ user: AdminUserRow | null; error: string | null }> {
|
||||
@@ -147,27 +121,21 @@ async function devCreateAdminUser(input: CreateAdminUserInput): Promise<{ user:
|
||||
return { user: null, error: "Not authenticated" };
|
||||
}
|
||||
|
||||
const service = getServiceClient();
|
||||
|
||||
// Create auth user with the provided password
|
||||
const { data: authUser, error: authError } = await service.auth.admin.createUser({
|
||||
// Create auth user with the provided password (better-auth signUpEmail)
|
||||
const { data: authUser, error: authError } = await authAdmin.createUser({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
email_confirm: true,
|
||||
user_metadata: {
|
||||
display_name: input.display_name || input.email.split("@")[0],
|
||||
phone_number: input.phone_number ?? null,
|
||||
},
|
||||
name: input.display_name || input.email.split("@")[0],
|
||||
});
|
||||
if (authError || !authUser.user) {
|
||||
if (authError || !authUser) {
|
||||
return { user: null, error: authError?.message ?? "Failed to create auth user" };
|
||||
}
|
||||
|
||||
// Insert into admin_users
|
||||
const { data: inserted, error: insertError } = await service
|
||||
// Insert into admin_users (service role bypasses RLS)
|
||||
const { data: inserted, error: insertError } = await svcApi
|
||||
.from("admin_users")
|
||||
.insert({
|
||||
user_id: authUser.user.id,
|
||||
user_id: authUser.id,
|
||||
role: input.role,
|
||||
brand_id: input.brand_id,
|
||||
display_name: input.display_name || input.email.split("@")[0],
|
||||
@@ -185,11 +153,14 @@ async function devCreateAdminUser(input: CreateAdminUserInput): Promise<{ user:
|
||||
must_change_password: input.mustChangePassword ?? true,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
.single() as { data: any; error: { message: string } | null };
|
||||
|
||||
if (insertError) {
|
||||
return { user: null, error: insertError.message };
|
||||
}
|
||||
if (!inserted) {
|
||||
return { user: null, error: "Insert returned no row" };
|
||||
}
|
||||
|
||||
// Send welcome email
|
||||
try {
|
||||
@@ -263,27 +234,24 @@ function mapUserRow(row: Record<string, unknown>): AdminUserRow {
|
||||
// ─── Dev path helpers (service role, local only) ───────────────────────────
|
||||
|
||||
async function devListAdminUsers(callerUid?: string): Promise<{ users: AdminUserRow[]; error: string | null }> {
|
||||
const service = getServiceClient();
|
||||
|
||||
// Ensure caller has an admin_users record
|
||||
if (callerUid) {
|
||||
const { data: existing } = await service
|
||||
const { data: existing } = await svcApi
|
||||
.from("admin_users")
|
||||
.select("id")
|
||||
.eq("user_id", callerUid)
|
||||
.maybeSingle();
|
||||
.maybeSingle() as { data: any };
|
||||
|
||||
if (!existing) {
|
||||
// auto-creating admin_users for uid
|
||||
const { data: authData } = await service.auth.admin.listUsers();
|
||||
const authUser = authData?.users?.find((u) => u.id === callerUid);
|
||||
const meta = (authUser as { user_metadata?: Record<string, unknown> })?.user_metadata;
|
||||
await service.from("admin_users").insert({
|
||||
const { data: listData } = await authAdmin.listUsers({ searchValue: undefined });
|
||||
const authUser = (listData ?? []).find((u: any) => u.id === callerUid);
|
||||
await svcApi.from("admin_users").insert({
|
||||
user_id: callerUid,
|
||||
role: "platform_admin",
|
||||
brand_id: null,
|
||||
display_name: (meta?.display_name as string | null) ?? authUser?.email?.split("@")[0] ?? "Admin",
|
||||
phone_number: (meta?.phone_number as string | null) ?? null,
|
||||
display_name: authUser?.name ?? authUser?.email?.split("@")[0] ?? "Admin",
|
||||
phone_number: null,
|
||||
can_manage_products: true,
|
||||
can_manage_stops: true,
|
||||
can_manage_orders: true,
|
||||
@@ -300,7 +268,7 @@ async function devListAdminUsers(callerUid?: string): Promise<{ users: AdminUser
|
||||
}
|
||||
|
||||
// Fetch all admin_users rows (no RLS for service role)
|
||||
const { data: adminRows, error: adminError } = await service
|
||||
const { data: adminRows, error: adminError } = await svcApi
|
||||
.from("admin_users")
|
||||
.select(`
|
||||
id, user_id, role, brand_id, active, must_change_password, created_at, last_login,
|
||||
@@ -308,57 +276,30 @@ async function devListAdminUsers(callerUid?: string): Promise<{ users: AdminUser
|
||||
can_manage_messages, can_manage_refunds, can_manage_users, can_manage_water_log, can_manage_reports,
|
||||
brands (name)
|
||||
`)
|
||||
.order("created_at", { ascending: false });
|
||||
.order("created_at", { ascending: false }) as { data: any; error: any };
|
||||
|
||||
if (adminError) return { users: [], error: adminError.message };
|
||||
|
||||
// Fetch auth user details via service role admin API
|
||||
const { data: authData, error: authError } = await service.auth.admin.listUsers();
|
||||
// Fetch auth user details via better-auth admin list
|
||||
const { data: listData, error: authError } = await authAdmin.listUsers({ searchValue: undefined });
|
||||
if (authError) return { users: [], error: authError.message };
|
||||
|
||||
const authMap: Record<string, { email: string; display_name: string | null; phone_number: string | null }> = {};
|
||||
(authData?.users ?? []).forEach((u) => {
|
||||
const user = u as { id: string; email?: string; user_metadata?: Record<string, unknown> };
|
||||
authMap[user.id] = {
|
||||
email: user.email ?? "",
|
||||
display_name: (user.user_metadata?.display_name as string | null) ?? (user.user_metadata?.full_name as string | null) ?? null,
|
||||
phone_number: (user.user_metadata?.phone_number as string | null) ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
const users: AdminUserRow[] = (adminRows ?? []).map((row) => {
|
||||
const r = row as Record<string, unknown> & { brands?: { name?: string } };
|
||||
const authInfo = authMap[String(r.user_id ?? "")] ?? { email: "", display_name: null, phone_number: null };
|
||||
return {
|
||||
...mapUserRow(r),
|
||||
email: authInfo.email || "No Email",
|
||||
display_name: authInfo.display_name ?? null,
|
||||
phone_number: authInfo.phone_number ?? (r.phone_number as string | null) ?? null,
|
||||
brand_name: r.brands?.name ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
return { users, error: null };
|
||||
}
|
||||
|
||||
function buildUsersFromRows(adminRows: Record<string, unknown>[], authUsers: { id: string; email?: string; user_metadata?: Record<string, unknown> }[]): { users: AdminUserRow[]; error: string | null } {
|
||||
const authMap: Record<string, { email: string; display_name: string | null; phone_number: string | null }> = {};
|
||||
(authUsers ?? []).forEach((u) => {
|
||||
const authMap: Record<string, { email: string; display_name: string | null }> = {};
|
||||
(listData ?? []).forEach((u: any) => {
|
||||
authMap[u.id] = {
|
||||
email: u.email ?? "",
|
||||
display_name: (u.user_metadata?.display_name as string | null) ?? (u.user_metadata?.full_name as string | null) ?? null,
|
||||
phone_number: (u.user_metadata?.phone_number as string | null) ?? null,
|
||||
display_name: u.name ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
const users: AdminUserRow[] = adminRows.map((row) => {
|
||||
const users: AdminUserRow[] = (adminRows ?? []).map((row: any) => {
|
||||
const r = row as Record<string, unknown> & { brands?: { name?: string } };
|
||||
const authInfo = authMap[String(r.user_id ?? "")] ?? { email: "", display_name: null, phone_number: null };
|
||||
const authInfo = authMap[String(r.user_id ?? "")] ?? { email: "", display_name: null };
|
||||
return {
|
||||
...mapUserRow(r),
|
||||
email: authInfo.email || "No Email",
|
||||
display_name: authInfo.display_name ?? null,
|
||||
phone_number: authInfo.phone_number ?? (r.phone_number as string | null) ?? null,
|
||||
phone_number: (r.phone_number as string | null) ?? null,
|
||||
brand_name: r.brands?.name ?? null,
|
||||
};
|
||||
});
|
||||
@@ -371,15 +312,6 @@ function buildUsersFromRows(adminRows: Record<string, unknown>[], authUsers: { i
|
||||
const DEV_FORCE_UID = "dev-user-00000000-0000-0000-0000-000000000001";
|
||||
|
||||
export async function getAdminUsers(brandId?: string): Promise<{ users: AdminUserRow[]; error: string | null }> {
|
||||
if (useMockData) {
|
||||
const mockUsers = getMockTableData("users") as AdminUserRow[];
|
||||
let filteredUsers = mockUsers;
|
||||
if (brandId) {
|
||||
filteredUsers = mockUsers.filter(u => u.brand_id === brandId);
|
||||
}
|
||||
return { users: filteredUsers, error: null };
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const headerStore = await headers();
|
||||
const devSession = cookieStore.get("dev_session")?.value;
|
||||
@@ -390,7 +322,7 @@ export async function getAdminUsers(brandId?: string): Promise<{ users: AdminUse
|
||||
.find(c => c.startsWith("rc_auth_uid="))?.split("=")[1] ?? null;
|
||||
|
||||
// In development mode: ALL requests with a valid rc_auth_uid use the dev/service path.
|
||||
// This includes real Supabase auth users — bypassing supabase.auth.getUser JWT validation.
|
||||
// This includes real Supabase auth users — bypassing api.auth.getUser JWT validation.
|
||||
if (process.env.NODE_ENV !== "production" && rcAuthUid && rcAuthUid !== DEV_FORCE_UID) {
|
||||
return devListAdminUsers(rcAuthUid);
|
||||
}
|
||||
@@ -406,38 +338,8 @@ export async function getAdminUsers(brandId?: string): Promise<{ users: AdminUse
|
||||
// Production path: try authenticated RPC first, fall back to service role if not authenticated
|
||||
const result = await callRpcWithAuth<AdminUserRow[]>("get_admin_users", { p_brand_id: brandId ?? null });
|
||||
if (result.error === "Not authenticated" && rcAuthUid) {
|
||||
// No Supabase session token in browser — use service role with rc_auth_uid
|
||||
const service = getServiceClient();
|
||||
const { data: adminRows, error: adminError } = await service
|
||||
.from("admin_users")
|
||||
.select(`id, user_id, role, brand_id, active, must_change_password, created_at, last_login,
|
||||
can_manage_products, can_manage_stops, can_manage_orders, can_manage_pickup,
|
||||
can_manage_messages, can_manage_refunds, can_manage_users, can_manage_water_log, can_manage_reports,
|
||||
brands (name)`)
|
||||
.order("created_at", { ascending: false });
|
||||
if (adminError) return { users: [], error: adminError.message };
|
||||
const { data: authData } = await service.auth.admin.listUsers();
|
||||
const authMap: Record<string, { email: string; display_name: string | null; phone_number: string | null }> = {};
|
||||
(authData?.users ?? []).forEach((u) => {
|
||||
const user = u as { id: string; email?: string; user_metadata?: Record<string, unknown> };
|
||||
authMap[user.id] = {
|
||||
email: user.email ?? "",
|
||||
display_name: (user.user_metadata?.display_name as string | null) ?? null,
|
||||
phone_number: (user.user_metadata?.phone_number as string | null) ?? null,
|
||||
};
|
||||
});
|
||||
const users: AdminUserRow[] = (adminRows ?? []).map((row) => {
|
||||
const r = row as Record<string, unknown> & { brands?: { name?: string } };
|
||||
const authInfo = authMap[String(r.user_id ?? "")] ?? { email: "", display_name: null, phone_number: null };
|
||||
return {
|
||||
...mapUserRow(r),
|
||||
email: authInfo.email || "No Email",
|
||||
display_name: authInfo.display_name ?? null,
|
||||
phone_number: authInfo.phone_number ?? (r.phone_number as string | null) ?? null,
|
||||
brand_name: r.brands?.name ?? null,
|
||||
};
|
||||
});
|
||||
return { users, error: null };
|
||||
// No better-auth session token in browser — use service role via devListAdminUsers
|
||||
return devListAdminUsers(rcAuthUid);
|
||||
}
|
||||
return { users: result.data ?? [], error: result.error };
|
||||
}
|
||||
@@ -466,27 +368,21 @@ export async function createAdminUser(input: CreateAdminUserInput): Promise<{ us
|
||||
// Production path — use service role directly (bypasses Supabase JWT auth)
|
||||
// rc_auth_uid cookie proves the admin is logged in; service role creates the account
|
||||
if (rcAuthUid) {
|
||||
const service = getServiceClient();
|
||||
|
||||
// Create auth user
|
||||
const { data: authUser, error: authError } = await service.auth.admin.createUser({
|
||||
// Create auth user via better-auth
|
||||
const { data: authUser, error: authError } = await authAdmin.createUser({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
email_confirm: true,
|
||||
user_metadata: {
|
||||
display_name: input.display_name || input.email.split("@")[0],
|
||||
phone_number: input.phone_number ?? null,
|
||||
},
|
||||
name: input.display_name || input.email.split("@")[0],
|
||||
});
|
||||
if (authError || !authUser.user) {
|
||||
if (authError || !authUser) {
|
||||
return { user: null, error: authError?.message ?? "Failed to create auth user" };
|
||||
}
|
||||
|
||||
// Insert into admin_users
|
||||
const { data: inserted, error: insertError } = await service
|
||||
// Insert into admin_users via service-role PostgREST
|
||||
const { data: inserted, error: insertError } = await svcApi
|
||||
.from("admin_users")
|
||||
.insert({
|
||||
user_id: authUser.user.id,
|
||||
user_id: authUser.id,
|
||||
role: input.role,
|
||||
brand_id: input.brand_id,
|
||||
display_name: input.display_name || input.email.split("@")[0],
|
||||
@@ -506,7 +402,9 @@ export async function createAdminUser(input: CreateAdminUserInput): Promise<{ us
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (insertError) return { user: null, error: insertError.message };
|
||||
if (insertError || !inserted) {
|
||||
return { user: null, error: insertError?.message ?? "Insert returned no row" };
|
||||
}
|
||||
|
||||
// Send welcome email
|
||||
try {
|
||||
@@ -525,26 +423,26 @@ export async function createAdminUser(input: CreateAdminUserInput): Promise<{ us
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: inserted.id,
|
||||
user_id: inserted.user_id,
|
||||
id: inserted.id ?? "",
|
||||
user_id: inserted.user_id ?? authUser.id,
|
||||
display_name: inserted.display_name ?? input.display_name ?? input.email.split("@")[0],
|
||||
email: input.email,
|
||||
phone_number: inserted.phone_number ?? input.phone_number ?? null,
|
||||
role: inserted.role,
|
||||
brand_id: inserted.brand_id,
|
||||
role: (inserted.role as AdminUserRow["role"]) ?? "store_employee",
|
||||
brand_id: inserted.brand_id ?? null,
|
||||
brand_name: null,
|
||||
can_manage_products: inserted.can_manage_products,
|
||||
can_manage_stops: inserted.can_manage_stops,
|
||||
can_manage_orders: inserted.can_manage_orders,
|
||||
can_manage_pickup: inserted.can_manage_pickup,
|
||||
can_manage_messages: inserted.can_manage_messages,
|
||||
can_manage_refunds: inserted.can_manage_refunds,
|
||||
can_manage_users: inserted.can_manage_users,
|
||||
can_manage_water_log: inserted.can_manage_water_log,
|
||||
can_manage_reports: inserted.can_manage_reports,
|
||||
active: inserted.active,
|
||||
can_manage_products: inserted.can_manage_products ?? false,
|
||||
can_manage_stops: inserted.can_manage_stops ?? false,
|
||||
can_manage_orders: inserted.can_manage_orders ?? false,
|
||||
can_manage_pickup: inserted.can_manage_pickup ?? false,
|
||||
can_manage_messages: inserted.can_manage_messages ?? false,
|
||||
can_manage_refunds: inserted.can_manage_refunds ?? false,
|
||||
can_manage_users: inserted.can_manage_users ?? false,
|
||||
can_manage_water_log: inserted.can_manage_water_log ?? false,
|
||||
can_manage_reports: inserted.can_manage_reports ?? false,
|
||||
active: inserted.active ?? true,
|
||||
must_change_password: inserted.must_change_password ?? true,
|
||||
created_at: inserted.created_at,
|
||||
created_at: inserted.created_at ?? new Date().toISOString(),
|
||||
last_login: null,
|
||||
},
|
||||
error: null,
|
||||
@@ -564,8 +462,7 @@ export async function updateAdminUser(input: UpdateAdminUserInput): Promise<{ us
|
||||
.find(c => c.startsWith("rc_auth_uid="))?.split("=")[1] ?? null;
|
||||
// DEV_FORCE_UID bypass — Emergency Force Login sets this cookie directly
|
||||
if (rcAuthUid === DEV_FORCE_UID) {
|
||||
const service = getServiceClient();
|
||||
const { data, error } = await service
|
||||
const { data, error } = await svcApi
|
||||
.from("admin_users")
|
||||
.update({
|
||||
role: input.role ?? undefined,
|
||||
@@ -586,8 +483,8 @@ export async function updateAdminUser(input: UpdateAdminUserInput): Promise<{ us
|
||||
.eq("id", input.id)
|
||||
.select()
|
||||
.single();
|
||||
if (error) return { user: null, error: error.message };
|
||||
return { user: mapUserRow(data), error: null };
|
||||
if (error || !data) return { user: null, error: error?.message ?? "Update returned no row" };
|
||||
return { user: mapUserRow(data as Record<string, unknown>), error: null };
|
||||
}
|
||||
|
||||
const result = await callRpcWithAuth<AdminUserRow[]>("update_admin_user", {
|
||||
@@ -613,20 +510,19 @@ export async function deleteAdminUser(id: string): Promise<{ success: boolean; e
|
||||
.find(c => c.startsWith("rc_auth_uid="))?.split("=")[1] ?? null;
|
||||
// DEV_FORCE_UID bypass — Emergency Force Login sets this cookie directly
|
||||
if (rcAuthUid === DEV_FORCE_UID) {
|
||||
const service = getServiceClient();
|
||||
// Get user_id first
|
||||
const { data: adminRow, error: fetchError } = await service
|
||||
const { data: adminRow, error: fetchError } = await svcApi
|
||||
.from("admin_users")
|
||||
.select("user_id")
|
||||
.eq("id", id)
|
||||
.single();
|
||||
if (fetchError) return { success: false, error: fetchError.message };
|
||||
// Delete from admin_users
|
||||
const { error: deleteError } = await service.from("admin_users").delete().eq("id", id);
|
||||
const { error: deleteError } = await svcApi.from("admin_users").delete().eq("id", id);
|
||||
if (deleteError) return { success: false, error: deleteError.message };
|
||||
// Delete auth user
|
||||
// Delete auth user via better-auth admin
|
||||
if (adminRow?.user_id) {
|
||||
await service.auth.admin.deleteUser(adminRow.user_id);
|
||||
await authAdmin.removeUser(adminRow.user_id);
|
||||
}
|
||||
return { success: true, error: null };
|
||||
}
|
||||
@@ -643,30 +539,24 @@ export async function setMustChangePassword(userId: string): Promise<{ success:
|
||||
.find(c => c.startsWith("rc_auth_uid="))?.split("=")[1] ?? null;
|
||||
|
||||
if (rcAuthUid === DEV_FORCE_UID || process.env.NODE_ENV !== "production") {
|
||||
const service = getServiceClient();
|
||||
const { error } = await service.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
||||
const { error } = await svcApi.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
||||
return { success: !error, error: error?.message ?? null };
|
||||
}
|
||||
|
||||
// Production path — use service role via direct update
|
||||
const service = getServiceClient();
|
||||
const { error } = await service.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
||||
const { error } = await svcApi.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
||||
return { success: !error, error: error?.message ?? null };
|
||||
}
|
||||
|
||||
export async function sendPasswordResetEmail(email: string): Promise<{ success: boolean; error: string | null }> {
|
||||
const { error } = await publicSupabase.auth.resetPasswordForEmail(email, {
|
||||
const { error } = await authAdmin.requestPasswordReset({
|
||||
email,
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"}/change-password`,
|
||||
});
|
||||
return { success: !error, error: error?.message ?? null };
|
||||
}
|
||||
|
||||
export async function getBrands(): Promise<{ brands: { id: string; name: string }[]; error: string | null }> {
|
||||
if (useMockData) {
|
||||
const brands = mockBrands.map(b => ({ id: b.id, name: b.name }));
|
||||
return { brands, error: null };
|
||||
}
|
||||
|
||||
const { data, error } = await publicSupabase.from("brands").select("id, name").order("name");
|
||||
const { data, error } = await publicApi.from("brands").select("id, name").order("name");
|
||||
return { brands: data ?? [], error: error?.message ?? null };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export type AIAuthConfig = {
|
||||
provider: string;
|
||||
@@ -18,7 +18,7 @@ export async function getAIPreferences(brandId: string): Promise<{
|
||||
model?: string;
|
||||
max_tokens?: number;
|
||||
} | null> {
|
||||
const { data, error } = await supabase
|
||||
const { data, error } = await api
|
||||
.from("brand_ai_settings")
|
||||
.select("api_key, organization_id, base_url, model, max_tokens")
|
||||
.eq("brand_id", brandId)
|
||||
@@ -32,7 +32,7 @@ export async function saveAIPreferences(
|
||||
brandId: string,
|
||||
config: AIAuthConfig
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const { error } = await supabase
|
||||
const { error } = await api
|
||||
.from("brand_ai_settings")
|
||||
.upsert({
|
||||
brand_id: brandId,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ type AuditResult =
|
||||
* Audit writes bypass RLS via the SECURITY DEFINER log_audit_event RPC function.
|
||||
*/
|
||||
export async function logAuditEvent(payload: AuditPayload): Promise<AuditResult> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { ADDONS, PLAN_TIERS, type AddonKey, type PlanTierKey } from "@/lib/pricing";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
export type BillingSubscriptionStatus =
|
||||
| "active"
|
||||
|
||||
@@ -32,8 +32,8 @@ export async function createRetailStripeCheckoutSession(
|
||||
}));
|
||||
|
||||
// Get brand name for Stripe metadata
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
let brandName = "Route Commerce";
|
||||
try {
|
||||
const brandRes = await fetch(
|
||||
|
||||
@@ -43,8 +43,8 @@ export async function createStripeCheckoutSession(
|
||||
const priceId = getPriceId(priceKey);
|
||||
if (!priceId) return { success: false, error: `No Stripe price configured for "${priceKey}". Set ${priceKey.toUpperCase()}_PRICE in your environment.` };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Get brand's Stripe customer ID
|
||||
const custRes = await fetch(
|
||||
@@ -123,8 +123,8 @@ export async function cancelAddonSubscription(
|
||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Get active subscription for this brand
|
||||
const subRes = await fetch(
|
||||
|
||||
@@ -13,8 +13,8 @@ export async function getStripeBillingPortalUrl(brandId: string): Promise<{ succ
|
||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Get stripe_customer_id from brands table
|
||||
const custRes = await fetch(
|
||||
@@ -49,8 +49,8 @@ export async function updateBrandPlanTier(brandId: string, planTier: string): Pr
|
||||
return { success: false, error: "Invalid plan tier" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/update_brand_plan_tier`,
|
||||
@@ -72,8 +72,8 @@ export async function updateBrandStripeCustomerId(brandId: string, stripeCustome
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/update_brand_stripe_customer_id`,
|
||||
@@ -89,8 +89,8 @@ export async function updateBrandStripeCustomerId(brandId: string, stripeCustome
|
||||
}
|
||||
|
||||
export async function getBrandPlanInfo(brandId: string): Promise<{ success: boolean; data?: any; error?: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_brand_plan_info`,
|
||||
@@ -108,8 +108,8 @@ export async function getBrandPlanInfo(brandId: string): Promise<{ success: bool
|
||||
}
|
||||
|
||||
export async function getEnabledAddons(brandId: string): Promise<Record<string, boolean>> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_brand_features`,
|
||||
@@ -128,8 +128,8 @@ export async function getEnabledAddons(brandId: string): Promise<Record<string,
|
||||
}
|
||||
|
||||
export async function getRecentWholesaleOrders(brandId: string, limit = 20): Promise<any[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_orders`,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { uploadFile, BUCKETS, publicUrl, storageKeys } from "@/lib/storage";
|
||||
|
||||
export type UploadLogoResult =
|
||||
| { success: true; logoUrl: string }
|
||||
@@ -30,31 +31,28 @@ export async function uploadBrandLogo(
|
||||
}
|
||||
|
||||
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
||||
const path = isDark ? `logo-dark.${ext}` : `logo.${ext}`;
|
||||
const storagePath = `brand-logos/${brandId}/${path}`;
|
||||
const fileName = isDark ? `logo-dark.${ext}` : `logo.${ext}`;
|
||||
const key = storageKeys.brandLogo(brandId, fileName);
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
||||
let url: string;
|
||||
try {
|
||||
const res = await uploadFile({
|
||||
bucket: BUCKETS.BRAND_LOGOS,
|
||||
key,
|
||||
body: buffer,
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
contentType: file.type,
|
||||
});
|
||||
url = res.url;
|
||||
} catch (e) {
|
||||
return { success: false, error: `Upload failed: ${(e as Error).message}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Save URL to brand_settings via RPC
|
||||
const saveRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||
{
|
||||
@@ -62,7 +60,7 @@ export async function uploadBrandLogo(
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
[isDark ? "p_logo_url_dark" : "p_logo_url"]: publicUrl,
|
||||
[isDark ? "p_logo_url_dark" : "p_logo_url"]: url,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -71,7 +69,7 @@ export async function uploadBrandLogo(
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
|
||||
return { success: true, logoUrl: publicUrl };
|
||||
return { success: true, logoUrl: url };
|
||||
}
|
||||
|
||||
export async function uploadOlatheSweetLogo(
|
||||
@@ -96,28 +94,26 @@ export async function uploadOlatheSweetLogo(
|
||||
}
|
||||
|
||||
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
||||
const storagePath = `brand-logos/${brandId}/olathe-sweet-logo.${ext}`;
|
||||
const key = storageKeys.brandLogo(brandId, `olathe-sweet-logo.${ext}`);
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
||||
let url: string;
|
||||
try {
|
||||
const res = await uploadFile({
|
||||
bucket: BUCKETS.BRAND_LOGOS,
|
||||
key,
|
||||
body: buffer,
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
contentType: file.type,
|
||||
});
|
||||
url = res.url;
|
||||
} catch (e) {
|
||||
return { success: false, error: `Upload failed: ${(e as Error).message}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const saveRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||
@@ -126,7 +122,7 @@ export async function uploadOlatheSweetLogo(
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
p_olathe_sweet_logo_url: publicUrl,
|
||||
p_olathe_sweet_logo_url: url,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -135,7 +131,7 @@ export async function uploadOlatheSweetLogo(
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
|
||||
return { success: true, logoUrl: publicUrl };
|
||||
return { success: true, logoUrl: url };
|
||||
}
|
||||
|
||||
export async function uploadOlatheSweetLogoDark(
|
||||
@@ -160,28 +156,26 @@ export async function uploadOlatheSweetLogoDark(
|
||||
}
|
||||
|
||||
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
||||
const storagePath = `brand-logos/${brandId}/olathe-sweet-logo-dark.${ext}`;
|
||||
const key = storageKeys.brandLogo(brandId, `olathe-sweet-logo-dark.${ext}`);
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
||||
let url: string;
|
||||
try {
|
||||
const res = await uploadFile({
|
||||
bucket: BUCKETS.BRAND_LOGOS,
|
||||
key,
|
||||
body: buffer,
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
contentType: file.type,
|
||||
});
|
||||
url = res.url;
|
||||
} catch (e) {
|
||||
return { success: false, error: `Upload failed: ${(e as Error).message}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const saveRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||
@@ -190,7 +184,7 @@ export async function uploadOlatheSweetLogoDark(
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
p_olathe_sweet_logo_url_dark: publicUrl,
|
||||
p_olathe_sweet_logo_url_dark: url,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -199,7 +193,7 @@ export async function uploadOlatheSweetLogoDark(
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
|
||||
return { success: true, logoUrl: publicUrl };
|
||||
return { success: true, logoUrl: url };
|
||||
}
|
||||
|
||||
export type BrandSettings = {
|
||||
@@ -252,8 +246,8 @@ export type SaveBrandSettingsResult =
|
||||
| { success: false; error: string };
|
||||
|
||||
export async function getBrandSettings(brandId: string): Promise<GetBrandSettingsResult> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_brand_settings`,
|
||||
@@ -271,8 +265,8 @@ export async function getBrandSettings(brandId: string): Promise<GetBrandSetting
|
||||
|
||||
// Public version for storefront pages — uses slug, no auth required
|
||||
export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBrandSettingsResult & { wholesaleEnabled?: boolean | null }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_brand_settings_by_slug`,
|
||||
@@ -337,8 +331,8 @@ export async function saveBrandSettings(params: {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||
|
||||
@@ -64,8 +64,8 @@ export async function createOrder(
|
||||
brandId?: string,
|
||||
shippingAddress?: ShippingAddress
|
||||
): Promise<CheckoutResult> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// ── Calculate tax if brand collects tax ─────────────────────────────────
|
||||
let taxAmount = 0;
|
||||
@@ -150,8 +150,8 @@ export async function createOrder(
|
||||
// ── Cart Persistence ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function getServerCart(userId: string): Promise<CartItem[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
@@ -177,8 +177,8 @@ export async function mergeLocalCart(
|
||||
): Promise<{ merged: CartItem[] }> {
|
||||
if (!localCart || localCart.length === 0) return { merged: [] };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Fetch server cart
|
||||
let serverCart: CartItem[] = [];
|
||||
@@ -243,8 +243,8 @@ export async function mergeLocalCart(
|
||||
}
|
||||
|
||||
export async function clearServerCart(userId: string): Promise<void> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
try {
|
||||
await fetch(
|
||||
|
||||
@@ -59,8 +59,8 @@ export async function getCommunicationCampaigns(brandId?: string): Promise<ListC
|
||||
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_campaigns`,
|
||||
@@ -97,8 +97,8 @@ export async function upsertCampaign(params: {
|
||||
return { success: false, error: "Not authorized to operate on this brand's campaigns" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_campaign`,
|
||||
@@ -139,8 +139,8 @@ export async function deleteCampaign(campaignId: string, brandId?: string): Prom
|
||||
return { success: false, error: "Not authorized to delete this brand's campaigns" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_campaign`,
|
||||
@@ -159,8 +159,8 @@ export async function getCampaignById(campaignId: string, brandId?: string): Pro
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_campaign_by_id`,
|
||||
|
||||
@@ -109,8 +109,8 @@ export async function getContacts(params: {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_contacts`,
|
||||
@@ -172,8 +172,8 @@ export async function upsertContact(contact: {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_contact`,
|
||||
@@ -228,8 +228,8 @@ export async function importContactsBatch(params: {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/import_communication_contacts_batch`,
|
||||
@@ -271,8 +271,8 @@ export async function optOutContact(params: {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/opt_out_contact`,
|
||||
@@ -302,8 +302,8 @@ export async function deleteContact(id: string, brandId?: string): Promise<{ suc
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_contact`,
|
||||
@@ -353,8 +353,8 @@ export async function exportContacts(params: {
|
||||
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand context" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const allContacts: Contact[] = [];
|
||||
let offset = 0;
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
// Contact imports bucket
|
||||
const CONTACTS_BUCKET_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
||||
import { uploadFile, listFiles, BUCKETS, publicUrl, storageKeys } from "@/lib/storage";
|
||||
|
||||
export type UploadContactsResult =
|
||||
| { success: true; fileId: string; fileUrl: string; recordCount: number }
|
||||
@@ -17,57 +15,41 @@ export async function uploadContactsToBucket(
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
// Validate file type
|
||||
if (!file.name.endsWith(".csv")) {
|
||||
return { success: false, error: "Only CSV files are supported" };
|
||||
}
|
||||
|
||||
// For very large files (farmers with tons of data), check size
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB max
|
||||
if (file.size > maxSize) {
|
||||
return { success: false, error: "File too large. Max 50MB for large imports." };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// Generate unique path
|
||||
const timestamp = Date.now();
|
||||
const safeName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_");
|
||||
const path = `imports/${brandId}/${timestamp}-${safeName}`;
|
||||
const key = storageKeys.contactsImport(brandId, safeName);
|
||||
|
||||
// Upload to bucket
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/${CONTACTS_BUCKET_ID}/${path}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
...svcHeaders(supabaseKey),
|
||||
"Authorization": `Bearer ${supabaseKey}`,
|
||||
"Content-Type": "text/csv",
|
||||
"x-upsert": "false",
|
||||
},
|
||||
let url: string;
|
||||
try {
|
||||
const res = await uploadFile({
|
||||
bucket: BUCKETS.CONTACTS_IMPORTS,
|
||||
key,
|
||||
body: buffer,
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
contentType: "text/csv",
|
||||
});
|
||||
url = res.url;
|
||||
} catch (e) {
|
||||
return { success: false, error: `Upload failed: ${(e as Error).message}` };
|
||||
}
|
||||
|
||||
const fileUrl = `${supabaseUrl}/storage/v1/object/public/${CONTACTS_BUCKET_ID}/${path}`;
|
||||
const fileId = `${brandId}/${timestamp}`;
|
||||
|
||||
// Get rough row count from file size (approx 200 bytes per row)
|
||||
const fileId = key.split("/").slice(0, 2).join("/"); // brandId/timestamp
|
||||
const estimatedRows = Math.floor(file.size / 200);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fileId,
|
||||
fileUrl,
|
||||
fileUrl: url,
|
||||
recordCount: estimatedRows,
|
||||
};
|
||||
}
|
||||
@@ -84,10 +66,9 @@ export async function processBucketImport(
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Call RPC to process the file from bucket
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/process_contact_import_from_url`,
|
||||
{
|
||||
@@ -122,37 +103,20 @@ export async function listImportHistory(
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// List files in the imports folder for this brand
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/list/${CONTACTS_BUCKET_ID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
prefix: `imports/${brandId}/`,
|
||||
limit: limit,
|
||||
sortBy: { column: "created_at", order: "desc" },
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: "Failed to list imports" };
|
||||
try {
|
||||
const files = await listFiles(BUCKETS.CONTACTS_IMPORTS, `${brandId}/`);
|
||||
return {
|
||||
success: true,
|
||||
imports: files.slice(0, limit).map((f) => ({
|
||||
filename: f.key.split("/").pop() ?? "",
|
||||
size: f.size,
|
||||
createdAt: f.lastModified.toISOString(),
|
||||
url: publicUrl(BUCKETS.CONTACTS_IMPORTS, f.key),
|
||||
})),
|
||||
};
|
||||
} catch (e) {
|
||||
return { success: false, error: (e as Error).message };
|
||||
}
|
||||
|
||||
const files = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
imports: files.map((f: { name: string; metadata: { size: number }; created_at: string }) => ({
|
||||
filename: f.name.split("/").pop() ?? "",
|
||||
size: f.metadata?.size ?? 0,
|
||||
createdAt: f.created_at,
|
||||
url: `${supabaseUrl}/storage/v1/object/public/${CONTACTS_BUCKET_ID}/${f.name}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export type ImportHistoryItem = {
|
||||
@@ -160,4 +124,4 @@ export type ImportHistoryItem = {
|
||||
size: number;
|
||||
createdAt: string;
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,8 +33,8 @@ export async function getCommunicationSegments(
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_segments`,
|
||||
@@ -64,8 +64,8 @@ export async function upsertSegment(params: {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_segment`,
|
||||
@@ -99,8 +99,8 @@ export async function deleteSegment(
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_segment`,
|
||||
|
||||
@@ -21,8 +21,8 @@ export async function previewCampaignAudience(
|
||||
return null;
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/preview_campaign_audience`,
|
||||
@@ -86,8 +86,8 @@ export async function getMessageLogs(params: {
|
||||
return { success: false, error: "Not authorized to view these logs" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_message_logs`,
|
||||
@@ -128,8 +128,8 @@ export async function sendCampaign(campaignId: string, brandId?: string): Promis
|
||||
return { success: false, error: "Not authorized to send this brand's campaigns" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/send_campaign`,
|
||||
|
||||
@@ -24,8 +24,8 @@ export type UpsertSettingsResult = {
|
||||
};
|
||||
|
||||
export async function getCommunicationSettings(brandId: string): Promise<CommunicationSettings | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_settings`,
|
||||
@@ -57,8 +57,8 @@ export async function upsertCommunicationSettings(params: {
|
||||
return { success: false, error: "Not authorized to modify this brand's communication settings" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_settings`,
|
||||
|
||||
@@ -22,8 +22,8 @@ export async function sendStopBlast(params: {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/send_stop_blast`,
|
||||
|
||||
@@ -40,8 +40,8 @@ export async function getCommunicationTemplates(brandId?: string): Promise<{ suc
|
||||
return { success: true, templates: [] };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_templates`,
|
||||
@@ -70,8 +70,8 @@ export async function upsertTemplate(params: {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_template`,
|
||||
@@ -104,8 +104,8 @@ export async function getTemplateById(templateId: string): Promise<Template | nu
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_templates`,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ export async function getHarvestReachCampaigns(
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_campaigns`,
|
||||
@@ -82,8 +82,8 @@ export async function getCampaignAnalytics(
|
||||
if (!adminUser) return [];
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_campaign_analytics`,
|
||||
|
||||
@@ -17,8 +17,8 @@ export async function getProductsForSegmentPicker(
|
||||
if (!adminUser) return [];
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_products_for_segment_picker`,
|
||||
|
||||
@@ -75,8 +75,8 @@ export async function getHarvestReachSegments(
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_communication_segments`,
|
||||
@@ -110,8 +110,8 @@ export async function upsertHarvestReachSegment(params: {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_segment`,
|
||||
@@ -149,8 +149,8 @@ export async function deleteHarvestReachSegment(
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_segment`,
|
||||
@@ -180,8 +180,8 @@ export async function previewSegmentWithCustomers(
|
||||
return null;
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/preview_campaign_audience`,
|
||||
|
||||
@@ -23,8 +23,8 @@ export async function getStopsForSegmentPicker(
|
||||
if (!adminUser) return [];
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_stops_for_segment_picker`,
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function importOrdersBatch(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const results = { imported: 0, errors: [] as { row: number; error: string }[] };
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ export async function importProductsBatch(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/bulk_upsert_products`,
|
||||
|
||||
@@ -33,8 +33,8 @@ const MINIMAX_DEFAULT_BASE_URL = "https://api.minimax.io/v1";
|
||||
// ── Get AI provider settings ─────────────────────────────────────────────────────
|
||||
|
||||
export async function getAIProviderSettings(brandId: string): Promise<AIProviderSettings> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_ai_provider_settings`,
|
||||
@@ -75,8 +75,8 @@ export async function setAIProviderSettings(
|
||||
const current = await getAIProviderSettings(brandId);
|
||||
const merged = { ...current, ...settings };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||
|
||||
const payload = {
|
||||
provider: merged.provider,
|
||||
@@ -206,8 +206,8 @@ export async function getAIClient(brandId: string): Promise<{
|
||||
// ── Custom integrations ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function getCustomIntegrations(brandId: string): Promise<CustomIntegration[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_custom_integrations`,
|
||||
@@ -234,8 +234,8 @@ export async function upsertCustomIntegration(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (adminUser.brand_id && adminUser.brand_id !== brandId) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_custom_integration`,
|
||||
@@ -263,8 +263,8 @@ export async function deleteCustomIntegration(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (adminUser.brand_id && adminUser.brand_id !== brandId) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_custom_integration`,
|
||||
|
||||
@@ -24,8 +24,8 @@ export type SaveCredentialsResult =
|
||||
// ── Resend Credentials ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function getResendCredentials(brandId: string): Promise<ResendCredentials> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_resend_credentials`,
|
||||
@@ -66,8 +66,8 @@ export async function saveResendCredentials(
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Get current credentials to merge
|
||||
const current = await getResendCredentials(brandId);
|
||||
@@ -99,8 +99,8 @@ export async function saveResendCredentials(
|
||||
// ── Twilio Credentials ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function getTwilioCredentials(brandId: string): Promise<TwilioCredentials> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_twilio_credentials`,
|
||||
@@ -141,8 +141,8 @@ export async function saveTwilioCredentials(
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Get current credentials to merge
|
||||
const current = await getTwilioCredentials(brandId);
|
||||
|
||||
+16
-41
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
export type LoginWithPasswordResult =
|
||||
| { success: true; redirect: true }
|
||||
@@ -11,46 +11,21 @@ export async function loginWithPassword(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<LoginWithPasswordResult> {
|
||||
const cookieStore = await cookies();
|
||||
try {
|
||||
const hdrs = await headers();
|
||||
const result = await auth.api.signInEmail({
|
||||
body: { email, password },
|
||||
headers: hdrs,
|
||||
asResponse: false,
|
||||
});
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
if (!result?.user) {
|
||||
return { success: false, error: "Invalid credentials" };
|
||||
}
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
return { success: false, error: "Server misconfiguration." };
|
||||
return { success: true, redirect: true };
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : "Invalid credentials";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
|
||||
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
cookieStore.set(name, value, options);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error || !data.user) {
|
||||
return { success: false, error: error?.message || "Invalid credentials" };
|
||||
}
|
||||
|
||||
// Set the rc_auth_uid cookie that getAdminUser() reads
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
cookieStore.set("rc_auth_uid", data.user.id, {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: isProd,
|
||||
});
|
||||
|
||||
return { success: true, redirect: true };
|
||||
}
|
||||
|
||||
+4
-64
@@ -2,7 +2,6 @@
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { mockOrders, mockStops } from "@/lib/mock-data";
|
||||
|
||||
type AdminOrder = {
|
||||
id: string;
|
||||
@@ -106,51 +105,12 @@ type AdminOrderDetail = {
|
||||
}>;
|
||||
};
|
||||
|
||||
// Mock orders for UI review
|
||||
const mockAdminOrders: AdminOrder[] = mockOrders.map((o: any) => ({
|
||||
id: o.id,
|
||||
customer_name: o.customer_name,
|
||||
customer_email: o.customer_email,
|
||||
customer_phone: o.customer_phone,
|
||||
stop_id: o.stop_id,
|
||||
status: o.status,
|
||||
subtotal: o.subtotal,
|
||||
pickup_complete: o.pickup_complete,
|
||||
pickup_completed_at: o.pickup_completed_at,
|
||||
pickup_completed_by: null,
|
||||
created_at: o.created_at,
|
||||
payment_processor: o.payment_processor,
|
||||
stops: o.stops,
|
||||
order_items: o.order_items,
|
||||
}));
|
||||
|
||||
const mockAdminStops: AdminStop[] = mockStops.map((s: any) => ({
|
||||
id: s.id,
|
||||
city: s.city,
|
||||
state: s.state,
|
||||
date: s.date,
|
||||
location: s.location,
|
||||
brand_id: s.brand_id,
|
||||
}));
|
||||
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
|
||||
export async function getAdminOrders(): Promise<AdminOrdersResponse> {
|
||||
// Return mock data in mock mode
|
||||
if (useMockData) {
|
||||
return {
|
||||
success: true,
|
||||
orders: mockAdminOrders,
|
||||
stops: mockAdminStops,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
const brandId = adminUser?.brand_id ?? null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_admin_orders`,
|
||||
@@ -193,31 +153,11 @@ export async function getAdminPickedUpOrders(): Promise<AdminOrder[]> {
|
||||
}
|
||||
|
||||
export async function getAdminOrderDetail(orderId: string): Promise<AdminOrderDetail | null> {
|
||||
// Return mock order detail in mock mode
|
||||
if (useMockData) {
|
||||
const order = mockAdminOrders.find((o) => o.id === orderId);
|
||||
if (!order) return null;
|
||||
return {
|
||||
...order,
|
||||
discount_amount: null,
|
||||
tax_amount: order.subtotal * 0.08,
|
||||
tax_rate: 0.08,
|
||||
tax_location: "Colorado",
|
||||
discount_reason: null,
|
||||
internal_notes: null,
|
||||
payment_status: "paid",
|
||||
payment_transaction_id: `txn_mock_${orderId}`,
|
||||
refunded_amount: 0,
|
||||
refund_reason: null,
|
||||
refunds: [],
|
||||
};
|
||||
}
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
const brandId = adminUser?.brand_id ?? null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_admin_order_detail`,
|
||||
|
||||
@@ -53,8 +53,8 @@ export async function createAdminOrder(
|
||||
return { success: false, error: "At least one item is required" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Build items for RPC (match checkout shape)
|
||||
const rpcItems = input.items.map((i) => ({
|
||||
|
||||
@@ -22,8 +22,8 @@ export async function createRefund(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/refunds`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -36,8 +36,8 @@ export async function updateOrder(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const patchData: Record<string, unknown> = {};
|
||||
if (data.customer_name !== undefined) patchData.customer_name = data.customer_name;
|
||||
@@ -85,8 +85,8 @@ export async function updateOrderItem(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const patchData: Record<string, unknown> = {};
|
||||
if (data.quantity !== undefined) patchData.quantity = data.quantity;
|
||||
@@ -111,8 +111,8 @@ export async function deleteOrderItem(itemId: string): Promise<UpdateOrderResult
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/order_items?id=eq.${itemId}`, {
|
||||
method: "DELETE",
|
||||
|
||||
@@ -25,8 +25,8 @@ export type GetPaymentSettingsResult =
|
||||
| { success: false; error: string };
|
||||
|
||||
export async function getPaymentSettings(brandId: string): Promise<GetPaymentSettingsResult> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_payment_settings`,
|
||||
@@ -72,8 +72,8 @@ export async function savePaymentSettings(params: {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_payment_settings`,
|
||||
|
||||
+10
-10
@@ -28,9 +28,9 @@ export async function markPickupComplete(
|
||||
// brand_admin: verify the order belongs to their brand
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id) {
|
||||
const brandRes = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/orders?id=eq.${orderId}&select=stop_id,brand_id`,
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/rest/v1/orders?id=eq.${orderId}&select=stop_id,brand_id`,
|
||||
{
|
||||
headers: svcHeaders(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!),
|
||||
headers: svcHeaders(process.env.NEXT_PUBLIC_API_ANON_KEY!),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -52,9 +52,9 @@ export async function markPickupComplete(
|
||||
|
||||
if (!order.brand_id && order.stop_id) {
|
||||
const stopRes = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/stops?id=eq.${order.stop_id}&select=brand_id`,
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/rest/v1/stops?id=eq.${order.stop_id}&select=brand_id`,
|
||||
{
|
||||
headers: svcHeaders(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!),
|
||||
headers: svcHeaders(process.env.NEXT_PUBLIC_API_ANON_KEY!),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -69,11 +69,11 @@ export async function markPickupComplete(
|
||||
|
||||
// PATCH the order
|
||||
const patchRes = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/orders?id=eq.${orderId}`,
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/rest/v1/orders?id=eq.${orderId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
...svcHeaders(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!),
|
||||
...svcHeaders(process.env.NEXT_PUBLIC_API_ANON_KEY!),
|
||||
"Content-Type": "application/json",
|
||||
Prefer: "return=representation",
|
||||
},
|
||||
@@ -111,9 +111,9 @@ export async function markPickupComplete(
|
||||
// Emit pickup_completed event
|
||||
// Need brand_id — get it from the order we just patched
|
||||
const orderRes = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/orders?id=eq.${orderId}&select=brand_id`,
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/rest/v1/orders?id=eq.${orderId}&select=brand_id`,
|
||||
{
|
||||
headers: svcHeaders(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!),
|
||||
headers: svcHeaders(process.env.NEXT_PUBLIC_API_ANON_KEY!),
|
||||
}
|
||||
);
|
||||
if (orderRes.ok) {
|
||||
@@ -121,11 +121,11 @@ export async function markPickupComplete(
|
||||
const orderBrandId = Array.isArray(orderData) && orderData[0]?.brand_id;
|
||||
if (orderBrandId) {
|
||||
await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/rpc/record_pickup_completed_event`,
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/rest/v1/rpc/record_pickup_completed_event`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...svcHeaders(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!),
|
||||
...svcHeaders(process.env.NEXT_PUBLIC_API_ANON_KEY!),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ export async function deleteProduct(
|
||||
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_product`,
|
||||
@@ -63,8 +63,8 @@ async function logAuditEvent(event: {
|
||||
new_data: Record<string, unknown>;
|
||||
brand_id: string | null;
|
||||
}) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc.log_audit_event`,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getMockTableData } from "@/lib/mock-data";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
|
||||
export type CreateProductResult =
|
||||
| { success: true; id: string }
|
||||
| { success: false; error: string };
|
||||
@@ -31,27 +28,8 @@ export async function createProduct(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
if (useMockData) {
|
||||
const mockProducts = getMockTableData("products") as any[];
|
||||
const newId = `mock-prod-${Date.now()}`;
|
||||
const newProduct = {
|
||||
id: newId,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
price: data.price,
|
||||
type: data.type,
|
||||
is_active: data.active,
|
||||
image_url: data.image_url ?? null,
|
||||
is_taxable: data.is_taxable,
|
||||
pickup_type: data.pickup_type ?? "scheduled_stop",
|
||||
brand_id: brandId,
|
||||
};
|
||||
mockProducts.push(newProduct);
|
||||
return { success: true, id: newId };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/bulk_upsert_products`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getMockTableData } from "@/lib/mock-data";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
|
||||
export type UpdateProductResult =
|
||||
| { success: true }
|
||||
| { success: false; error: string };
|
||||
@@ -32,12 +29,8 @@ export async function updateProduct(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
if (useMockData) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/products?id=eq.${productId}`, {
|
||||
method: "PATCH",
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
// Product images bucket - UUID from Supabase
|
||||
const PRODUCT_IMAGES_BUCKET_ID = "80aa01da-ab4b-44f8-b6e7-700552457e18";
|
||||
import { uploadFile, BUCKETS, storageKeys } from "@/lib/storage";
|
||||
|
||||
export type UploadProductImageResult =
|
||||
| { success: true; imageUrl: string }
|
||||
@@ -25,42 +23,41 @@ export async function uploadProductImage(
|
||||
return { success: false, error: "File too large. Max 5MB." };
|
||||
}
|
||||
|
||||
const ext = file.type.split("/")[1] === "jpeg" ? "jpg" : file.type.split("/")[1];
|
||||
const path = `products/${productId}/${crypto.randomUUID()}.${ext}`;
|
||||
const rawExt = file.type.split("/")[1];
|
||||
const ext = rawExt === "jpeg" ? "jpg" : rawExt;
|
||||
const targetProductId = productId === "__NEW__" ? "new" : productId;
|
||||
const key = storageKeys.productImage(targetProductId, ext);
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/${PRODUCT_IMAGES_BUCKET_ID}/${path}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { ...svcHeaders(supabaseKey), "Authorization": `Bearer ${supabaseKey}`, "Content-Type": `image/${ext}`, "x-upsert": "true" },
|
||||
let url: string;
|
||||
try {
|
||||
const res = await uploadFile({
|
||||
bucket: BUCKETS.PRODUCT_IMAGES,
|
||||
key,
|
||||
body: buffer,
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
contentType: file.type,
|
||||
});
|
||||
url = res.url;
|
||||
} catch (e) {
|
||||
return { success: false, error: `Upload failed: ${(e as Error).message}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/${PRODUCT_IMAGES_BUCKET_ID}/${path}`;
|
||||
|
||||
// If productId is "__NEW__", we just upload and return the URL (caller handles saving it)
|
||||
if (productId === "__NEW__") {
|
||||
return { success: true, imageUrl: publicUrl };
|
||||
return { success: true, imageUrl: url };
|
||||
}
|
||||
|
||||
// Update product record with new image URL
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const patchRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ image_url: publicUrl }),
|
||||
body: JSON.stringify({ image_url: url }),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -68,7 +65,7 @@ export async function uploadProductImage(
|
||||
return { success: false, error: "Upload succeeded but failed to save image URL" };
|
||||
}
|
||||
|
||||
return { success: true, imageUrl: publicUrl };
|
||||
return { success: true, imageUrl: url };
|
||||
}
|
||||
|
||||
export async function deleteProductImage(
|
||||
@@ -77,8 +74,8 @@ export async function deleteProductImage(
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const patchRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
|
||||
@@ -94,4 +91,4 @@ export async function deleteProductImage(
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
export type DateRange = { start: string; end: string };
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const SUPABASE_PAT = process.env.SUPABASE_PAT!;
|
||||
|
||||
async function adminFetch(endpoint: string, options?: RequestInit) {
|
||||
|
||||
@@ -23,8 +23,8 @@ export async function toggleBrandFeature(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/set_brand_feature`,
|
||||
|
||||
@@ -16,8 +16,8 @@ export async function updateShippingStatus(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/update_shipping_order`,
|
||||
@@ -70,8 +70,8 @@ export async function getShippingOrders(): Promise<GetShippingOrdersResult> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_shipping_orders`,
|
||||
|
||||
@@ -58,8 +58,8 @@ async function getFedExTokenForCreate(
|
||||
brandId: string | null,
|
||||
adminUserId: string | null
|
||||
): Promise<{ accessToken: string } | { error: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Try to get from shipping_settings
|
||||
const settingsRes = await fetch(
|
||||
@@ -88,8 +88,8 @@ async function getFedExTokenForCreate(
|
||||
}
|
||||
|
||||
async function isShipmentPerishable(orderId: string): Promise<boolean> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_order_items_perishable?p_order_id=${encodeURIComponent(orderId)}`,
|
||||
@@ -128,8 +128,8 @@ export async function createFedExShipment(
|
||||
return { success: false, error: "Not authorized to create shipments" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Fetch order
|
||||
const orderRes = await fetch(
|
||||
|
||||
@@ -95,8 +95,8 @@ async function isOrderPerishable(
|
||||
orderId: string,
|
||||
brandId: string | null
|
||||
): Promise<boolean> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_order_items_perishable?p_order_id=${encodeURIComponent(orderId)}`,
|
||||
@@ -151,8 +151,8 @@ export async function getFedExRates(
|
||||
return { success: false, error: "Not authorized to view shipping rates" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Fetch order + brand + address
|
||||
const orderRes = await fetch(
|
||||
|
||||
@@ -77,8 +77,8 @@ async function getFedExToken(apiKey: string, apiSecret: string, useProduction: b
|
||||
// ── Get Settings ─────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getShippingSettings(brandId: string): Promise<GetShippingSettingsResult> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/shipping_settings?brand_id=eq.${encodeURIComponent(brandId)}&limit=1`,
|
||||
@@ -115,8 +115,8 @@ export async function saveShippingSettings(params: {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Get existing settings to get ID for update
|
||||
const existing = await fetch(
|
||||
|
||||
@@ -110,8 +110,8 @@ export async function syncInventoryToSquare(
|
||||
const errors: string[] = [];
|
||||
const synced = 0;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Build product name → quantity map
|
||||
const itemQtyMap = new Map(items.map((i) => [i.productId, i.quantity]));
|
||||
@@ -177,8 +177,8 @@ export async function syncInventoryFromSquare(brandId: string): Promise<SyncResu
|
||||
|
||||
const errors: string[] = [];
|
||||
const synced = 0;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
try {
|
||||
// Fetch Square inventory counts for the location
|
||||
|
||||
@@ -85,8 +85,8 @@ export async function syncOrdersFromSquare(brandId: string): Promise<SyncResult>
|
||||
|
||||
const errors: string[] = [];
|
||||
let synced = 0;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Determine sync start time — last sync or 30 days ago
|
||||
const since = settings.square_last_sync_at
|
||||
|
||||
@@ -136,8 +136,8 @@ export async function syncProductsToSquare(brandId: string): Promise<SyncResult>
|
||||
|
||||
try {
|
||||
// Fetch wholesale products via RPC (avoids rc_product_id bug in direct query)
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const rpcRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
||||
@@ -247,8 +247,8 @@ export async function syncProductsFromSquare(brandId: string): Promise<SyncResul
|
||||
|
||||
const errors: string[] = [];
|
||||
let synced = 0;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
let cursor: string | undefined;
|
||||
do {
|
||||
|
||||
@@ -65,8 +65,8 @@ export async function getSyncLog(brandId: string): Promise<{
|
||||
return { success: false, logs: [] };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/square_sync_log?brand_id=eq.${brandId}&order=created_at.desc&limit=10`,
|
||||
|
||||
@@ -30,11 +30,11 @@ export async function createStopsBatch(
|
||||
return { success: false, created: 0, error: "No brand selected" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
// Use anon key — admin_create_stops_batch is SECURITY DEFINER so RLS is
|
||||
// bypassed. This fixes the prior 42501 RLS violation that came from doing
|
||||
// direct REST inserts against the RLS-blocked `stops` table.
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const rows = stops.map((s) => ({
|
||||
city: s.city,
|
||||
@@ -74,8 +74,8 @@ export async function publishStop(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_stops) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/stops?id=eq.${stopId}`, {
|
||||
method: "PATCH",
|
||||
@@ -105,8 +105,8 @@ export type StopForSitemap = {
|
||||
};
|
||||
|
||||
export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Get all active stops with their brand slug
|
||||
const response = await fetch(
|
||||
@@ -127,7 +127,7 @@ export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
||||
* Fetch active stops for a brand by slug.
|
||||
* Public action used by the storefront stop pages (e.g. /tuxedo/stops,
|
||||
* /indian-river-direct/stops) — replaces the previous client-side
|
||||
* `supabase.from("stops")` query so supabase-js no longer ships to
|
||||
* `api.from("stops")` query so api-js no longer ships to
|
||||
* the browser for those routes.
|
||||
*
|
||||
* Cached at the edge for 5 minutes; invalidated by `revalidateTag('stops')`
|
||||
@@ -150,8 +150,8 @@ export async function getPublicStopsForBrand(
|
||||
): Promise<PublicStop[]> {
|
||||
if (!brandSlug) return [];
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_public_stops_for_brand`,
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { getMockTableData } from "@/lib/mock-data";
|
||||
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
|
||||
export type CreateStopResult =
|
||||
| { success: true; id: string }
|
||||
@@ -33,18 +30,13 @@ export async function createStop(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
if (useMockData) {
|
||||
const mockStops = getMockTableData("stops") as Array<{ id: string }>;
|
||||
return { success: true, id: `mock-stop-${Date.now()}` };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
// Preferred path: anon key + SECURITY DEFINER RPC (bypasses RLS, works even if
|
||||
// service role key is absent at runtime). See:
|
||||
// supabase/migrations/202_fix_admin_create_stop.sql
|
||||
// api/migrations/202_fix_admin_create_stop.sql
|
||||
// scripts/apply-admin-create-stop.js (run this locally with the keys you have)
|
||||
// supabase/ADMIN_CREATE_STOP_FIX.sql (exact prompt SQL for SQL editor)
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
// api/ADMIN_CREATE_STOP_FIX.sql (exact prompt SQL for SQL editor)
|
||||
const anonKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/admin_create_stop`, {
|
||||
method: "POST",
|
||||
@@ -114,7 +106,7 @@ export async function createStop(
|
||||
" 1. On a machine that can reach your database (normal laptop usually works):\n" +
|
||||
" node scripts/apply-admin-create-stop.js\n" +
|
||||
" 2. Or: npm run migrate:one 202\n" +
|
||||
" 3. Or apply supabase/migrations/202_fix_admin_create_stop.sql via psql / Supabase CLI with --db-url.\n\n" +
|
||||
" 3. Or apply api/migrations/202_fix_admin_create_stop.sql via psql / Supabase CLI with --db-url.\n\n" +
|
||||
"After it succeeds, restart your dev server and Add New Stop will work.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { logAuditEvent } from "@/actions/audit";
|
||||
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
|
||||
export type UpdateStopResult =
|
||||
| { success: true }
|
||||
| { success: false; error: string };
|
||||
@@ -33,12 +31,8 @@ export async function updateStop(
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
if (useMockData) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const slug = `${data.city.toLowerCase().replace(/\s+/g, "-")}-${data.date}`;
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ export async function getStripeConnectStatus(brandId: string): Promise<{
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { is_connected: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Get brand's payment settings
|
||||
const res = await fetch(
|
||||
@@ -183,8 +183,8 @@ export async function saveStripeConnectAccount(brandId: string, accountId: strin
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
// Save to payment_settings via RPC
|
||||
const res = await fetch(
|
||||
@@ -220,8 +220,8 @@ export async function disconnectStripeConnect(brandId: string): Promise<{
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/disconnect_stripe_connect`,
|
||||
|
||||
+2
-2
@@ -102,8 +102,8 @@ export async function calculateOrderTax(params: {
|
||||
}
|
||||
|
||||
async function getBrandTaxSettings(brandId: string): Promise<BrandTaxSettings | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_brand_settings`,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
function rpcBody(body: Record<string, unknown>) {
|
||||
return JSON.stringify(body);
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { mockWorkers, mockTasks, mockTimeEntries } from "@/lib/mock-data";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// Mock mode flag - only enabled when explicitly set
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
function rpcBody(body: Record<string, unknown>) {
|
||||
return JSON.stringify(body);
|
||||
@@ -62,20 +58,6 @@ export type TimeSummary = {
|
||||
// ── Workers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getTimeTrackingWorkers(brandId: string): Promise<TimeWorker[]> {
|
||||
if (useMockData) {
|
||||
return mockWorkers.map(w => ({
|
||||
id: w.id,
|
||||
brand_id: brandId,
|
||||
name: w.name,
|
||||
role: w.role,
|
||||
lang: w.language,
|
||||
pin: "0000",
|
||||
active: w.is_active,
|
||||
last_used_at: null,
|
||||
created_at: new Date().toISOString(),
|
||||
worker_number: null,
|
||||
}));
|
||||
}
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_workers`,
|
||||
{
|
||||
@@ -182,16 +164,6 @@ export async function deleteTimeWorker(workerId: string): Promise<{ success: boo
|
||||
// ── Tasks ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getTimeTrackingTasks(brandId: string, activeOnly = false): Promise<TimeTask[]> {
|
||||
if (useMockData) {
|
||||
return mockTasks.map(t => ({
|
||||
id: t.id,
|
||||
name: t.name_en,
|
||||
name_es: t.name_es,
|
||||
unit: t.unit,
|
||||
active: true,
|
||||
sort_order: t.sort_order,
|
||||
}));
|
||||
}
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_tasks`,
|
||||
{
|
||||
@@ -282,30 +254,6 @@ export async function getWorkerTimeLogs(
|
||||
offset?: number;
|
||||
} = {}
|
||||
): Promise<TimeLog[]> {
|
||||
if (useMockData) {
|
||||
// Filter by worker, task, date range
|
||||
let entries = mockTimeEntries.filter(e => e.brand_id === brandId);
|
||||
if (options.workerId) entries = entries.filter(e => e.worker_id === options.workerId);
|
||||
|
||||
return entries.map(e => {
|
||||
const worker = mockWorkers.find(w => w.id === e.worker_id);
|
||||
const task = mockTasks.find(t => t.id === e.task_id);
|
||||
return {
|
||||
id: e.id,
|
||||
worker_id: e.worker_id,
|
||||
worker_name: worker?.name ?? "Unknown",
|
||||
task_id: e.task_id,
|
||||
task_name: task?.name_en ?? "Unknown",
|
||||
clock_in: `${e.date}T09:00:00Z`,
|
||||
clock_out: `${e.date}T${9 + e.hours}:00:00Z`,
|
||||
lunch_break_minutes: 0,
|
||||
notes: null,
|
||||
submitted_via: "web",
|
||||
total_minutes: Math.round(e.hours * 60),
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_worker_time_logs`,
|
||||
{
|
||||
@@ -380,39 +328,6 @@ export async function getTimeTrackingSummary(
|
||||
start: string,
|
||||
end: string
|
||||
): Promise<TimeSummary> {
|
||||
if (useMockData) {
|
||||
const entries = mockTimeEntries.filter(e => e.brand_id === brandId);
|
||||
|
||||
// Calculate by worker
|
||||
const workerMap = new Map<string, { id: string; name: string; total_hours: number; entry_count: number }>();
|
||||
entries.forEach(e => {
|
||||
const worker = mockWorkers.find(w => w.id === e.worker_id);
|
||||
const existing = workerMap.get(e.worker_id) || { id: e.worker_id, name: worker?.name ?? "Unknown", total_hours: 0, entry_count: 0 };
|
||||
existing.total_hours += e.hours;
|
||||
existing.entry_count += 1;
|
||||
workerMap.set(e.worker_id, existing);
|
||||
});
|
||||
|
||||
// Calculate by task
|
||||
const taskMap = new Map<string, { id: string; name: string; name_es: string | null; total_hours: number; entry_count: number }>();
|
||||
entries.forEach(e => {
|
||||
const task = mockTasks.find(t => t.id === e.task_id);
|
||||
const existing = taskMap.get(e.task_id) || { id: e.task_id, name: task?.name_en ?? "Unknown", name_es: task?.name_es ?? null, total_hours: 0, entry_count: 0 };
|
||||
existing.total_hours += e.hours;
|
||||
existing.entry_count += 1;
|
||||
taskMap.set(e.task_id, existing);
|
||||
});
|
||||
|
||||
return {
|
||||
by_worker: Array.from(workerMap.values()),
|
||||
by_task: Array.from(taskMap.values()),
|
||||
totals: {
|
||||
entry_count: entries.length,
|
||||
total_hours: entries.reduce((sum, e) => sum + e.hours, 0),
|
||||
open_count: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_summary`,
|
||||
{
|
||||
@@ -447,26 +362,6 @@ export type TimeTrackingSettings = {
|
||||
};
|
||||
|
||||
export async function getTimeTrackingSettings(brandId: string): Promise<TimeTrackingSettings | null> {
|
||||
if (useMockData) {
|
||||
return {
|
||||
id: "settings-mock",
|
||||
brand_id: brandId,
|
||||
pay_period_start_day: 0,
|
||||
pay_period_length_days: 7,
|
||||
daily_overtime_threshold: 8,
|
||||
weekly_overtime_threshold: 40,
|
||||
overtime_multiplier: 1.5,
|
||||
overtime_notifications: true,
|
||||
notification_emails: ["admin@tuxedocorn.com"],
|
||||
notification_sms_numbers: [],
|
||||
enable_daily_alerts: true,
|
||||
enable_weekly_alerts: true,
|
||||
daily_alert_threshold: 8,
|
||||
weekly_alert_threshold: 40,
|
||||
send_end_of_period_summary: true,
|
||||
brand_name: "Tuxedo Corn",
|
||||
};
|
||||
}
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_settings`,
|
||||
{
|
||||
@@ -552,10 +447,6 @@ export async function getTimeTrackingNotificationLog(
|
||||
brandId: string,
|
||||
limit = 100
|
||||
): Promise<NotificationLogEntry[]> {
|
||||
if (useMockData) {
|
||||
// Return empty log in mock mode
|
||||
return [];
|
||||
}
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_notification_log`,
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
export type OvertimeCheckResult = {
|
||||
sent: boolean;
|
||||
|
||||
@@ -50,8 +50,8 @@ export async function createWaterHeadgate(brandId: string, name: string, unit: s
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; // prefer service for admin muts
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!; // prefer service for admin muts
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/create_water_headgate`,
|
||||
@@ -81,8 +81,8 @@ export async function updateWaterHeadgate(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/update_water_headgate`,
|
||||
@@ -111,8 +111,8 @@ export async function updateWaterHeadgate(
|
||||
// ── Irrigator Admin ─────────────────────────────────────────
|
||||
|
||||
export async function getWaterIrrigators(brandId: string): Promise<Irrigator[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_users`,
|
||||
@@ -142,8 +142,8 @@ export async function createWaterUser(
|
||||
role: "irrigator" | "water_admin",
|
||||
lang: string = "en"
|
||||
): Promise<{ success: boolean; user?: Irrigator; pin?: string; error?: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/create_water_user`,
|
||||
@@ -172,8 +172,8 @@ export async function updateWaterIrrigator(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/update_water_user`,
|
||||
@@ -205,8 +205,8 @@ export async function resetWaterIrrigatorPin(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/reset_water_user_pin`,
|
||||
@@ -232,8 +232,8 @@ export async function deleteWaterUser(userId: string): Promise<{ success: boolea
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_water_user`,
|
||||
@@ -259,8 +259,8 @@ export async function deleteWaterHeadgate(headgateId: string): Promise<{ success
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_water_headgate`,
|
||||
@@ -308,8 +308,8 @@ export async function deleteWaterHeadgate(headgateId: string): Promise<{ success
|
||||
// ── Entries ────────────────────────────────────────────────
|
||||
|
||||
export async function getWaterEntries(brandId: string, limit = 50): Promise<WaterEntry[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_entries`,
|
||||
@@ -326,8 +326,8 @@ export async function getWaterEntries(brandId: string, limit = 50): Promise<Wate
|
||||
}
|
||||
|
||||
export async function getWaterHeadgatesAdmin(brandId: string): Promise<Headgate[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_headgates_admin`,
|
||||
@@ -348,8 +348,8 @@ export async function regenerateHeadgateToken(headgateId: string): Promise<{ suc
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/regenerate_headgate_token`,
|
||||
@@ -379,8 +379,8 @@ export async function updateWaterEntry(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/update_water_entry`,
|
||||
@@ -411,8 +411,8 @@ export async function deleteWaterEntry(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_water_entry`,
|
||||
@@ -434,8 +434,8 @@ export async function deleteWaterEntry(
|
||||
}
|
||||
|
||||
export async function getWaterEntryById(entryId: string): Promise<WaterEntry | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_entry_by_id`,
|
||||
@@ -481,8 +481,8 @@ export type WaterDisplaySummary = {
|
||||
};
|
||||
|
||||
export async function getWaterDisplaySummary(brandId: string): Promise<WaterDisplaySummary | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_display_summary`,
|
||||
@@ -511,8 +511,8 @@ export type AlertLogEntry = {
|
||||
};
|
||||
|
||||
export async function getWaterAlertLog(brandId: string, limit = 50): Promise<AlertLogEntry[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_alert_log`,
|
||||
|
||||
@@ -35,8 +35,8 @@ type HeadgatesResult = {
|
||||
};
|
||||
|
||||
export async function getWaterHeadgates(brandId: string, activeOnly = false): Promise<Headgate[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_headgates`,
|
||||
@@ -53,8 +53,8 @@ export async function getWaterHeadgates(brandId: string, activeOnly = false): Pr
|
||||
}
|
||||
|
||||
export async function verifyWaterPin(brandId: string, pin: string): Promise<VerifyPinResult> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/verify_water_pin`,
|
||||
@@ -134,8 +134,8 @@ export async function submitWaterEntry(
|
||||
return { success: false, error: "Not logged in" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/submit_water_entry`,
|
||||
@@ -236,8 +236,8 @@ export async function getWaterAdminSession(): Promise<{ user_id: string; name: s
|
||||
|
||||
if (!sessionId) return null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Use get_water_admin_session RPC (SECURITY DEFINER) to avoid direct water_sessions + water_users JOIN
|
||||
const response = await fetch(
|
||||
|
||||
@@ -14,8 +14,8 @@ export type WaterAdminSettings = {
|
||||
};
|
||||
|
||||
export async function getWaterAdminSettings(brandId: string): Promise<WaterAdminSettings | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_admin_settings`,
|
||||
@@ -39,8 +39,8 @@ export async function saveWaterAdminSettings(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
let pinHash: string | null = null;
|
||||
if (settings.pin) {
|
||||
@@ -88,8 +88,8 @@ export async function verifyWaterAdminPin(
|
||||
brandId: string,
|
||||
pin: string
|
||||
): Promise<{ success: boolean; session_id?: string; error?: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const settingsRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_admin_settings`,
|
||||
|
||||
+42
-111
@@ -1,8 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
export type WholesaleLoginResult =
|
||||
| { success: true; token: string; userId: string; customerId: string }
|
||||
@@ -12,121 +11,53 @@ export async function wholesaleLoginAction(formData: FormData): Promise<Wholesal
|
||||
const email = formData.get("email") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
try {
|
||||
const hdrs = await headers();
|
||||
const result = await auth.api.signInEmail({
|
||||
body: { email, password },
|
||||
headers: hdrs,
|
||||
asResponse: false,
|
||||
});
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const request = new NextRequest("http://localhost/wholesale/login", {
|
||||
headers: new Headers(),
|
||||
});
|
||||
const response = NextResponse.next({ request });
|
||||
|
||||
const { createServerClient } = await import("@supabase/ssr");
|
||||
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(cookiesToSet, headers) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
response.cookies.set(name, value, options);
|
||||
});
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
response.headers.set(key, value);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error || !data.user) {
|
||||
return { success: false, error: error?.message ?? "Invalid credentials" };
|
||||
}
|
||||
|
||||
const { data: sessionData } = await supabase.auth.getSession();
|
||||
const token = sessionData?.session?.access_token;
|
||||
|
||||
if (!token) {
|
||||
return { success: false, error: "No session returned from auth" };
|
||||
}
|
||||
|
||||
// Find the wholesale customer record for this user
|
||||
const customerRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_by_user`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...svcHeaders(supabaseAnonKey),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
p_brand_id: "placeholder", // will use any-brand lookup below
|
||||
p_user_id: data.user.id,
|
||||
}),
|
||||
if (!result?.user) {
|
||||
return { success: false, error: "Invalid credentials" };
|
||||
}
|
||||
);
|
||||
|
||||
// If no brand_id known, try all brands — just use first active one found
|
||||
// For now, set the cookie with user_id and a placeholder; portal will resolve proper customer
|
||||
response.cookies.set("wholesale_session", JSON.stringify({
|
||||
user_id: data.user.id,
|
||||
access_token: token,
|
||||
}), {
|
||||
path: "/",
|
||||
maxAge: 3600 * 24 * 7,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: false,
|
||||
});
|
||||
const cookieStore = await cookies();
|
||||
// Better Auth sets its own session cookie (rc_session_token).
|
||||
// Mark wholesale session for portal routing.
|
||||
cookieStore.set("wholesale_session", JSON.stringify({
|
||||
user_id: result.user.id,
|
||||
}), {
|
||||
path: "/",
|
||||
maxAge: 3600 * 24 * 7,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: false,
|
||||
});
|
||||
|
||||
// Also set the standard auth token for RPC calls
|
||||
response.cookies.set("sb-wnzkhezyhnfzhkhiflrp-auth-token", token, {
|
||||
path: "/",
|
||||
maxAge: 3600 * 24 * 7,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: false,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
userId: data.user.id,
|
||||
customerId: "pending", // resolved by portal page on load
|
||||
};
|
||||
return {
|
||||
success: true,
|
||||
token: "better-auth-session", // session lives in cookie
|
||||
userId: result.user.id,
|
||||
customerId: "pending", // resolved by portal page on load
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : "Invalid credentials";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function wholesaleLogoutAction() {
|
||||
try {
|
||||
const hdrs = await headers();
|
||||
await auth.api.signOut({ headers: hdrs });
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const request = new NextRequest("http://localhost/wholesale/portal", {
|
||||
headers: new Headers(),
|
||||
});
|
||||
const response = NextResponse.next({ request });
|
||||
|
||||
response.cookies.delete("wholesale_session");
|
||||
|
||||
const { createServerClient } = await import("@supabase/ssr");
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(cookiesToSet, headers) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
response.cookies.set(name, value, options);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await supabase.auth.signOut();
|
||||
cookieStore.delete("wholesale_session");
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ export async function registerWholesaleCustomer(params: {
|
||||
email: string;
|
||||
phone?: string;
|
||||
}): Promise<{ success: boolean; error?: string; requiresApproval?: boolean }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/register_wholesale_customer`,
|
||||
@@ -54,8 +54,8 @@ export async function getPendingWholesaleRegistrations(brandId: string) {
|
||||
if (!adminUser) return [];
|
||||
if (!adminUser.can_manage_orders) return [];
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_pending_wholesale_registrations`,
|
||||
@@ -85,8 +85,8 @@ export async function approveWholesaleRegistration(
|
||||
return { success: false, error: "Not authorized to operate on this brand" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/approve_wholesale_registration`,
|
||||
@@ -128,8 +128,8 @@ export async function getWholesaleCustomerByUser(
|
||||
role: string;
|
||||
brand_id: string;
|
||||
} | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_by_user`,
|
||||
@@ -162,8 +162,8 @@ export async function getWholesaleCustomer(
|
||||
role: string;
|
||||
brand_id: string;
|
||||
} | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/wholesale_customers?id=eq.${customerId}&select=id,user_id,company_name,contact_name,email,phone,account_status,role,brand_id`,
|
||||
@@ -179,8 +179,8 @@ export async function getWholesaleCustomer(
|
||||
|
||||
export async function getWholesaleProducts(brandId: string) {
|
||||
// Uses SECURITY DEFINER RPC — no auth required for admins, anon key works for customers too
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
||||
@@ -217,8 +217,8 @@ export async function submitWholesaleOrder(params: {
|
||||
orderTotal?: number;
|
||||
idempotent?: boolean;
|
||||
}> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
// Generate UUID at the start of checkout — before RPC call for true idempotency
|
||||
const checkoutSessionId = params.checkoutSessionId ?? crypto.randomUUID();
|
||||
@@ -284,8 +284,8 @@ export async function submitWholesaleOrder(params: {
|
||||
|
||||
export async function enqueueWholesaleWebhookForOrderCreated(orderId: string, brandId: string, customerId: string, invoiceNumber: string | null, subtotal: number) {
|
||||
try {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
@@ -341,8 +341,8 @@ export type WholesalePricingOverride = {
|
||||
};
|
||||
|
||||
export async function getWholesaleCustomerPricing(customerId: string): Promise<WholesalePricingOverride[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_pricing`,
|
||||
@@ -365,8 +365,8 @@ export async function upsertWholesaleCustomerPricing(params: {
|
||||
productId: string;
|
||||
customUnitPrice: number;
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_customer_pricing`,
|
||||
@@ -392,8 +392,8 @@ export async function deleteWholesaleCustomerPricing(params: {
|
||||
customerId: string;
|
||||
productId: string;
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_customer_pricing`,
|
||||
@@ -415,8 +415,8 @@ export async function deleteWholesaleCustomerPricing(params: {
|
||||
}
|
||||
|
||||
export async function getWholesaleCustomerOrders(customerId: string): Promise<WholesaleCustomerOrder[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_orders`,
|
||||
|
||||
+52
-52
@@ -187,8 +187,8 @@ export async function getWholesaleOrders(brandId?: string): Promise<WholesaleOrd
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_orders`,
|
||||
@@ -211,8 +211,8 @@ export async function getWholesalePickupOrders(brandId?: string): Promise<Wholes
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_pickup_orders`,
|
||||
@@ -259,8 +259,8 @@ export async function markWholesaleOrderFulfilled(orderId: string, brandId?: str
|
||||
const { brandId: resolved, error } = enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/mark_wholesale_order_fulfilled`,
|
||||
@@ -282,8 +282,8 @@ export async function markWholesaleOrderFulfilled(orderId: string, brandId?: str
|
||||
}
|
||||
|
||||
async function enqueueWholesaleWebhookForOrderFulfilled(orderId: string, brandId?: string) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
@@ -308,8 +308,8 @@ export async function updateWholesaleOrderStatus(
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/update_wholesale_order_status`,
|
||||
@@ -335,8 +335,8 @@ export async function deleteWholesaleOrder(orderId: string, brandId?: string): P
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_order`,
|
||||
@@ -362,8 +362,8 @@ export async function deleteWholesaleCustomer(customerId: string, brandId?: stri
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_customer`,
|
||||
@@ -391,8 +391,8 @@ export async function deleteWholesaleProduct(productId: string, brandId?: string
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_product`,
|
||||
@@ -419,8 +419,8 @@ export async function getWholesaleCustomers(brandId?: string): Promise<Wholesale
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customers`,
|
||||
@@ -461,8 +461,8 @@ export async function saveWholesaleCustomer(params: {
|
||||
const { error } = enforceBrandScope(adminUser, params.brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_customer`,
|
||||
@@ -503,8 +503,8 @@ export async function getWholesaleProducts(brandId?: string): Promise<WholesaleP
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
||||
@@ -548,8 +548,8 @@ export async function saveWholesaleProduct(params: {
|
||||
const { error } = enforceBrandScope(adminUser, params.brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_product`,
|
||||
@@ -593,8 +593,8 @@ export async function getWholesaleSettings(brandId?: string): Promise<WholesaleS
|
||||
if (!adminUser) return null;
|
||||
const bid = brandId ?? adminUser.brand_id ?? null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_settings`,
|
||||
@@ -635,8 +635,8 @@ export async function saveWholesaleSettings(params: {
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_settings`,
|
||||
@@ -683,8 +683,8 @@ export async function recordWholesaleDeposit(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/record_wholesale_deposit`,
|
||||
@@ -716,8 +716,8 @@ export async function recordWholesaleDeposit(
|
||||
}
|
||||
|
||||
async function enqueueWholesaleWebhookForDepositRecorded(orderId: string, amount: number, brandId?: string) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
@@ -739,8 +739,8 @@ export async function bulkFulfillWholesaleOrders(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/bulk_fulfill_wholesale_orders`,
|
||||
@@ -774,8 +774,8 @@ export async function bulkRecordWholesaleDeposit(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/bulk_record_wholesale_deposit`,
|
||||
@@ -824,8 +824,8 @@ export type WholesaleNotification = {
|
||||
export async function getWholesaleNotificationStats(
|
||||
brandId: string
|
||||
): Promise<{ pending: number; sent: number; failed: number; total: number }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_notification_stats`,
|
||||
@@ -851,8 +851,8 @@ export async function getWholesalePendingNotifications(
|
||||
if (!adminUser) return [];
|
||||
if (!adminUser.can_manage_orders) return [];
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_pending_notifications`,
|
||||
@@ -878,8 +878,8 @@ export async function markWholesaleNotificationSent(
|
||||
if (!adminUser) return { success: false };
|
||||
if (!adminUser.can_manage_orders) return { success: false };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/mark_wholesale_notification_sent`,
|
||||
@@ -907,8 +907,8 @@ export async function enqueueWholesaleNotification(params: {
|
||||
bodyHtml?: string;
|
||||
bodyText?: string;
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_notification`,
|
||||
@@ -949,8 +949,8 @@ export type WebhookSettings = {
|
||||
};
|
||||
|
||||
export async function getWebhookSettings(brandId: string): Promise<WebhookSettings | null> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/wholesale_webhook_settings?brand_id=eq.${brandId}&select=*`,
|
||||
@@ -974,8 +974,8 @@ export async function saveWebhookSettings(params: {
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const serviceKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
brand_id: params.brandId,
|
||||
@@ -1007,8 +1007,8 @@ export async function enqueueWholesaleWebhook(
|
||||
payload: Record<string, unknown> | null = null,
|
||||
brandId?: string
|
||||
): Promise<{ success: boolean; logId?: string }> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`,
|
||||
@@ -1041,8 +1041,8 @@ export async function getRecentWebhookActivity(brandId: string, limit = 10): Pro
|
||||
created_at: string;
|
||||
response: string | null;
|
||||
}>> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/wholesale_sync_log?brand_id=eq.${brandId}&order=created_at.desc&limit=${limit}`,
|
||||
|
||||
@@ -11,8 +11,8 @@ export default async function DebugAuthPage() {
|
||||
let adminUsersResult: string | null = null;
|
||||
|
||||
if (rcAuthUid) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const serviceKey = process.env.POSTGREST_SERVICE_KEY;
|
||||
if (supabaseUrl && serviceKey) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { AdminUserRow } from "@/actions/admin/users";
|
||||
import { logUserActivity } from "@/actions/admin/audit";
|
||||
import { updateAdminProfileAction } from "@/actions/admin/profile";
|
||||
|
||||
type ProfilePageProps = {
|
||||
currentUser: AdminUserRow;
|
||||
@@ -26,13 +27,13 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { error: rpcError } = await supabase.rpc("update_admin_user", {
|
||||
p_id: currentUser.id,
|
||||
p_display_name: displayName || null,
|
||||
p_phone_number: phoneNumber || null,
|
||||
});
|
||||
if (rpcError) {
|
||||
setError(rpcError.message);
|
||||
const result = await updateAdminProfileAction(
|
||||
currentUser.id,
|
||||
displayName || null,
|
||||
phoneNumber || null
|
||||
);
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
return;
|
||||
}
|
||||
await logUserActivity({
|
||||
@@ -51,11 +52,11 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
||||
setChangingEmail(true);
|
||||
setEmailError(null);
|
||||
try {
|
||||
const { error: updateError } = await supabase.auth.updateUser({
|
||||
email: newEmail,
|
||||
const { error: updateError } = await authClient.changeEmail({
|
||||
newEmail: newEmail,
|
||||
});
|
||||
if (updateError) {
|
||||
setEmailError(updateError.message);
|
||||
setEmailError(updateError.message ?? "Failed to change email");
|
||||
return;
|
||||
}
|
||||
await logUserActivity({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAdminOrders } from "@/actions/orders";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -33,7 +33,7 @@ export default async function AdminOrdersPage() {
|
||||
// Fetch active products for this brand (for admin "New Order" item picker)
|
||||
let brandProducts: Array<{ id: string; name: string; price: number; type?: string | null; active?: boolean }> = [];
|
||||
try {
|
||||
let prodQuery = supabase
|
||||
let prodQuery = api
|
||||
.from("products")
|
||||
.select("id, name, price, type, active")
|
||||
.eq("active", true)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { isFeatureEnabled } from "@/lib/feature-flags";
|
||||
import { getBillingOverview } from "@/actions/billing/billing-overview";
|
||||
@@ -30,7 +30,7 @@ export default async function AdminPage() {
|
||||
// (0/0/0) usage values and contradicts the billing page's "Products 1/25".
|
||||
let dashboardBrandId: string | null = adminUser?.brand_id ?? null;
|
||||
if (!dashboardBrandId && adminUser?.role === "platform_admin") {
|
||||
const { data: firstBrand } = await supabase
|
||||
const { data: firstBrand } = await api
|
||||
.from("brands")
|
||||
.select("id")
|
||||
.limit(1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import ProductEditForm from "@/components/admin/ProductEditForm";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
@@ -14,10 +14,10 @@ type ProductDetailPageProps = {
|
||||
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
|
||||
const { id } = await params;
|
||||
|
||||
const [{ data: product, error }, { data: brands }] = await Promise.all([
|
||||
supabase.from("products").select("*, brands(name, slug)").eq("id", id).single(),
|
||||
supabase.from("brands").select("id, name, slug"),
|
||||
]);
|
||||
const [{ data: product, error }, { data: brands }] = (await Promise.all([
|
||||
api.from("products").select("*, brands(name, slug)").eq("id", id).single(),
|
||||
api.from("brands").select("id, name, slug"),
|
||||
])) as any;
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import ProductsClient from "@/components/admin/ProductsClient";
|
||||
@@ -31,7 +31,7 @@ export default async function AdminProductsPage() {
|
||||
|
||||
const brandId = adminUser.brand_id;
|
||||
|
||||
let query = supabase
|
||||
let query = api
|
||||
.from("products")
|
||||
.select(`
|
||||
id,
|
||||
@@ -52,7 +52,7 @@ export default async function AdminProductsPage() {
|
||||
query = query.eq("brand_id", adminUser.brand_id);
|
||||
}
|
||||
|
||||
const { data: products, error } = await query;
|
||||
const { data: products, error } = (await query) as any;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import ReportsDashboard from "@/components/admin/ReportsDashboard";
|
||||
@@ -12,7 +12,7 @@ export default async function ReportsPage() {
|
||||
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
|
||||
const { data: brands } = await supabase
|
||||
const { data: brands } = await api
|
||||
.from("brands")
|
||||
.select("id, name")
|
||||
.order("name");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getBillingOverview } from "@/actions/billing/billing-overview";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
@@ -18,7 +18,7 @@ export default async function BillingPage({ params }: Props) {
|
||||
|
||||
let resolvedBrandId = effectiveBrandId;
|
||||
if (isPlatformAdmin && !resolvedBrandId) {
|
||||
const { data: firstBrand } = await supabase
|
||||
const { data: firstBrand } = await api
|
||||
.from("brands")
|
||||
.select("id")
|
||||
.limit(1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import IntegrationsClientPage from "./IntegrationsClientPage";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
@@ -18,7 +18,7 @@ export default async function IntegrationsPage() {
|
||||
|
||||
// Platform admins: fetch all brands for the picker
|
||||
const brands = isPlatformAdmin
|
||||
? (await supabase.from("brands").select("id, name").order("name")).data ?? []
|
||||
? (await api.from("brands").select("id, name").order("name")).data ?? []
|
||||
: [];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getShippingSettings } from "@/actions/shipping/settings";
|
||||
import ShippingSettingsForm from "@/components/admin/ShippingSettingsForm";
|
||||
@@ -17,7 +17,7 @@ export default async function ShippingSettingsPage() {
|
||||
|
||||
// Platform admins: fetch all brands for the picker
|
||||
const brands = isPlatformAdmin
|
||||
? (await supabase.from("brands").select("id, name").order("name")).data ?? []
|
||||
? (await api.from("brands").select("id, name").order("name")).data ?? []
|
||||
: [];
|
||||
|
||||
const effectiveBrandId = brandId || (brands[0]?.id ?? "");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import StopEditForm from "@/components/admin/StopEditForm";
|
||||
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
||||
import MessageCustomersSection from "@/components/admin/MessageCustomersSection";
|
||||
@@ -16,14 +16,14 @@ type StopDetailPageProps = {
|
||||
export default async function StopDetailPage({ params }: StopDetailPageProps) {
|
||||
const { id } = await params;
|
||||
|
||||
const [{ data: stop, error }, { data: brands }] = await Promise.all([
|
||||
supabase
|
||||
const [{ data: stop, error }, { data: brands }] = (await Promise.all([
|
||||
api
|
||||
.from("stops")
|
||||
.select("*, brands(name, slug)")
|
||||
.eq("id", id)
|
||||
.single(),
|
||||
supabase.from("brands").select("id, name, slug"),
|
||||
]);
|
||||
api.from("brands").select("id, name, slug"),
|
||||
])) as any;
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
|
||||
@@ -56,17 +56,17 @@ export default async function StopDetailPage({ params }: StopDetailPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const [{ data: allProducts }, { data: productStops }] = await Promise.all([
|
||||
supabase
|
||||
const [{ data: allProducts }, { data: productStops }] = (await Promise.all([
|
||||
api
|
||||
.from("products")
|
||||
.select("id, name, type, price")
|
||||
.eq("brand_id", stop.brand_id)
|
||||
.eq("active", true),
|
||||
supabase
|
||||
api
|
||||
.from("product_stops")
|
||||
.select("id, product_id, products(id, name, type, price)")
|
||||
.eq("stop_id", id),
|
||||
]);
|
||||
])) as any;
|
||||
|
||||
const assignedProducts = (productStops ?? [])
|
||||
.map((ps: any) => ps)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import NewStopForm from "@/components/admin/NewStopForm";
|
||||
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
@@ -32,11 +32,11 @@ export default async function NewStopPage({
|
||||
|
||||
let duplicateFrom: Stop | null = null;
|
||||
if (duplicate) {
|
||||
const { data } = await supabase
|
||||
const { data } = (await api
|
||||
.from("stops")
|
||||
.select("city, state, location, date, time, brand_id, active, address, zip, cutoff_time")
|
||||
.eq("id", duplicate)
|
||||
.single();
|
||||
.single()) as { data: Stop | null };
|
||||
duplicateFrom = data;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function NewStopPage({
|
||||
adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||
|
||||
const [{ data: allProducts }] = await Promise.all([
|
||||
supabase
|
||||
api
|
||||
.from("products")
|
||||
.select("id, name, type, price")
|
||||
.eq("brand_id", brandId)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import StopTableClient from "@/components/admin/StopTableClient";
|
||||
import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
@@ -22,7 +22,7 @@ export default async function AdminStopsPage() {
|
||||
redirect("/admin/pickup");
|
||||
}
|
||||
|
||||
let query = supabase
|
||||
let query = api
|
||||
.from("stops")
|
||||
.select(`
|
||||
id,
|
||||
@@ -49,7 +49,7 @@ export default async function AdminStopsPage() {
|
||||
query = query.eq("brand_id", adminUser.brand_id);
|
||||
}
|
||||
|
||||
const { data: stops, error } = await query;
|
||||
const { data: stops, error } = (await query) as any;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import TaxDashboard from "@/components/admin/TaxDashboard";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
@@ -20,7 +20,7 @@ export default async function TaxesPage({ params }: Props) {
|
||||
|
||||
let resolvedBrandId = effectiveBrandId;
|
||||
if (isPlatformAdmin && !resolvedBrandId) {
|
||||
const { data: firstBrand } = await supabase
|
||||
const { data: firstBrand } = await api
|
||||
.from("brands")
|
||||
.select("id")
|
||||
.limit(1)
|
||||
@@ -54,7 +54,7 @@ export default async function TaxesPage({ params }: Props) {
|
||||
|
||||
if (!resolvedBrandId) return <AdminAccessDenied />;
|
||||
|
||||
const { data: brands } = await supabase
|
||||
const { data: brands } = await api
|
||||
.from("brands")
|
||||
.select("id, name")
|
||||
.order("name");
|
||||
|
||||
@@ -82,8 +82,8 @@ Use "recent_orders" for recent order questions.`;
|
||||
const queryType = parsed.queryType ?? "recent_orders";
|
||||
const days = parsed.days ?? 30;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY;
|
||||
|
||||
// Route to appropriate RPC based on query type
|
||||
let rpcName = "get_recent_orders_insights";
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth);
|
||||
@@ -2,8 +2,8 @@ import { NextResponse } from "next/server";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export async function GET() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const supabaseKey = process.env.POSTGREST_SERVICE_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseKey) {
|
||||
return NextResponse.json({ error: "Missing configuration" }, { status: 500 });
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? "";
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
|
||||
|
||||
if (!serviceKey || !supabaseUrl) {
|
||||
return NextResponse.json({ error: "Missing env vars", serviceKey: !!serviceKey, supabaseUrl: !!supabaseUrl }, { status: 500 });
|
||||
}
|
||||
|
||||
// Test 1: just a simple health endpoint that doesn't require the key
|
||||
let healthResult = null;
|
||||
try {
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/`, {
|
||||
headers: { apikey: serviceKey },
|
||||
});
|
||||
healthResult = { status: res.status, ok: res.ok };
|
||||
} catch (e: any) {
|
||||
healthResult = { error: e?.message };
|
||||
}
|
||||
|
||||
// Test 2: try admin_users with POST (bypasses RLS select policies)
|
||||
let adminResult = null;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/admin_users?select=id,user_id,role,email,display_name&limit=5`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
apikey: serviceKey,
|
||||
"Content-Type": "application/json",
|
||||
Prefer: "return=representation",
|
||||
},
|
||||
}
|
||||
);
|
||||
const body = await res.text();
|
||||
let parsed = null;
|
||||
try { parsed = JSON.parse(body); } catch { parsed = body; }
|
||||
adminResult = { status: res.status, body: parsed };
|
||||
} catch (e: any) {
|
||||
adminResult = { error: e?.message };
|
||||
}
|
||||
|
||||
return NextResponse.json({ healthResult, adminResult, serviceKeyLen: serviceKey.length });
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function GET() {
|
||||
const cookieStore = await cookies();
|
||||
const allCookies = cookieStore.getAll();
|
||||
const rcAuthUid = cookieStore.get("rc_auth_uid")?.value;
|
||||
const rcUid = cookieStore.get("rc_uid")?.value;
|
||||
const rcAccessToken = cookieStore.get("rc_access_token")?.value;
|
||||
|
||||
let adminUsersStatus = "not_tried";
|
||||
let adminUsersResult: string | null = null;
|
||||
|
||||
if (rcAuthUid) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (supabaseUrl && serviceKey) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/admin_users?user_id=eq.${rcAuthUid}&limit=1`,
|
||||
{ headers: { apikey: serviceKey, "Content-Type": "application/json" } }
|
||||
);
|
||||
adminUsersStatus = String(res.status);
|
||||
const data = await res.json().catch(() => null);
|
||||
adminUsersResult = JSON.stringify(data);
|
||||
} catch (e) {
|
||||
adminUsersStatus = "error: " + (e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
} else {
|
||||
adminUsersStatus = "missing_env_vars";
|
||||
}
|
||||
} else {
|
||||
adminUsersStatus = "no_rc_auth_uid_cookie";
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
cookies: {
|
||||
rc_auth_uid: rcAuthUid ? `${rcAuthUid.slice(0, 10)}...` : null,
|
||||
rc_uid: rcUid ? `${rcUid.slice(0, 10)}...` : null,
|
||||
rc_access_token: rcAccessToken ? "present" : null,
|
||||
all_cookie_names: allCookies.map(c => c.name),
|
||||
},
|
||||
admin_users: {
|
||||
status: adminUsersStatus,
|
||||
result: adminUsersResult,
|
||||
},
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user