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:
|
with:
|
||||||
node-version: '22'
|
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
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Load env
|
|
||||||
run: cp /home/tyler/route-commerce/.env.production .env.production
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
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
|
- 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: |
|
run: |
|
||||||
APP_DIR=/home/tyler/route-commerce
|
APP_DIR=/home/tyler/route-commerce
|
||||||
mkdir -p $APP_DIR
|
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
|
# Copy build output and required files
|
||||||
rsync -a --delete .next/ $APP_DIR/.next/
|
rsync -a --delete .next/ $APP_DIR/.next/
|
||||||
rsync -a --delete public/ $APP_DIR/public/
|
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
|
cp next.config.ts $APP_DIR/ 2>/dev/null || cp next.config.js $APP_DIR/ 2>/dev/null || true
|
||||||
|
|
||||||
# Install production deps only
|
# Install production deps only
|
||||||
cd $APP_DIR
|
cd $APP_DIR
|
||||||
npm install --omit=dev
|
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
|
# Start or restart PM2 process
|
||||||
if pm2 describe route-commerce > /dev/null 2>&1; then
|
if pm2 describe route-commerce > /dev/null 2>&1; then
|
||||||
pm2 restart route-commerce
|
pm2 restart route-commerce
|
||||||
|
|||||||
+17
-1
@@ -41,4 +41,20 @@ supabase/.temp/
|
|||||||
|
|
||||||
# IDE / local config
|
# IDE / local config
|
||||||
.mcp.json
|
.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.
|
||||||
+21
-27
@@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, type NextRequest } from "next/server";
|
import { NextResponse, type NextRequest } from "next/server";
|
||||||
|
import { getSessionCookie } from "better-auth/cookies";
|
||||||
|
|
||||||
const DEV_UID = "dev-user-00000000-0000-0000-0000-000000000000";
|
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=/"
|
// Allow dev cookies via: document.cookie = "dev_session=platform_admin; path=/"
|
||||||
const devSession = request.cookies.get("dev_session")?.value;
|
const devSession = request.cookies.get("dev_session")?.value;
|
||||||
const isDevMode = devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee";
|
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) {
|
if (isDevMode) {
|
||||||
// Dev session only valid in development
|
authed = true;
|
||||||
authUid = DEV_UID;
|
} else if (hasSession) {
|
||||||
} else if (rcAuthUid) {
|
authed = true;
|
||||||
// rc_auth_uid is set by /api/login — treat as authenticated
|
|
||||||
authUid = rcAuthUid;
|
|
||||||
}
|
}
|
||||||
// No rc_auth_uid in production → authUid stays null → redirect to /login
|
|
||||||
|
|
||||||
const isAdmin = request.nextUrl.pathname.startsWith("/admin");
|
const isAdmin = request.nextUrl.pathname.startsWith("/admin");
|
||||||
const isLogin = request.nextUrl.pathname === "/login";
|
const isLogin = request.nextUrl.pathname === "/login";
|
||||||
|
|
||||||
if (isAdmin && !authUid) {
|
if (isAdmin && !authed) {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
// Auto-login for demo: no auth cookie present
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
url.pathname = "/login";
|
url.pathname = "/admin";
|
||||||
return NextResponse.redirect(url);
|
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();
|
const url = request.nextUrl.clone();
|
||||||
url.pathname = "/admin";
|
url.pathname = "/admin";
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
|
|||||||
+29
-1
@@ -19,6 +19,24 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "picsum.photos",
|
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"],
|
formats: ["image/avif", "image/webp"],
|
||||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||||
@@ -88,8 +106,18 @@ const nextConfig: NextConfig = {
|
|||||||
|
|
||||||
// Rewrites for API proxy
|
// Rewrites for API proxy
|
||||||
async rewrites() {
|
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 [
|
return [
|
||||||
// Add any necessary rewrites here
|
{
|
||||||
|
source: "/storage/:path*",
|
||||||
|
destination: `${storageBase}/:path*`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -15,17 +15,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.96.0",
|
"@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",
|
"@clerk/nextjs": "^7.4.2",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"@gsap/react": "^2.1.2",
|
"@gsap/react": "^2.1.2",
|
||||||
"@sentry/nextjs": "^10.55.0",
|
"@sentry/nextjs": "^10.55.0",
|
||||||
"@supabase/ssr": "^0.10.2",
|
|
||||||
"@supabase/supabase-js": "^2.105.3",
|
|
||||||
"@upstash/ratelimit": "^2.0.8",
|
"@upstash/ratelimit": "^2.0.8",
|
||||||
"@upstash/redis": "^1.38.0",
|
"@upstash/redis": "^1.38.0",
|
||||||
|
"better-auth": "^1.6.14",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"framer-motion": "^12.40.0",
|
"framer-motion": "^12.40.0",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
|
"kysely": "^0.29.2",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.17.0",
|
||||||
"next": "^16.2.6",
|
"next": "^16.2.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
import { svcRpc } from "@/lib/svc-fetch";
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminActionPayload = {
|
type AdminActionPayload = {
|
||||||
action_type: "create" | "update" | "delete";
|
action_type: "create" | "update" | "delete";
|
||||||
@@ -30,8 +21,7 @@ type UserActivityPayload = {
|
|||||||
|
|
||||||
export async function logAdminAction(payload: AdminActionPayload): Promise<void> {
|
export async function logAdminAction(payload: AdminActionPayload): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const service = getServiceClient();
|
await svcRpc("log_admin_action", {
|
||||||
await service.rpc("log_admin_action", {
|
|
||||||
p_payload: {
|
p_payload: {
|
||||||
action_type: payload.action_type,
|
action_type: payload.action_type,
|
||||||
admin_id: payload.admin_id ?? null,
|
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> {
|
export async function logUserActivity(payload: UserActivityPayload): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const service = getServiceClient();
|
await svcRpc("log_user_activity", {
|
||||||
await service.rpc("log_user_activity", {
|
|
||||||
p_payload: {
|
p_payload: {
|
||||||
user_id: payload.user_id,
|
user_id: payload.user_id,
|
||||||
activity_type: payload.activity_type,
|
activity_type: payload.activity_type,
|
||||||
|
|||||||
@@ -1,63 +1,41 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createServerClient } from "@supabase/ssr";
|
import { Pool } from "pg";
|
||||||
import { cookies } from "next/headers";
|
import { randomUUID } from "crypto";
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
const DEV_ADMIN_UID = "dev-user-00000000-0000-0000-0000-000000000001";
|
const DEV_ADMIN_UID = "dev-user-00000000-0000-0000-0000-000000000001";
|
||||||
|
|
||||||
export async function forceAdminLogin(): Promise<{ success: boolean; uid?: string; error?: string }> {
|
export async function forceAdminLogin(): Promise<{ success: boolean; uid?: string; error?: string }> {
|
||||||
const cookieStore = await cookies();
|
try {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
// Upsert dev platform_admin record
|
||||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
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();
|
if (res.rows.length === 0) {
|
||||||
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
|
return { success: false, error: "Failed to upsert dev admin" };
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
"use server";
|
||||||
|
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
import { svcRpc } from "@/lib/svc-fetch";
|
||||||
|
|
||||||
export async function updatePasswordAction(
|
export async function updatePasswordAction(
|
||||||
newPassword: string
|
newPassword: string
|
||||||
@@ -15,12 +15,7 @@ export async function updatePasswordAction(
|
|||||||
return { error: "Not authenticated. Please log in again." };
|
return { error: "Not authenticated. Please log in again." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = createServiceClient(
|
const { error } = await svcRpc("update_user_password", {
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
||||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
|
||||||
);
|
|
||||||
|
|
||||||
const { error } = await service.rpc("update_user_password", {
|
|
||||||
p_user_id: uid,
|
p_user_id: uid,
|
||||||
p_password: newPassword,
|
p_password: newPassword,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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";
|
"use server";
|
||||||
|
|
||||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
import * as authAdmin from "@/lib/auth-admin";
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ResetAdminPasswordResult =
|
export type ResetAdminPasswordResult =
|
||||||
| { success: true; tempPassword: string }
|
| { success: true; tempPassword: string }
|
||||||
@@ -24,24 +15,19 @@ export async function resetAdminPassword(
|
|||||||
email: string,
|
email: string,
|
||||||
newPassword: string
|
newPassword: string
|
||||||
): Promise<ResetAdminPasswordResult> {
|
): Promise<ResetAdminPasswordResult> {
|
||||||
const service = getServiceClient();
|
|
||||||
|
|
||||||
// Look up auth user by email
|
// Look up auth user by email
|
||||||
const { data: authUsers, error: listError } = await service.auth.admin.listUsers();
|
const { data: users, error: listError } = await authAdmin.listUsers({ searchValue: email });
|
||||||
if (listError || !authUsers?.users) {
|
if (listError) {
|
||||||
return { success: false, error: "Could not list users: " + listError?.message };
|
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) {
|
if (!authUser) {
|
||||||
return { success: false, error: "No auth user found for that email address." };
|
return { success: false, error: "No auth user found for that email address." };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update password via service role
|
// Update password via the better-auth admin wrapper (uses `update_user_password` RPC internally)
|
||||||
const { error: updateError } = await service.auth.admin.updateUserById(
|
const { error: updateError } = await authAdmin.setUserPassword(authUser.id, newPassword);
|
||||||
authUser.id,
|
|
||||||
{ password: newPassword, email_confirm: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updateError) {
|
if (updateError) {
|
||||||
return { success: false, error: updateError.message };
|
return { success: false, error: updateError.message };
|
||||||
|
|||||||
+93
-203
@@ -1,14 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import { createServerClient } from "@supabase/ssr";
|
import { api as publicApi } from "@/lib/api";
|
||||||
import { NextRequest } from "next/server";
|
import { auth } from "@/lib/auth";
|
||||||
import { NextResponse } from "next/server";
|
import { svcApi, svcRpc } from "@/lib/svc-fetch";
|
||||||
import { createClient as createServiceClient } from "@supabase/supabase-js";
|
import * as authAdmin from "@/lib/auth-admin";
|
||||||
import { supabase as publicSupabase } from "@/lib/supabase";
|
|
||||||
import { getMockTableData, mockBrands } from "@/lib/mock-data";
|
|
||||||
|
|
||||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
|
||||||
|
|
||||||
export type AdminUserRow = {
|
export type AdminUserRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -75,66 +71,44 @@ export type UpdateAdminUserInput = {
|
|||||||
phone_number?: string | null;
|
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 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 cookieHeader = headerStore.get("cookie") || "";
|
||||||
const allCookies = cookieHeader.split(";").map(c => c.trim());
|
const allCookies = cookieHeader.split(";").map(c => c.trim());
|
||||||
const rcUidCookie = allCookies.find(c => c.startsWith("rc_auth_uid="));
|
const rcUidCookie = allCookies.find(c => c.startsWith("rc_auth_uid="));
|
||||||
const rcAuthUid = rcUidCookie ? rcUidCookie.split("=")[1] : null;
|
return 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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 }> {
|
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 DEV_FORCE_UID = "dev-user-00000000-0000-0000-0000-000000000001";
|
||||||
|
const rcAuthUid = await readRcAuthUid();
|
||||||
if (rcAuthUid === DEV_FORCE_UID) {
|
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();
|
const headerStore = await headers();
|
||||||
if (userError || !userData.user) {
|
const session = await auth.api.getSession({ headers: headerStore });
|
||||||
|
if (!session?.user) {
|
||||||
return { data: null, error: "Not authenticated" };
|
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 };
|
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 ──
|
// ─── Dev-only path — uses service role to create auth user + admin_users ──
|
||||||
|
|
||||||
async function devCreateAdminUser(input: CreateAdminUserInput): Promise<{ user: AdminUserRow | null; error: string | null }> {
|
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" };
|
return { user: null, error: "Not authenticated" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = getServiceClient();
|
// Create auth user with the provided password (better-auth signUpEmail)
|
||||||
|
const { data: authUser, error: authError } = await authAdmin.createUser({
|
||||||
// Create auth user with the provided password
|
|
||||||
const { data: authUser, error: authError } = await service.auth.admin.createUser({
|
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: input.password,
|
password: input.password,
|
||||||
email_confirm: true,
|
name: input.display_name || input.email.split("@")[0],
|
||||||
user_metadata: {
|
|
||||||
display_name: input.display_name || input.email.split("@")[0],
|
|
||||||
phone_number: input.phone_number ?? null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (authError || !authUser.user) {
|
if (authError || !authUser) {
|
||||||
return { user: null, error: authError?.message ?? "Failed to create auth user" };
|
return { user: null, error: authError?.message ?? "Failed to create auth user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert into admin_users
|
// Insert into admin_users (service role bypasses RLS)
|
||||||
const { data: inserted, error: insertError } = await service
|
const { data: inserted, error: insertError } = await svcApi
|
||||||
.from("admin_users")
|
.from("admin_users")
|
||||||
.insert({
|
.insert({
|
||||||
user_id: authUser.user.id,
|
user_id: authUser.id,
|
||||||
role: input.role,
|
role: input.role,
|
||||||
brand_id: input.brand_id,
|
brand_id: input.brand_id,
|
||||||
display_name: input.display_name || input.email.split("@")[0],
|
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,
|
must_change_password: input.mustChangePassword ?? true,
|
||||||
})
|
})
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single() as { data: any; error: { message: string } | null };
|
||||||
|
|
||||||
if (insertError) {
|
if (insertError) {
|
||||||
return { user: null, error: insertError.message };
|
return { user: null, error: insertError.message };
|
||||||
}
|
}
|
||||||
|
if (!inserted) {
|
||||||
|
return { user: null, error: "Insert returned no row" };
|
||||||
|
}
|
||||||
|
|
||||||
// Send welcome email
|
// Send welcome email
|
||||||
try {
|
try {
|
||||||
@@ -263,27 +234,24 @@ function mapUserRow(row: Record<string, unknown>): AdminUserRow {
|
|||||||
// ─── Dev path helpers (service role, local only) ───────────────────────────
|
// ─── Dev path helpers (service role, local only) ───────────────────────────
|
||||||
|
|
||||||
async function devListAdminUsers(callerUid?: string): Promise<{ users: AdminUserRow[]; error: string | null }> {
|
async function devListAdminUsers(callerUid?: string): Promise<{ users: AdminUserRow[]; error: string | null }> {
|
||||||
const service = getServiceClient();
|
|
||||||
|
|
||||||
// Ensure caller has an admin_users record
|
// Ensure caller has an admin_users record
|
||||||
if (callerUid) {
|
if (callerUid) {
|
||||||
const { data: existing } = await service
|
const { data: existing } = await svcApi
|
||||||
.from("admin_users")
|
.from("admin_users")
|
||||||
.select("id")
|
.select("id")
|
||||||
.eq("user_id", callerUid)
|
.eq("user_id", callerUid)
|
||||||
.maybeSingle();
|
.maybeSingle() as { data: any };
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
// auto-creating admin_users for uid
|
// auto-creating admin_users for uid
|
||||||
const { data: authData } = await service.auth.admin.listUsers();
|
const { data: listData } = await authAdmin.listUsers({ searchValue: undefined });
|
||||||
const authUser = authData?.users?.find((u) => u.id === callerUid);
|
const authUser = (listData ?? []).find((u: any) => u.id === callerUid);
|
||||||
const meta = (authUser as { user_metadata?: Record<string, unknown> })?.user_metadata;
|
await svcApi.from("admin_users").insert({
|
||||||
await service.from("admin_users").insert({
|
|
||||||
user_id: callerUid,
|
user_id: callerUid,
|
||||||
role: "platform_admin",
|
role: "platform_admin",
|
||||||
brand_id: null,
|
brand_id: null,
|
||||||
display_name: (meta?.display_name as string | null) ?? authUser?.email?.split("@")[0] ?? "Admin",
|
display_name: authUser?.name ?? authUser?.email?.split("@")[0] ?? "Admin",
|
||||||
phone_number: (meta?.phone_number as string | null) ?? null,
|
phone_number: null,
|
||||||
can_manage_products: true,
|
can_manage_products: true,
|
||||||
can_manage_stops: true,
|
can_manage_stops: true,
|
||||||
can_manage_orders: 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)
|
// 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")
|
.from("admin_users")
|
||||||
.select(`
|
.select(`
|
||||||
id, user_id, role, brand_id, active, must_change_password, created_at, last_login,
|
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,
|
can_manage_messages, can_manage_refunds, can_manage_users, can_manage_water_log, can_manage_reports,
|
||||||
brands (name)
|
brands (name)
|
||||||
`)
|
`)
|
||||||
.order("created_at", { ascending: false });
|
.order("created_at", { ascending: false }) as { data: any; error: any };
|
||||||
|
|
||||||
if (adminError) return { users: [], error: adminError.message };
|
if (adminError) return { users: [], error: adminError.message };
|
||||||
|
|
||||||
// Fetch auth user details via service role admin API
|
// Fetch auth user details via better-auth admin list
|
||||||
const { data: authData, error: authError } = await service.auth.admin.listUsers();
|
const { data: listData, error: authError } = await authAdmin.listUsers({ searchValue: undefined });
|
||||||
if (authError) return { users: [], error: authError.message };
|
if (authError) return { users: [], error: authError.message };
|
||||||
|
|
||||||
const authMap: Record<string, { email: string; display_name: string | null; phone_number: string | null }> = {};
|
const authMap: Record<string, { email: string; display_name: string | null }> = {};
|
||||||
(authData?.users ?? []).forEach((u) => {
|
(listData ?? []).forEach((u: any) => {
|
||||||
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) => {
|
|
||||||
authMap[u.id] = {
|
authMap[u.id] = {
|
||||||
email: u.email ?? "",
|
email: u.email ?? "",
|
||||||
display_name: (u.user_metadata?.display_name as string | null) ?? (u.user_metadata?.full_name as string | null) ?? null,
|
display_name: u.name ?? null,
|
||||||
phone_number: (u.user_metadata?.phone_number as string | null) ?? 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 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 {
|
return {
|
||||||
...mapUserRow(r),
|
...mapUserRow(r),
|
||||||
email: authInfo.email || "No Email",
|
email: authInfo.email || "No Email",
|
||||||
display_name: authInfo.display_name ?? null,
|
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,
|
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";
|
const DEV_FORCE_UID = "dev-user-00000000-0000-0000-0000-000000000001";
|
||||||
|
|
||||||
export async function getAdminUsers(brandId?: string): Promise<{ users: AdminUserRow[]; error: string | null }> {
|
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 cookieStore = await cookies();
|
||||||
const headerStore = await headers();
|
const headerStore = await headers();
|
||||||
const devSession = cookieStore.get("dev_session")?.value;
|
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;
|
.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.
|
// 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) {
|
if (process.env.NODE_ENV !== "production" && rcAuthUid && rcAuthUid !== DEV_FORCE_UID) {
|
||||||
return devListAdminUsers(rcAuthUid);
|
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
|
// 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 });
|
const result = await callRpcWithAuth<AdminUserRow[]>("get_admin_users", { p_brand_id: brandId ?? null });
|
||||||
if (result.error === "Not authenticated" && rcAuthUid) {
|
if (result.error === "Not authenticated" && rcAuthUid) {
|
||||||
// No Supabase session token in browser — use service role with rc_auth_uid
|
// No better-auth session token in browser — use service role via devListAdminUsers
|
||||||
const service = getServiceClient();
|
return devListAdminUsers(rcAuthUid);
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
return { users: result.data ?? [], error: result.error };
|
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)
|
// 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
|
// rc_auth_uid cookie proves the admin is logged in; service role creates the account
|
||||||
if (rcAuthUid) {
|
if (rcAuthUid) {
|
||||||
const service = getServiceClient();
|
// Create auth user via better-auth
|
||||||
|
const { data: authUser, error: authError } = await authAdmin.createUser({
|
||||||
// Create auth user
|
|
||||||
const { data: authUser, error: authError } = await service.auth.admin.createUser({
|
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: input.password,
|
password: input.password,
|
||||||
email_confirm: true,
|
name: input.display_name || input.email.split("@")[0],
|
||||||
user_metadata: {
|
|
||||||
display_name: input.display_name || input.email.split("@")[0],
|
|
||||||
phone_number: input.phone_number ?? null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (authError || !authUser.user) {
|
if (authError || !authUser) {
|
||||||
return { user: null, error: authError?.message ?? "Failed to create auth user" };
|
return { user: null, error: authError?.message ?? "Failed to create auth user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert into admin_users
|
// Insert into admin_users via service-role PostgREST
|
||||||
const { data: inserted, error: insertError } = await service
|
const { data: inserted, error: insertError } = await svcApi
|
||||||
.from("admin_users")
|
.from("admin_users")
|
||||||
.insert({
|
.insert({
|
||||||
user_id: authUser.user.id,
|
user_id: authUser.id,
|
||||||
role: input.role,
|
role: input.role,
|
||||||
brand_id: input.brand_id,
|
brand_id: input.brand_id,
|
||||||
display_name: input.display_name || input.email.split("@")[0],
|
display_name: input.display_name || input.email.split("@")[0],
|
||||||
@@ -506,7 +402,9 @@ export async function createAdminUser(input: CreateAdminUserInput): Promise<{ us
|
|||||||
.select()
|
.select()
|
||||||
.single();
|
.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
|
// Send welcome email
|
||||||
try {
|
try {
|
||||||
@@ -525,26 +423,26 @@ export async function createAdminUser(input: CreateAdminUserInput): Promise<{ us
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: inserted.id,
|
id: inserted.id ?? "",
|
||||||
user_id: inserted.user_id,
|
user_id: inserted.user_id ?? authUser.id,
|
||||||
display_name: inserted.display_name ?? input.display_name ?? input.email.split("@")[0],
|
display_name: inserted.display_name ?? input.display_name ?? input.email.split("@")[0],
|
||||||
email: input.email,
|
email: input.email,
|
||||||
phone_number: inserted.phone_number ?? input.phone_number ?? null,
|
phone_number: inserted.phone_number ?? input.phone_number ?? null,
|
||||||
role: inserted.role,
|
role: (inserted.role as AdminUserRow["role"]) ?? "store_employee",
|
||||||
brand_id: inserted.brand_id,
|
brand_id: inserted.brand_id ?? null,
|
||||||
brand_name: null,
|
brand_name: null,
|
||||||
can_manage_products: inserted.can_manage_products,
|
can_manage_products: inserted.can_manage_products ?? false,
|
||||||
can_manage_stops: inserted.can_manage_stops,
|
can_manage_stops: inserted.can_manage_stops ?? false,
|
||||||
can_manage_orders: inserted.can_manage_orders,
|
can_manage_orders: inserted.can_manage_orders ?? false,
|
||||||
can_manage_pickup: inserted.can_manage_pickup,
|
can_manage_pickup: inserted.can_manage_pickup ?? false,
|
||||||
can_manage_messages: inserted.can_manage_messages,
|
can_manage_messages: inserted.can_manage_messages ?? false,
|
||||||
can_manage_refunds: inserted.can_manage_refunds,
|
can_manage_refunds: inserted.can_manage_refunds ?? false,
|
||||||
can_manage_users: inserted.can_manage_users,
|
can_manage_users: inserted.can_manage_users ?? false,
|
||||||
can_manage_water_log: inserted.can_manage_water_log,
|
can_manage_water_log: inserted.can_manage_water_log ?? false,
|
||||||
can_manage_reports: inserted.can_manage_reports,
|
can_manage_reports: inserted.can_manage_reports ?? false,
|
||||||
active: inserted.active,
|
active: inserted.active ?? true,
|
||||||
must_change_password: inserted.must_change_password ?? 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,
|
last_login: null,
|
||||||
},
|
},
|
||||||
error: 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;
|
.find(c => c.startsWith("rc_auth_uid="))?.split("=")[1] ?? null;
|
||||||
// DEV_FORCE_UID bypass — Emergency Force Login sets this cookie directly
|
// DEV_FORCE_UID bypass — Emergency Force Login sets this cookie directly
|
||||||
if (rcAuthUid === DEV_FORCE_UID) {
|
if (rcAuthUid === DEV_FORCE_UID) {
|
||||||
const service = getServiceClient();
|
const { data, error } = await svcApi
|
||||||
const { data, error } = await service
|
|
||||||
.from("admin_users")
|
.from("admin_users")
|
||||||
.update({
|
.update({
|
||||||
role: input.role ?? undefined,
|
role: input.role ?? undefined,
|
||||||
@@ -586,8 +483,8 @@ export async function updateAdminUser(input: UpdateAdminUserInput): Promise<{ us
|
|||||||
.eq("id", input.id)
|
.eq("id", input.id)
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
if (error) return { user: null, error: error.message };
|
if (error || !data) return { user: null, error: error?.message ?? "Update returned no row" };
|
||||||
return { user: mapUserRow(data), error: null };
|
return { user: mapUserRow(data as Record<string, unknown>), error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await callRpcWithAuth<AdminUserRow[]>("update_admin_user", {
|
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;
|
.find(c => c.startsWith("rc_auth_uid="))?.split("=")[1] ?? null;
|
||||||
// DEV_FORCE_UID bypass — Emergency Force Login sets this cookie directly
|
// DEV_FORCE_UID bypass — Emergency Force Login sets this cookie directly
|
||||||
if (rcAuthUid === DEV_FORCE_UID) {
|
if (rcAuthUid === DEV_FORCE_UID) {
|
||||||
const service = getServiceClient();
|
|
||||||
// Get user_id first
|
// Get user_id first
|
||||||
const { data: adminRow, error: fetchError } = await service
|
const { data: adminRow, error: fetchError } = await svcApi
|
||||||
.from("admin_users")
|
.from("admin_users")
|
||||||
.select("user_id")
|
.select("user_id")
|
||||||
.eq("id", id)
|
.eq("id", id)
|
||||||
.single();
|
.single();
|
||||||
if (fetchError) return { success: false, error: fetchError.message };
|
if (fetchError) return { success: false, error: fetchError.message };
|
||||||
// Delete from admin_users
|
// 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 };
|
if (deleteError) return { success: false, error: deleteError.message };
|
||||||
// Delete auth user
|
// Delete auth user via better-auth admin
|
||||||
if (adminRow?.user_id) {
|
if (adminRow?.user_id) {
|
||||||
await service.auth.admin.deleteUser(adminRow.user_id);
|
await authAdmin.removeUser(adminRow.user_id);
|
||||||
}
|
}
|
||||||
return { success: true, error: null };
|
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;
|
.find(c => c.startsWith("rc_auth_uid="))?.split("=")[1] ?? null;
|
||||||
|
|
||||||
if (rcAuthUid === DEV_FORCE_UID || process.env.NODE_ENV !== "production") {
|
if (rcAuthUid === DEV_FORCE_UID || process.env.NODE_ENV !== "production") {
|
||||||
const service = getServiceClient();
|
const { error } = await svcApi.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
||||||
const { error } = await service.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
|
||||||
return { success: !error, error: error?.message ?? null };
|
return { success: !error, error: error?.message ?? null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production path — use service role via direct update
|
// Production path — use service role via direct update
|
||||||
const service = getServiceClient();
|
const { error } = await svcApi.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
||||||
const { error } = await service.from("admin_users").update({ must_change_password: true }).eq("id", userId);
|
|
||||||
return { success: !error, error: error?.message ?? null };
|
return { success: !error, error: error?.message ?? null };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPasswordResetEmail(email: string): Promise<{ success: boolean; error: string | 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`,
|
redirectTo: `${process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"}/change-password`,
|
||||||
});
|
});
|
||||||
return { success: !error, error: error?.message ?? null };
|
return { success: !error, error: error?.message ?? null };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBrands(): Promise<{ brands: { id: string; name: string }[]; error: string | null }> {
|
export async function getBrands(): Promise<{ brands: { id: string; name: string }[]; error: string | null }> {
|
||||||
if (useMockData) {
|
const { data, error } = await publicApi.from("brands").select("id, name").order("name");
|
||||||
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");
|
|
||||||
return { brands: data ?? [], error: error?.message ?? null };
|
return { brands: data ?? [], error: error?.message ?? null };
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
|
|
||||||
export type AIAuthConfig = {
|
export type AIAuthConfig = {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -18,7 +18,7 @@ export async function getAIPreferences(brandId: string): Promise<{
|
|||||||
model?: string;
|
model?: string;
|
||||||
max_tokens?: number;
|
max_tokens?: number;
|
||||||
} | null> {
|
} | null> {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await api
|
||||||
.from("brand_ai_settings")
|
.from("brand_ai_settings")
|
||||||
.select("api_key, organization_id, base_url, model, max_tokens")
|
.select("api_key, organization_id, base_url, model, max_tokens")
|
||||||
.eq("brand_id", brandId)
|
.eq("brand_id", brandId)
|
||||||
@@ -32,7 +32,7 @@ export async function saveAIPreferences(
|
|||||||
brandId: string,
|
brandId: string,
|
||||||
config: AIAuthConfig
|
config: AIAuthConfig
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
const { error } = await supabase
|
const { error } = await api
|
||||||
.from("brand_ai_settings")
|
.from("brand_ai_settings")
|
||||||
.upsert({
|
.upsert({
|
||||||
brand_id: brandId,
|
brand_id: brandId,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ type AuditResult =
|
|||||||
* Audit writes bypass RLS via the SECURITY DEFINER log_audit_event RPC function.
|
* Audit writes bypass RLS via the SECURITY DEFINER log_audit_event RPC function.
|
||||||
*/
|
*/
|
||||||
export async function logAuditEvent(payload: AuditPayload): Promise<AuditResult> {
|
export async function logAuditEvent(payload: AuditPayload): Promise<AuditResult> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ import { getAdminUser } from "@/lib/admin-permissions";
|
|||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
import { ADDONS, PLAN_TIERS, type AddonKey, type PlanTierKey } from "@/lib/pricing";
|
import { ADDONS, PLAN_TIERS, type AddonKey, type PlanTierKey } from "@/lib/pricing";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
export type BillingSubscriptionStatus =
|
export type BillingSubscriptionStatus =
|
||||||
| "active"
|
| "active"
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export async function createRetailStripeCheckoutSession(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Get brand name for Stripe metadata
|
// Get brand name for Stripe metadata
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
let brandName = "Route Commerce";
|
let brandName = "Route Commerce";
|
||||||
try {
|
try {
|
||||||
const brandRes = await fetch(
|
const brandRes = await fetch(
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export async function createStripeCheckoutSession(
|
|||||||
const priceId = getPriceId(priceKey);
|
const priceId = getPriceId(priceKey);
|
||||||
if (!priceId) return { success: false, error: `No Stripe price configured for "${priceKey}". Set ${priceKey.toUpperCase()}_PRICE in your environment.` };
|
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 supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Get brand's Stripe customer ID
|
// Get brand's Stripe customer ID
|
||||||
const custRes = await fetch(
|
const custRes = await fetch(
|
||||||
@@ -123,8 +123,8 @@ export async function cancelAddonSubscription(
|
|||||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||||
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Get active subscription for this brand
|
// Get active subscription for this brand
|
||||||
const subRes = await fetch(
|
const subRes = await fetch(
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export async function getStripeBillingPortalUrl(brandId: string): Promise<{ succ
|
|||||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||||
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Get stripe_customer_id from brands table
|
// Get stripe_customer_id from brands table
|
||||||
const custRes = await fetch(
|
const custRes = await fetch(
|
||||||
@@ -49,8 +49,8 @@ export async function updateBrandPlanTier(brandId: string, planTier: string): Pr
|
|||||||
return { success: false, error: "Invalid plan tier" };
|
return { success: false, error: "Invalid plan tier" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/update_brand_plan_tier`,
|
`${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" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/update_brand_stripe_customer_id`,
|
`${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 }> {
|
export async function getBrandPlanInfo(brandId: string): Promise<{ success: boolean; data?: any; error?: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_brand_plan_info`,
|
`${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>> {
|
export async function getEnabledAddons(brandId: string): Promise<Record<string, boolean>> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_brand_features`,
|
`${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[]> {
|
export async function getRecentWholesaleOrders(brandId: string, limit = 20): Promise<any[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_orders`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_orders`,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
import { uploadFile, BUCKETS, publicUrl, storageKeys } from "@/lib/storage";
|
||||||
|
|
||||||
export type UploadLogoResult =
|
export type UploadLogoResult =
|
||||||
| { success: true; logoUrl: string }
|
| { 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 ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
||||||
const path = isDark ? `logo-dark.${ext}` : `logo.${ext}`;
|
const fileName = isDark ? `logo-dark.${ext}` : `logo.${ext}`;
|
||||||
const storagePath = `brand-logos/${brandId}/${path}`;
|
const key = storageKeys.brandLogo(brandId, fileName);
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
let url: string;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
try {
|
||||||
|
const res = await uploadFile({
|
||||||
const uploadRes = await fetch(
|
bucket: BUCKETS.BRAND_LOGOS,
|
||||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
key,
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
|
||||||
body: buffer,
|
body: buffer,
|
||||||
}
|
contentType: file.type,
|
||||||
);
|
});
|
||||||
|
url = res.url;
|
||||||
if (!uploadRes.ok) {
|
} catch (e) {
|
||||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
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(
|
const saveRes = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||||
{
|
{
|
||||||
@@ -62,7 +60,7 @@ export async function uploadBrandLogo(
|
|||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
p_brand_id: brandId,
|
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: false, error: "Upload succeeded but failed to save URL" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, logoUrl: publicUrl };
|
return { success: true, logoUrl: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadOlatheSweetLogo(
|
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 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 arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
let url: string;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
try {
|
||||||
|
const res = await uploadFile({
|
||||||
const uploadRes = await fetch(
|
bucket: BUCKETS.BRAND_LOGOS,
|
||||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
key,
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
|
||||||
body: buffer,
|
body: buffer,
|
||||||
}
|
contentType: file.type,
|
||||||
);
|
});
|
||||||
|
url = res.url;
|
||||||
if (!uploadRes.ok) {
|
} catch (e) {
|
||||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
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(
|
const saveRes = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||||
@@ -126,7 +122,7 @@ export async function uploadOlatheSweetLogo(
|
|||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
p_brand_id: brandId,
|
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: false, error: "Upload succeeded but failed to save URL" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, logoUrl: publicUrl };
|
return { success: true, logoUrl: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadOlatheSweetLogoDark(
|
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 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 arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
let url: string;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
try {
|
||||||
|
const res = await uploadFile({
|
||||||
const uploadRes = await fetch(
|
bucket: BUCKETS.BRAND_LOGOS,
|
||||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
key,
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
|
||||||
body: buffer,
|
body: buffer,
|
||||||
}
|
contentType: file.type,
|
||||||
);
|
});
|
||||||
|
url = res.url;
|
||||||
if (!uploadRes.ok) {
|
} catch (e) {
|
||||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
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(
|
const saveRes = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||||
@@ -190,7 +184,7 @@ export async function uploadOlatheSweetLogoDark(
|
|||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
p_brand_id: brandId,
|
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: false, error: "Upload succeeded but failed to save URL" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, logoUrl: publicUrl };
|
return { success: true, logoUrl: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrandSettings = {
|
export type BrandSettings = {
|
||||||
@@ -252,8 +246,8 @@ export type SaveBrandSettingsResult =
|
|||||||
| { success: false; error: string };
|
| { success: false; error: string };
|
||||||
|
|
||||||
export async function getBrandSettings(brandId: string): Promise<GetBrandSettingsResult> {
|
export async function getBrandSettings(brandId: string): Promise<GetBrandSettingsResult> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_brand_settings`,
|
`${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
|
// Public version for storefront pages — uses slug, no auth required
|
||||||
export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBrandSettingsResult & { wholesaleEnabled?: boolean | null }> {
|
export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBrandSettingsResult & { wholesaleEnabled?: boolean | null }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_brand_settings_by_slug`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ export async function createOrder(
|
|||||||
brandId?: string,
|
brandId?: string,
|
||||||
shippingAddress?: ShippingAddress
|
shippingAddress?: ShippingAddress
|
||||||
): Promise<CheckoutResult> {
|
): Promise<CheckoutResult> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// ── Calculate tax if brand collects tax ─────────────────────────────────
|
// ── Calculate tax if brand collects tax ─────────────────────────────────
|
||||||
let taxAmount = 0;
|
let taxAmount = 0;
|
||||||
@@ -150,8 +150,8 @@ export async function createOrder(
|
|||||||
// ── Cart Persistence ──────────────────────────────────────────────────────────
|
// ── Cart Persistence ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getServerCart(userId: string): Promise<CartItem[]> {
|
export async function getServerCart(userId: string): Promise<CartItem[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -177,8 +177,8 @@ export async function mergeLocalCart(
|
|||||||
): Promise<{ merged: CartItem[] }> {
|
): Promise<{ merged: CartItem[] }> {
|
||||||
if (!localCart || localCart.length === 0) return { merged: [] };
|
if (!localCart || localCart.length === 0) return { merged: [] };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Fetch server cart
|
// Fetch server cart
|
||||||
let serverCart: CartItem[] = [];
|
let serverCart: CartItem[] = [];
|
||||||
@@ -243,8 +243,8 @@ export async function mergeLocalCart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function clearServerCart(userId: string): Promise<void> {
|
export async function clearServerCart(userId: string): Promise<void> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(
|
await fetch(
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ export async function getCommunicationCampaigns(brandId?: string): Promise<ListC
|
|||||||
|
|
||||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_campaigns`,
|
`${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" };
|
return { success: false, error: "Not authorized to operate on this brand's campaigns" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_campaign`,
|
`${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" };
|
return { success: false, error: "Not authorized to delete this brand's campaigns" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_campaign`,
|
`${supabaseUrl}/rest/v1/rpc/delete_communication_campaign`,
|
||||||
@@ -159,8 +159,8 @@ export async function getCampaignById(campaignId: string, brandId?: string): Pro
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return null;
|
if (!adminUser) return null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_campaign_by_id`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_contacts`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_contact`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/import_communication_contacts_batch`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/opt_out_contact`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_contact`,
|
`${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" };
|
if (!effectiveBrandId) return { success: false, error: "No brand context" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const allContacts: Contact[] = [];
|
const allContacts: Contact[] = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
import { uploadFile, listFiles, BUCKETS, publicUrl, storageKeys } from "@/lib/storage";
|
||||||
// Contact imports bucket
|
|
||||||
const CONTACTS_BUCKET_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
||||||
|
|
||||||
export type UploadContactsResult =
|
export type UploadContactsResult =
|
||||||
| { success: true; fileId: string; fileUrl: string; recordCount: number }
|
| { success: true; fileId: string; fileUrl: string; recordCount: number }
|
||||||
@@ -17,57 +15,41 @@ export async function uploadContactsToBucket(
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
|
|
||||||
// Validate file type
|
|
||||||
if (!file.name.endsWith(".csv")) {
|
if (!file.name.endsWith(".csv")) {
|
||||||
return { success: false, error: "Only CSV files are supported" };
|
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
|
const maxSize = 50 * 1024 * 1024; // 50MB max
|
||||||
if (file.size > maxSize) {
|
if (file.size > maxSize) {
|
||||||
return { success: false, error: "File too large. Max 50MB for large imports." };
|
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 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 arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
const uploadRes = await fetch(
|
let url: string;
|
||||||
`${supabaseUrl}/storage/v1/object/${CONTACTS_BUCKET_ID}/${path}`,
|
try {
|
||||||
{
|
const res = await uploadFile({
|
||||||
method: "PUT",
|
bucket: BUCKETS.CONTACTS_IMPORTS,
|
||||||
headers: {
|
key,
|
||||||
...svcHeaders(supabaseKey),
|
|
||||||
"Authorization": `Bearer ${supabaseKey}`,
|
|
||||||
"Content-Type": "text/csv",
|
|
||||||
"x-upsert": "false",
|
|
||||||
},
|
|
||||||
body: buffer,
|
body: buffer,
|
||||||
}
|
contentType: "text/csv",
|
||||||
);
|
});
|
||||||
|
url = res.url;
|
||||||
if (!uploadRes.ok) {
|
} catch (e) {
|
||||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
return { success: false, error: `Upload failed: ${(e as Error).message}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileUrl = `${supabaseUrl}/storage/v1/object/public/${CONTACTS_BUCKET_ID}/${path}`;
|
const fileId = key.split("/").slice(0, 2).join("/"); // brandId/timestamp
|
||||||
const fileId = `${brandId}/${timestamp}`;
|
|
||||||
|
|
||||||
// Get rough row count from file size (approx 200 bytes per row)
|
|
||||||
const estimatedRows = Math.floor(file.size / 200);
|
const estimatedRows = Math.floor(file.size / 200);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
fileId,
|
fileId,
|
||||||
fileUrl,
|
fileUrl: url,
|
||||||
recordCount: estimatedRows,
|
recordCount: estimatedRows,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -84,10 +66,9 @@ export async function processBucketImport(
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Call RPC to process the file from bucket
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/process_contact_import_from_url`,
|
`${supabaseUrl}/rest/v1/rpc/process_contact_import_from_url`,
|
||||||
{
|
{
|
||||||
@@ -122,37 +103,20 @@ export async function listImportHistory(
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
try {
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const files = await listFiles(BUCKETS.CONTACTS_IMPORTS, `${brandId}/`);
|
||||||
|
return {
|
||||||
// List files in the imports folder for this brand
|
success: true,
|
||||||
const response = await fetch(
|
imports: files.slice(0, limit).map((f) => ({
|
||||||
`${supabaseUrl}/storage/v1/object/list/${CONTACTS_BUCKET_ID}`,
|
filename: f.key.split("/").pop() ?? "",
|
||||||
{
|
size: f.size,
|
||||||
method: "POST",
|
createdAt: f.lastModified.toISOString(),
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
url: publicUrl(BUCKETS.CONTACTS_IMPORTS, f.key),
|
||||||
body: JSON.stringify({
|
})),
|
||||||
prefix: `imports/${brandId}/`,
|
};
|
||||||
limit: limit,
|
} catch (e) {
|
||||||
sortBy: { column: "created_at", order: "desc" },
|
return { success: false, error: (e as Error).message };
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return { success: false, error: "Failed to list imports" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {
|
export type ImportHistoryItem = {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export async function getCommunicationSegments(
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_segments`,
|
`${supabaseUrl}/rest/v1/rpc/get_communication_segments`,
|
||||||
@@ -64,8 +64,8 @@ export async function upsertSegment(params: {
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_segment`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_communication_segment`,
|
||||||
@@ -99,8 +99,8 @@ export async function deleteSegment(
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_segment`,
|
`${supabaseUrl}/rest/v1/rpc/delete_communication_segment`,
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export async function previewCampaignAudience(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/preview_campaign_audience`,
|
`${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" };
|
return { success: false, error: "Not authorized to view these logs" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_message_logs`,
|
`${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" };
|
return { success: false, error: "Not authorized to send this brand's campaigns" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/send_campaign`,
|
`${supabaseUrl}/rest/v1/rpc/send_campaign`,
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export type UpsertSettingsResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getCommunicationSettings(brandId: string): Promise<CommunicationSettings | null> {
|
export async function getCommunicationSettings(brandId: string): Promise<CommunicationSettings | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_settings`,
|
`${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" };
|
return { success: false, error: "Not authorized to modify this brand's communication settings" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_settings`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_communication_settings`,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export async function sendStopBlast(params: {
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/send_stop_blast`,
|
`${supabaseUrl}/rest/v1/rpc/send_stop_blast`,
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export async function getCommunicationTemplates(brandId?: string): Promise<{ suc
|
|||||||
return { success: true, templates: [] };
|
return { success: true, templates: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_templates`,
|
`${supabaseUrl}/rest/v1/rpc/get_communication_templates`,
|
||||||
@@ -70,8 +70,8 @@ export async function upsertTemplate(params: {
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_template`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_communication_template`,
|
||||||
@@ -104,8 +104,8 @@ export async function getTemplateById(templateId: string): Promise<Template | nu
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return null;
|
if (!adminUser) return null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_templates`,
|
`${supabaseUrl}/rest/v1/rpc/get_communication_templates`,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export async function getHarvestReachCampaigns(
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_campaigns`,
|
`${supabaseUrl}/rest/v1/rpc/get_communication_campaigns`,
|
||||||
@@ -82,8 +82,8 @@ export async function getCampaignAnalytics(
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_campaign_analytics`,
|
`${supabaseUrl}/rest/v1/rpc/get_campaign_analytics`,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export async function getProductsForSegmentPicker(
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_products_for_segment_picker`,
|
`${supabaseUrl}/rest/v1/rpc/get_products_for_segment_picker`,
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ export async function getHarvestReachSegments(
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_communication_segments`,
|
`${supabaseUrl}/rest/v1/rpc/get_communication_segments`,
|
||||||
@@ -110,8 +110,8 @@ export async function upsertHarvestReachSegment(params: {
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_communication_segment`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_communication_segment`,
|
||||||
@@ -149,8 +149,8 @@ export async function deleteHarvestReachSegment(
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_communication_segment`,
|
`${supabaseUrl}/rest/v1/rpc/delete_communication_segment`,
|
||||||
@@ -180,8 +180,8 @@ export async function previewSegmentWithCustomers(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/preview_campaign_audience`,
|
`${supabaseUrl}/rest/v1/rpc/preview_campaign_audience`,
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export async function getStopsForSegmentPicker(
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_stops_for_segment_picker`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const results = { imported: 0, errors: [] as { row: number; error: string }[] };
|
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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/bulk_upsert_products`,
|
`${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 ─────────────────────────────────────────────────────
|
// ── Get AI provider settings ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getAIProviderSettings(brandId: string): Promise<AIProviderSettings> {
|
export async function getAIProviderSettings(brandId: string): Promise<AIProviderSettings> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_ai_provider_settings`,
|
`${supabaseUrl}/rest/v1/rpc/get_ai_provider_settings`,
|
||||||
@@ -75,8 +75,8 @@ export async function setAIProviderSettings(
|
|||||||
const current = await getAIProviderSettings(brandId);
|
const current = await getAIProviderSettings(brandId);
|
||||||
const merged = { ...current, ...settings };
|
const merged = { ...current, ...settings };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
provider: merged.provider,
|
provider: merged.provider,
|
||||||
@@ -206,8 +206,8 @@ export async function getAIClient(brandId: string): Promise<{
|
|||||||
// ── Custom integrations ─────────────────────────────────────────────────────────
|
// ── Custom integrations ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getCustomIntegrations(brandId: string): Promise<CustomIntegration[]> {
|
export async function getCustomIntegrations(brandId: string): Promise<CustomIntegration[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_custom_integrations`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (adminUser.brand_id && adminUser.brand_id !== brandId) return { success: false, error: "Not authorized" };
|
if (adminUser.brand_id && adminUser.brand_id !== brandId) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_custom_integration`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (adminUser.brand_id && adminUser.brand_id !== brandId) return { success: false, error: "Not authorized" };
|
if (adminUser.brand_id && adminUser.brand_id !== brandId) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_custom_integration`,
|
`${supabaseUrl}/rest/v1/rpc/delete_custom_integration`,
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export type SaveCredentialsResult =
|
|||||||
// ── Resend Credentials ─────────────────────────────────────────────────────────
|
// ── Resend Credentials ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getResendCredentials(brandId: string): Promise<ResendCredentials> {
|
export async function getResendCredentials(brandId: string): Promise<ResendCredentials> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_resend_credentials`,
|
`${supabaseUrl}/rest/v1/rpc/get_resend_credentials`,
|
||||||
@@ -66,8 +66,8 @@ export async function saveResendCredentials(
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Get current credentials to merge
|
// Get current credentials to merge
|
||||||
const current = await getResendCredentials(brandId);
|
const current = await getResendCredentials(brandId);
|
||||||
@@ -99,8 +99,8 @@ export async function saveResendCredentials(
|
|||||||
// ── Twilio Credentials ─────────────────────────────────────────────────────────
|
// ── Twilio Credentials ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getTwilioCredentials(brandId: string): Promise<TwilioCredentials> {
|
export async function getTwilioCredentials(brandId: string): Promise<TwilioCredentials> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_twilio_credentials`,
|
`${supabaseUrl}/rest/v1/rpc/get_twilio_credentials`,
|
||||||
@@ -141,8 +141,8 @@ export async function saveTwilioCredentials(
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Get current credentials to merge
|
// Get current credentials to merge
|
||||||
const current = await getTwilioCredentials(brandId);
|
const current = await getTwilioCredentials(brandId);
|
||||||
|
|||||||
+16
-41
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { cookies } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { createServerClient } from "@supabase/ssr";
|
import { auth } from "@/lib/auth";
|
||||||
|
|
||||||
export type LoginWithPasswordResult =
|
export type LoginWithPasswordResult =
|
||||||
| { success: true; redirect: true }
|
| { success: true; redirect: true }
|
||||||
@@ -11,46 +11,21 @@ export async function loginWithPassword(
|
|||||||
email: string,
|
email: string,
|
||||||
password: string
|
password: string
|
||||||
): Promise<LoginWithPasswordResult> {
|
): 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;
|
if (!result?.user) {
|
||||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
return { success: false, error: "Invalid credentials" };
|
||||||
|
}
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseAnonKey) {
|
return { success: true, redirect: true };
|
||||||
return { success: false, error: "Server misconfiguration." };
|
} 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 { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
import { mockOrders, mockStops } from "@/lib/mock-data";
|
|
||||||
|
|
||||||
type AdminOrder = {
|
type AdminOrder = {
|
||||||
id: string;
|
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> {
|
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 adminUser = await getAdminUser();
|
||||||
const brandId = adminUser?.brand_id ?? null;
|
const brandId = adminUser?.brand_id ?? null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_admin_orders`,
|
`${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> {
|
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 adminUser = await getAdminUser();
|
||||||
const brandId = adminUser?.brand_id ?? null;
|
const brandId = adminUser?.brand_id ?? null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_admin_order_detail`,
|
`${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" };
|
return { success: false, error: "At least one item is required" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Build items for RPC (match checkout shape)
|
// Build items for RPC (match checkout shape)
|
||||||
const rpcItems = input.items.map((i) => ({
|
const rpcItems = input.items.map((i) => ({
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export async function createRefund(
|
|||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(`${supabaseUrl}/rest/v1/refunds`, {
|
const res = await fetch(`${supabaseUrl}/rest/v1/refunds`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export async function updateOrder(
|
|||||||
return { success: false, error: "Not authorized for this brand" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const patchData: Record<string, unknown> = {};
|
const patchData: Record<string, unknown> = {};
|
||||||
if (data.customer_name !== undefined) patchData.customer_name = data.customer_name;
|
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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const patchData: Record<string, unknown> = {};
|
const patchData: Record<string, unknown> = {};
|
||||||
if (data.quantity !== undefined) patchData.quantity = data.quantity;
|
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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(`${supabaseUrl}/rest/v1/order_items?id=eq.${itemId}`, {
|
const res = await fetch(`${supabaseUrl}/rest/v1/order_items?id=eq.${itemId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export type GetPaymentSettingsResult =
|
|||||||
| { success: false; error: string };
|
| { success: false; error: string };
|
||||||
|
|
||||||
export async function getPaymentSettings(brandId: string): Promise<GetPaymentSettingsResult> {
|
export async function getPaymentSettings(brandId: string): Promise<GetPaymentSettingsResult> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_payment_settings`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_payment_settings`,
|
`${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
|
// brand_admin: verify the order belongs to their brand
|
||||||
if (adminUser.role === "brand_admin" && adminUser.brand_id) {
|
if (adminUser.role === "brand_admin" && adminUser.brand_id) {
|
||||||
const brandRes = await fetch(
|
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) {
|
if (!order.brand_id && order.stop_id) {
|
||||||
const stopRes = await fetch(
|
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
|
// PATCH the order
|
||||||
const patchRes = await fetch(
|
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",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
...svcHeaders(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!),
|
...svcHeaders(process.env.NEXT_PUBLIC_API_ANON_KEY!),
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Prefer: "return=representation",
|
Prefer: "return=representation",
|
||||||
},
|
},
|
||||||
@@ -111,9 +111,9 @@ export async function markPickupComplete(
|
|||||||
// Emit pickup_completed event
|
// Emit pickup_completed event
|
||||||
// Need brand_id — get it from the order we just patched
|
// Need brand_id — get it from the order we just patched
|
||||||
const orderRes = await fetch(
|
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) {
|
if (orderRes.ok) {
|
||||||
@@ -121,11 +121,11 @@ export async function markPickupComplete(
|
|||||||
const orderBrandId = Array.isArray(orderData) && orderData[0]?.brand_id;
|
const orderBrandId = Array.isArray(orderData) && orderData[0]?.brand_id;
|
||||||
if (orderBrandId) {
|
if (orderBrandId) {
|
||||||
await fetch(
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...svcHeaders(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!),
|
...svcHeaders(process.env.NEXT_PUBLIC_API_ANON_KEY!),
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export async function deleteProduct(
|
|||||||
|
|
||||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_product`,
|
`${supabaseUrl}/rest/v1/rpc/delete_product`,
|
||||||
@@ -63,8 +63,8 @@ async function logAuditEvent(event: {
|
|||||||
new_data: Record<string, unknown>;
|
new_data: Record<string, unknown>;
|
||||||
brand_id: string | null;
|
brand_id: string | null;
|
||||||
}) {
|
}) {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
await fetch(
|
await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc.log_audit_event`,
|
`${supabaseUrl}/rest/v1/rpc.log_audit_event`,
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { getMockTableData } from "@/lib/mock-data";
|
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
|
||||||
|
|
||||||
export type CreateProductResult =
|
export type CreateProductResult =
|
||||||
| { success: true; id: string }
|
| { success: true; id: string }
|
||||||
| { success: false; error: string };
|
| { success: false; error: string };
|
||||||
@@ -31,27 +28,8 @@ export async function createProduct(
|
|||||||
return { success: false, error: "Not authorized for this brand" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useMockData) {
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const mockProducts = getMockTableData("products") as any[];
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
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 res = await fetch(`${supabaseUrl}/rest/v1/rpc/bulk_upsert_products`, {
|
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/bulk_upsert_products`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { getMockTableData } from "@/lib/mock-data";
|
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
|
||||||
|
|
||||||
export type UpdateProductResult =
|
export type UpdateProductResult =
|
||||||
| { success: true }
|
| { success: true }
|
||||||
| { success: false; error: string };
|
| { success: false; error: string };
|
||||||
@@ -32,12 +29,8 @@ export async function updateProduct(
|
|||||||
return { success: false, error: "Not authorized for this brand" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useMockData) {
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
return { success: true };
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
}
|
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
|
||||||
|
|
||||||
const res = await fetch(`${supabaseUrl}/rest/v1/products?id=eq.${productId}`, {
|
const res = await fetch(`${supabaseUrl}/rest/v1/products?id=eq.${productId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
import { uploadFile, BUCKETS, storageKeys } from "@/lib/storage";
|
||||||
// Product images bucket - UUID from Supabase
|
|
||||||
const PRODUCT_IMAGES_BUCKET_ID = "80aa01da-ab4b-44f8-b6e7-700552457e18";
|
|
||||||
|
|
||||||
export type UploadProductImageResult =
|
export type UploadProductImageResult =
|
||||||
| { success: true; imageUrl: string }
|
| { success: true; imageUrl: string }
|
||||||
@@ -25,42 +23,41 @@ export async function uploadProductImage(
|
|||||||
return { success: false, error: "File too large. Max 5MB." };
|
return { success: false, error: "File too large. Max 5MB." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = file.type.split("/")[1] === "jpeg" ? "jpg" : file.type.split("/")[1];
|
const rawExt = file.type.split("/")[1];
|
||||||
const path = `products/${productId}/${crypto.randomUUID()}.${ext}`;
|
const ext = rawExt === "jpeg" ? "jpg" : rawExt;
|
||||||
|
const targetProductId = productId === "__NEW__" ? "new" : productId;
|
||||||
|
const key = storageKeys.productImage(targetProductId, ext);
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
let url: string;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
try {
|
||||||
|
const res = await uploadFile({
|
||||||
const uploadRes = await fetch(
|
bucket: BUCKETS.PRODUCT_IMAGES,
|
||||||
`${supabaseUrl}/storage/v1/object/${PRODUCT_IMAGES_BUCKET_ID}/${path}`,
|
key,
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: { ...svcHeaders(supabaseKey), "Authorization": `Bearer ${supabaseKey}`, "Content-Type": `image/${ext}`, "x-upsert": "true" },
|
|
||||||
body: buffer,
|
body: buffer,
|
||||||
}
|
contentType: file.type,
|
||||||
);
|
});
|
||||||
|
url = res.url;
|
||||||
if (!uploadRes.ok) {
|
} catch (e) {
|
||||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
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 is "__NEW__", we just upload and return the URL (caller handles saving it)
|
||||||
if (productId === "__NEW__") {
|
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(
|
const patchRes = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
|
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
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: false, error: "Upload succeeded but failed to save image URL" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, imageUrl: publicUrl };
|
return { success: true, imageUrl: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProductImage(
|
export async function deleteProductImage(
|
||||||
@@ -77,8 +74,8 @@ export async function deleteProductImage(
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const patchRes = await fetch(
|
const patchRes = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
|
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
export type DateRange = { start: string; end: string };
|
export type DateRange = { start: string; end: string };
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
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!;
|
const SUPABASE_PAT = process.env.SUPABASE_PAT!;
|
||||||
|
|
||||||
async function adminFetch(endpoint: string, options?: RequestInit) {
|
async function adminFetch(endpoint: string, options?: RequestInit) {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export async function toggleBrandFeature(
|
|||||||
return { success: false, error: "Not authorized for this brand" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/set_brand_feature`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/update_shipping_order`,
|
`${supabaseUrl}/rest/v1/rpc/update_shipping_order`,
|
||||||
@@ -70,8 +70,8 @@ export async function getShippingOrders(): Promise<GetShippingOrdersResult> {
|
|||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_shipping_orders`,
|
`${supabaseUrl}/rest/v1/rpc/get_shipping_orders`,
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ async function getFedExTokenForCreate(
|
|||||||
brandId: string | null,
|
brandId: string | null,
|
||||||
adminUserId: string | null
|
adminUserId: string | null
|
||||||
): Promise<{ accessToken: string } | { error: string }> {
|
): Promise<{ accessToken: string } | { error: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Try to get from shipping_settings
|
// Try to get from shipping_settings
|
||||||
const settingsRes = await fetch(
|
const settingsRes = await fetch(
|
||||||
@@ -88,8 +88,8 @@ async function getFedExTokenForCreate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function isShipmentPerishable(orderId: string): Promise<boolean> {
|
async function isShipmentPerishable(orderId: string): Promise<boolean> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_order_items_perishable?p_order_id=${encodeURIComponent(orderId)}`,
|
`${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" };
|
return { success: false, error: "Not authorized to create shipments" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Fetch order
|
// Fetch order
|
||||||
const orderRes = await fetch(
|
const orderRes = await fetch(
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ async function isOrderPerishable(
|
|||||||
orderId: string,
|
orderId: string,
|
||||||
brandId: string | null
|
brandId: string | null
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_order_items_perishable?p_order_id=${encodeURIComponent(orderId)}`,
|
`${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" };
|
return { success: false, error: "Not authorized to view shipping rates" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Fetch order + brand + address
|
// Fetch order + brand + address
|
||||||
const orderRes = await fetch(
|
const orderRes = await fetch(
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ async function getFedExToken(apiKey: string, apiSecret: string, useProduction: b
|
|||||||
// ── Get Settings ─────────────────────────────────────────────────────────────
|
// ── Get Settings ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getShippingSettings(brandId: string): Promise<GetShippingSettingsResult> {
|
export async function getShippingSettings(brandId: string): Promise<GetShippingSettingsResult> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/shipping_settings?brand_id=eq.${encodeURIComponent(brandId)}&limit=1`,
|
`${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" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Get existing settings to get ID for update
|
// Get existing settings to get ID for update
|
||||||
const existing = await fetch(
|
const existing = await fetch(
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ export async function syncInventoryToSquare(
|
|||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const synced = 0;
|
const synced = 0;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Build product name → quantity map
|
// Build product name → quantity map
|
||||||
const itemQtyMap = new Map(items.map((i) => [i.productId, i.quantity]));
|
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 errors: string[] = [];
|
||||||
const synced = 0;
|
const synced = 0;
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch Square inventory counts for the location
|
// Fetch Square inventory counts for the location
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ export async function syncOrdersFromSquare(brandId: string): Promise<SyncResult>
|
|||||||
|
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
let synced = 0;
|
let synced = 0;
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Determine sync start time — last sync or 30 days ago
|
// Determine sync start time — last sync or 30 days ago
|
||||||
const since = settings.square_last_sync_at
|
const since = settings.square_last_sync_at
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ export async function syncProductsToSquare(brandId: string): Promise<SyncResult>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch wholesale products via RPC (avoids rc_product_id bug in direct query)
|
// Fetch wholesale products via RPC (avoids rc_product_id bug in direct query)
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const rpcRes = await fetch(
|
const rpcRes = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
||||||
@@ -247,8 +247,8 @@ export async function syncProductsFromSquare(brandId: string): Promise<SyncResul
|
|||||||
|
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
let synced = 0;
|
let synced = 0;
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
let cursor: string | undefined;
|
let cursor: string | undefined;
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ export async function getSyncLog(brandId: string): Promise<{
|
|||||||
return { success: false, logs: [] };
|
return { success: false, logs: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/square_sync_log?brand_id=eq.${brandId}&order=created_at.desc&limit=10`,
|
`${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" };
|
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
|
// 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
|
// bypassed. This fixes the prior 42501 RLS violation that came from doing
|
||||||
// direct REST inserts against the RLS-blocked `stops` table.
|
// 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) => ({
|
const rows = stops.map((s) => ({
|
||||||
city: s.city,
|
city: s.city,
|
||||||
@@ -74,8 +74,8 @@ export async function publishStop(
|
|||||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_stops) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_stops) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const res = await fetch(`${supabaseUrl}/rest/v1/stops?id=eq.${stopId}`, {
|
const res = await fetch(`${supabaseUrl}/rest/v1/stops?id=eq.${stopId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -105,8 +105,8 @@ export type StopForSitemap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Get all active stops with their brand slug
|
// Get all active stops with their brand slug
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -127,7 +127,7 @@ export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
|||||||
* Fetch active stops for a brand by slug.
|
* Fetch active stops for a brand by slug.
|
||||||
* Public action used by the storefront stop pages (e.g. /tuxedo/stops,
|
* Public action used by the storefront stop pages (e.g. /tuxedo/stops,
|
||||||
* /indian-river-direct/stops) — replaces the previous client-side
|
* /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.
|
* the browser for those routes.
|
||||||
*
|
*
|
||||||
* Cached at the edge for 5 minutes; invalidated by `revalidateTag('stops')`
|
* Cached at the edge for 5 minutes; invalidated by `revalidateTag('stops')`
|
||||||
@@ -150,8 +150,8 @@ export async function getPublicStopsForBrand(
|
|||||||
): Promise<PublicStop[]> {
|
): Promise<PublicStop[]> {
|
||||||
if (!brandSlug) return [];
|
if (!brandSlug) return [];
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_public_stops_for_brand`,
|
`${supabaseUrl}/rest/v1/rpc/get_public_stops_for_brand`,
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
import { revalidateTag } from "next/cache";
|
import { revalidateTag } from "next/cache";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
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 =
|
export type CreateStopResult =
|
||||||
| { success: true; id: string }
|
| { success: true; id: string }
|
||||||
@@ -33,18 +30,13 @@ export async function createStop(
|
|||||||
return { success: false, error: "Not authorized for this brand" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useMockData) {
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const mockStops = getMockTableData("stops") as Array<{ id: string }>;
|
|
||||||
return { success: true, id: `mock-stop-${Date.now()}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
||||||
// Preferred path: anon key + SECURITY DEFINER RPC (bypasses RLS, works even if
|
// Preferred path: anon key + SECURITY DEFINER RPC (bypasses RLS, works even if
|
||||||
// service role key is absent at runtime). See:
|
// 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)
|
// 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)
|
// api/ADMIN_CREATE_STOP_FIX.sql (exact prompt SQL for SQL editor)
|
||||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const anonKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/admin_create_stop`, {
|
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/admin_create_stop`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -114,7 +106,7 @@ export async function createStop(
|
|||||||
" 1. On a machine that can reach your database (normal laptop usually works):\n" +
|
" 1. On a machine that can reach your database (normal laptop usually works):\n" +
|
||||||
" node scripts/apply-admin-create-stop.js\n" +
|
" node scripts/apply-admin-create-stop.js\n" +
|
||||||
" 2. Or: npm run migrate:one 202\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.",
|
"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 { svcHeaders } from "@/lib/svc-headers";
|
||||||
import { logAuditEvent } from "@/actions/audit";
|
import { logAuditEvent } from "@/actions/audit";
|
||||||
|
|
||||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
|
||||||
|
|
||||||
export type UpdateStopResult =
|
export type UpdateStopResult =
|
||||||
| { success: true }
|
| { success: true }
|
||||||
| { success: false; error: string };
|
| { success: false; error: string };
|
||||||
@@ -33,12 +31,8 @@ export async function updateStop(
|
|||||||
return { success: false, error: "Not authorized for this brand" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useMockData) {
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
return { success: true };
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
}
|
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
|
||||||
|
|
||||||
const slug = `${data.city.toLowerCase().replace(/\s+/g, "-")}-${data.date}`;
|
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();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return { is_connected: false, error: "Not authenticated" };
|
if (!adminUser) return { is_connected: false, error: "Not authenticated" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Get brand's payment settings
|
// Get brand's payment settings
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -183,8 +183,8 @@ export async function saveStripeConnectAccount(brandId: string, accountId: strin
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Save to payment_settings via RPC
|
// Save to payment_settings via RPC
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -220,8 +220,8 @@ export async function disconnectStripeConnect(brandId: string): Promise<{
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/disconnect_stripe_connect`,
|
`${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> {
|
async function getBrandTaxSettings(brandId: string): Promise<BrandTaxSettings | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_brand_settings`,
|
`${supabaseUrl}/rest/v1/rpc/get_brand_settings`,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
function rpcBody(body: Record<string, unknown>) {
|
function rpcBody(body: Record<string, unknown>) {
|
||||||
return JSON.stringify(body);
|
return JSON.stringify(body);
|
||||||
|
|||||||
@@ -2,13 +2,9 @@
|
|||||||
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
import { mockWorkers, mockTasks, mockTimeEntries } from "@/lib/mock-data";
|
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
// Mock mode flag - only enabled when explicitly set
|
|
||||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
|
||||||
|
|
||||||
function rpcBody(body: Record<string, unknown>) {
|
function rpcBody(body: Record<string, unknown>) {
|
||||||
return JSON.stringify(body);
|
return JSON.stringify(body);
|
||||||
@@ -62,20 +58,6 @@ export type TimeSummary = {
|
|||||||
// ── Workers ───────────────────────────────────────────────────────────────────
|
// ── Workers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getTimeTrackingWorkers(brandId: string): Promise<TimeWorker[]> {
|
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(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_workers`,
|
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_workers`,
|
||||||
{
|
{
|
||||||
@@ -182,16 +164,6 @@ export async function deleteTimeWorker(workerId: string): Promise<{ success: boo
|
|||||||
// ── Tasks ────────────────────────────────────────────────────────────────────
|
// ── Tasks ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getTimeTrackingTasks(brandId: string, activeOnly = false): Promise<TimeTask[]> {
|
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(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_tasks`,
|
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_tasks`,
|
||||||
{
|
{
|
||||||
@@ -282,30 +254,6 @@ export async function getWorkerTimeLogs(
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<TimeLog[]> {
|
): 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(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_worker_time_logs`,
|
`${supabaseUrl}/rest/v1/rpc/get_worker_time_logs`,
|
||||||
{
|
{
|
||||||
@@ -380,39 +328,6 @@ export async function getTimeTrackingSummary(
|
|||||||
start: string,
|
start: string,
|
||||||
end: string
|
end: string
|
||||||
): Promise<TimeSummary> {
|
): 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(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_summary`,
|
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_summary`,
|
||||||
{
|
{
|
||||||
@@ -447,26 +362,6 @@ export type TimeTrackingSettings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getTimeTrackingSettings(brandId: string): Promise<TimeTrackingSettings | null> {
|
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(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_settings`,
|
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_settings`,
|
||||||
{
|
{
|
||||||
@@ -552,10 +447,6 @@ export async function getTimeTrackingNotificationLog(
|
|||||||
brandId: string,
|
brandId: string,
|
||||||
limit = 100
|
limit = 100
|
||||||
): Promise<NotificationLogEntry[]> {
|
): Promise<NotificationLogEntry[]> {
|
||||||
if (useMockData) {
|
|
||||||
// Return empty log in mock mode
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_notification_log`,
|
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_notification_log`,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
export type OvertimeCheckResult = {
|
export type OvertimeCheckResult = {
|
||||||
sent: boolean;
|
sent: boolean;
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export async function createWaterHeadgate(brandId: string, name: string, unit: s
|
|||||||
return { success: false, error: "Not authorized" };
|
return { success: false, error: "Not authorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; // prefer service for admin muts
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY!; // prefer service for admin muts
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/create_water_headgate`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/update_water_headgate`,
|
`${supabaseUrl}/rest/v1/rpc/update_water_headgate`,
|
||||||
@@ -111,8 +111,8 @@ export async function updateWaterHeadgate(
|
|||||||
// ── Irrigator Admin ─────────────────────────────────────────
|
// ── Irrigator Admin ─────────────────────────────────────────
|
||||||
|
|
||||||
export async function getWaterIrrigators(brandId: string): Promise<Irrigator[]> {
|
export async function getWaterIrrigators(brandId: string): Promise<Irrigator[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_users`,
|
`${supabaseUrl}/rest/v1/rpc/get_water_users`,
|
||||||
@@ -142,8 +142,8 @@ export async function createWaterUser(
|
|||||||
role: "irrigator" | "water_admin",
|
role: "irrigator" | "water_admin",
|
||||||
lang: string = "en"
|
lang: string = "en"
|
||||||
): Promise<{ success: boolean; user?: Irrigator; pin?: string; error?: string }> {
|
): Promise<{ success: boolean; user?: Irrigator; pin?: string; error?: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/create_water_user`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/update_water_user`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/reset_water_user_pin`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_water_user`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_water_headgate`,
|
`${supabaseUrl}/rest/v1/rpc/delete_water_headgate`,
|
||||||
@@ -308,8 +308,8 @@ export async function deleteWaterHeadgate(headgateId: string): Promise<{ success
|
|||||||
// ── Entries ────────────────────────────────────────────────
|
// ── Entries ────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getWaterEntries(brandId: string, limit = 50): Promise<WaterEntry[]> {
|
export async function getWaterEntries(brandId: string, limit = 50): Promise<WaterEntry[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_entries`,
|
`${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[]> {
|
export async function getWaterHeadgatesAdmin(brandId: string): Promise<Headgate[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_headgates_admin`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/regenerate_headgate_token`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/update_water_entry`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_water_entry`,
|
`${supabaseUrl}/rest/v1/rpc/delete_water_entry`,
|
||||||
@@ -434,8 +434,8 @@ export async function deleteWaterEntry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getWaterEntryById(entryId: string): Promise<WaterEntry | null> {
|
export async function getWaterEntryById(entryId: string): Promise<WaterEntry | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_entry_by_id`,
|
`${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> {
|
export async function getWaterDisplaySummary(brandId: string): Promise<WaterDisplaySummary | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_display_summary`,
|
`${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[]> {
|
export async function getWaterAlertLog(brandId: string, limit = 50): Promise<AlertLogEntry[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_alert_log`,
|
`${supabaseUrl}/rest/v1/rpc/get_water_alert_log`,
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ type HeadgatesResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getWaterHeadgates(brandId: string, activeOnly = false): Promise<Headgate[]> {
|
export async function getWaterHeadgates(brandId: string, activeOnly = false): Promise<Headgate[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_headgates`,
|
`${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> {
|
export async function verifyWaterPin(brandId: string, pin: string): Promise<VerifyPinResult> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/verify_water_pin`,
|
`${supabaseUrl}/rest/v1/rpc/verify_water_pin`,
|
||||||
@@ -134,8 +134,8 @@ export async function submitWaterEntry(
|
|||||||
return { success: false, error: "Not logged in" };
|
return { success: false, error: "Not logged in" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/submit_water_entry`,
|
`${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;
|
if (!sessionId) return null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
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
|
// Use get_water_admin_session RPC (SECURITY DEFINER) to avoid direct water_sessions + water_users JOIN
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export type WaterAdminSettings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getWaterAdminSettings(brandId: string): Promise<WaterAdminSettings | null> {
|
export async function getWaterAdminSettings(brandId: string): Promise<WaterAdminSettings | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_admin_settings`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_water_log) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
let pinHash: string | null = null;
|
let pinHash: string | null = null;
|
||||||
if (settings.pin) {
|
if (settings.pin) {
|
||||||
@@ -88,8 +88,8 @@ export async function verifyWaterAdminPin(
|
|||||||
brandId: string,
|
brandId: string,
|
||||||
pin: string
|
pin: string
|
||||||
): Promise<{ success: boolean; session_id?: string; error?: string }> {
|
): Promise<{ success: boolean; session_id?: string; error?: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const settingsRes = await fetch(
|
const settingsRes = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_water_admin_settings`,
|
`${supabaseUrl}/rest/v1/rpc/get_water_admin_settings`,
|
||||||
|
|||||||
+41
-110
@@ -1,8 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { cookies } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { auth } from "@/lib/auth";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
|
||||||
|
|
||||||
export type WholesaleLoginResult =
|
export type WholesaleLoginResult =
|
||||||
| { success: true; token: string; userId: string; customerId: string }
|
| { 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 email = formData.get("email") as string;
|
||||||
const password = formData.get("password") as string;
|
const password = formData.get("password") as string;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
try {
|
||||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const hdrs = await headers();
|
||||||
|
const result = await auth.api.signInEmail({
|
||||||
|
body: { email, password },
|
||||||
|
headers: hdrs,
|
||||||
|
asResponse: false,
|
||||||
|
});
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
if (!result?.user) {
|
||||||
const request = new NextRequest("http://localhost/wholesale/login", {
|
return { success: false, error: "Invalid credentials" };
|
||||||
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 no brand_id known, try all brands — just use first active one found
|
const cookieStore = await cookies();
|
||||||
// For now, set the cookie with user_id and a placeholder; portal will resolve proper customer
|
// Better Auth sets its own session cookie (rc_session_token).
|
||||||
response.cookies.set("wholesale_session", JSON.stringify({
|
// Mark wholesale session for portal routing.
|
||||||
user_id: data.user.id,
|
cookieStore.set("wholesale_session", JSON.stringify({
|
||||||
access_token: token,
|
user_id: result.user.id,
|
||||||
}), {
|
}), {
|
||||||
path: "/",
|
path: "/",
|
||||||
maxAge: 3600 * 24 * 7,
|
maxAge: 3600 * 24 * 7,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also set the standard auth token for RPC calls
|
return {
|
||||||
response.cookies.set("sb-wnzkhezyhnfzhkhiflrp-auth-token", token, {
|
success: true,
|
||||||
path: "/",
|
token: "better-auth-session", // session lives in cookie
|
||||||
maxAge: 3600 * 24 * 7,
|
userId: result.user.id,
|
||||||
sameSite: "lax",
|
customerId: "pending", // resolved by portal page on load
|
||||||
secure: process.env.NODE_ENV === "production",
|
};
|
||||||
httpOnly: false,
|
} catch (e: unknown) {
|
||||||
});
|
const message = e instanceof Error ? e.message : "Invalid credentials";
|
||||||
|
return { success: false, error: message };
|
||||||
return {
|
}
|
||||||
success: true,
|
|
||||||
token,
|
|
||||||
userId: data.user.id,
|
|
||||||
customerId: "pending", // resolved by portal page on load
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function wholesaleLogoutAction() {
|
export async function wholesaleLogoutAction() {
|
||||||
|
try {
|
||||||
|
const hdrs = await headers();
|
||||||
|
await auth.api.signOut({ headers: hdrs });
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const request = new NextRequest("http://localhost/wholesale/portal", {
|
cookieStore.delete("wholesale_session");
|
||||||
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();
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,8 @@ export async function registerWholesaleCustomer(params: {
|
|||||||
email: string;
|
email: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
}): Promise<{ success: boolean; error?: string; requiresApproval?: boolean }> {
|
}): Promise<{ success: boolean; error?: string; requiresApproval?: boolean }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/register_wholesale_customer`,
|
`${supabaseUrl}/rest/v1/rpc/register_wholesale_customer`,
|
||||||
@@ -54,8 +54,8 @@ export async function getPendingWholesaleRegistrations(brandId: string) {
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
if (!adminUser.can_manage_orders) return [];
|
if (!adminUser.can_manage_orders) return [];
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_pending_wholesale_registrations`,
|
`${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" };
|
return { success: false, error: "Not authorized to operate on this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/approve_wholesale_registration`,
|
`${supabaseUrl}/rest/v1/rpc/approve_wholesale_registration`,
|
||||||
@@ -128,8 +128,8 @@ export async function getWholesaleCustomerByUser(
|
|||||||
role: string;
|
role: string;
|
||||||
brand_id: string;
|
brand_id: string;
|
||||||
} | null> {
|
} | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_by_user`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_by_user`,
|
||||||
@@ -162,8 +162,8 @@ export async function getWholesaleCustomer(
|
|||||||
role: string;
|
role: string;
|
||||||
brand_id: string;
|
brand_id: string;
|
||||||
} | null> {
|
} | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
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`,
|
`${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) {
|
export async function getWholesaleProducts(brandId: string) {
|
||||||
// Uses SECURITY DEFINER RPC — no auth required for admins, anon key works for customers too
|
// Uses SECURITY DEFINER RPC — no auth required for admins, anon key works for customers too
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
||||||
@@ -217,8 +217,8 @@ export async function submitWholesaleOrder(params: {
|
|||||||
orderTotal?: number;
|
orderTotal?: number;
|
||||||
idempotent?: boolean;
|
idempotent?: boolean;
|
||||||
}> {
|
}> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
// Generate UUID at the start of checkout — before RPC call for true idempotency
|
// Generate UUID at the start of checkout — before RPC call for true idempotency
|
||||||
const checkoutSessionId = params.checkoutSessionId ?? crypto.randomUUID();
|
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) {
|
export async function enqueueWholesaleWebhookForOrderCreated(orderId: string, brandId: string, customerId: string, invoiceNumber: string | null, subtotal: number) {
|
||||||
try {
|
try {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||||
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||||
@@ -341,8 +341,8 @@ export type WholesalePricingOverride = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getWholesaleCustomerPricing(customerId: string): Promise<WholesalePricingOverride[]> {
|
export async function getWholesaleCustomerPricing(customerId: string): Promise<WholesalePricingOverride[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_pricing`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_pricing`,
|
||||||
@@ -365,8 +365,8 @@ export async function upsertWholesaleCustomerPricing(params: {
|
|||||||
productId: string;
|
productId: string;
|
||||||
customUnitPrice: number;
|
customUnitPrice: number;
|
||||||
}): Promise<{ success: boolean; error?: string }> {
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_customer_pricing`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_customer_pricing`,
|
||||||
@@ -392,8 +392,8 @@ export async function deleteWholesaleCustomerPricing(params: {
|
|||||||
customerId: string;
|
customerId: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
}): Promise<{ success: boolean; error?: string }> {
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_customer_pricing`,
|
`${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[]> {
|
export async function getWholesaleCustomerOrders(customerId: string): Promise<WholesaleCustomerOrder[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_orders`,
|
`${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 [];
|
if (!adminUser) return [];
|
||||||
const bid = resolveBrandId(adminUser, brandId);
|
const bid = resolveBrandId(adminUser, brandId);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_orders`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_orders`,
|
||||||
@@ -211,8 +211,8 @@ export async function getWholesalePickupOrders(brandId?: string): Promise<Wholes
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
const bid = resolveBrandId(adminUser, brandId);
|
const bid = resolveBrandId(adminUser, brandId);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_pickup_orders`,
|
`${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);
|
const { brandId: resolved, error } = enforceBrandScope(adminUser, brandId);
|
||||||
if (error) return { success: false, error };
|
if (error) return { success: false, error };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/mark_wholesale_order_fulfilled`,
|
`${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) {
|
async function enqueueWholesaleWebhookForOrderFulfilled(orderId: string, brandId?: string) {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||||
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||||
@@ -308,8 +308,8 @@ export async function updateWholesaleOrderStatus(
|
|||||||
const { error } = enforceBrandScope(adminUser, brandId);
|
const { error } = enforceBrandScope(adminUser, brandId);
|
||||||
if (error) return { success: false, error };
|
if (error) return { success: false, error };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/update_wholesale_order_status`,
|
`${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);
|
const { error } = enforceBrandScope(adminUser, brandId);
|
||||||
if (error) return { success: false, error };
|
if (error) return { success: false, error };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_order`,
|
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_order`,
|
||||||
@@ -362,8 +362,8 @@ export async function deleteWholesaleCustomer(customerId: string, brandId?: stri
|
|||||||
const { error } = enforceBrandScope(adminUser, brandId);
|
const { error } = enforceBrandScope(adminUser, brandId);
|
||||||
if (error) return { success: false, error };
|
if (error) return { success: false, error };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_customer`,
|
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_customer`,
|
||||||
@@ -391,8 +391,8 @@ export async function deleteWholesaleProduct(productId: string, brandId?: string
|
|||||||
const { error } = enforceBrandScope(adminUser, brandId);
|
const { error } = enforceBrandScope(adminUser, brandId);
|
||||||
if (error) return { success: false, error };
|
if (error) return { success: false, error };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_product`,
|
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_product`,
|
||||||
@@ -419,8 +419,8 @@ export async function getWholesaleCustomers(brandId?: string): Promise<Wholesale
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
const bid = resolveBrandId(adminUser, brandId);
|
const bid = resolveBrandId(adminUser, brandId);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customers`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customers`,
|
||||||
@@ -461,8 +461,8 @@ export async function saveWholesaleCustomer(params: {
|
|||||||
const { error } = enforceBrandScope(adminUser, params.brandId);
|
const { error } = enforceBrandScope(adminUser, params.brandId);
|
||||||
if (error) return { success: false, error };
|
if (error) return { success: false, error };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_customer`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_customer`,
|
||||||
@@ -503,8 +503,8 @@ export async function getWholesaleProducts(brandId?: string): Promise<WholesaleP
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
const bid = resolveBrandId(adminUser, brandId);
|
const bid = resolveBrandId(adminUser, brandId);
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
|
||||||
@@ -548,8 +548,8 @@ export async function saveWholesaleProduct(params: {
|
|||||||
const { error } = enforceBrandScope(adminUser, params.brandId);
|
const { error } = enforceBrandScope(adminUser, params.brandId);
|
||||||
if (error) return { success: false, error };
|
if (error) return { success: false, error };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_product`,
|
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_product`,
|
||||||
@@ -593,8 +593,8 @@ export async function getWholesaleSettings(brandId?: string): Promise<WholesaleS
|
|||||||
if (!adminUser) return null;
|
if (!adminUser) return null;
|
||||||
const bid = brandId ?? adminUser.brand_id ?? null;
|
const bid = brandId ?? adminUser.brand_id ?? null;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_settings`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_settings`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/record_wholesale_deposit`,
|
`${supabaseUrl}/rest/v1/rpc/record_wholesale_deposit`,
|
||||||
@@ -716,8 +716,8 @@ export async function recordWholesaleDeposit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function enqueueWholesaleWebhookForDepositRecorded(orderId: string, amount: number, brandId?: string) {
|
async function enqueueWholesaleWebhookForDepositRecorded(orderId: string, amount: number, brandId?: string) {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY!;
|
||||||
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/bulk_fulfill_wholesale_orders`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/bulk_record_wholesale_deposit`,
|
`${supabaseUrl}/rest/v1/rpc/bulk_record_wholesale_deposit`,
|
||||||
@@ -824,8 +824,8 @@ export type WholesaleNotification = {
|
|||||||
export async function getWholesaleNotificationStats(
|
export async function getWholesaleNotificationStats(
|
||||||
brandId: string
|
brandId: string
|
||||||
): Promise<{ pending: number; sent: number; failed: number; total: number }> {
|
): Promise<{ pending: number; sent: number; failed: number; total: number }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_notification_stats`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_notification_stats`,
|
||||||
@@ -851,8 +851,8 @@ export async function getWholesalePendingNotifications(
|
|||||||
if (!adminUser) return [];
|
if (!adminUser) return [];
|
||||||
if (!adminUser.can_manage_orders) return [];
|
if (!adminUser.can_manage_orders) return [];
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_wholesale_pending_notifications`,
|
`${supabaseUrl}/rest/v1/rpc/get_wholesale_pending_notifications`,
|
||||||
@@ -878,8 +878,8 @@ export async function markWholesaleNotificationSent(
|
|||||||
if (!adminUser) return { success: false };
|
if (!adminUser) return { success: false };
|
||||||
if (!adminUser.can_manage_orders) return { success: false };
|
if (!adminUser.can_manage_orders) return { success: false };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/mark_wholesale_notification_sent`,
|
`${supabaseUrl}/rest/v1/rpc/mark_wholesale_notification_sent`,
|
||||||
@@ -907,8 +907,8 @@ export async function enqueueWholesaleNotification(params: {
|
|||||||
bodyHtml?: string;
|
bodyHtml?: string;
|
||||||
bodyText?: string;
|
bodyText?: string;
|
||||||
}): Promise<{ success: boolean; error?: string }> {
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_notification`,
|
`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_notification`,
|
||||||
@@ -949,8 +949,8 @@ export type WebhookSettings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getWebhookSettings(brandId: string): Promise<WebhookSettings | null> {
|
export async function getWebhookSettings(brandId: string): Promise<WebhookSettings | null> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/wholesale_webhook_settings?brand_id=eq.${brandId}&select=*`,
|
`${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) return { success: false, error: "Not authenticated" };
|
||||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
const serviceKey = process.env.POSTGREST_SERVICE_KEY!;
|
||||||
|
|
||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
brand_id: params.brandId,
|
brand_id: params.brandId,
|
||||||
@@ -1007,8 +1007,8 @@ export async function enqueueWholesaleWebhook(
|
|||||||
payload: Record<string, unknown> | null = null,
|
payload: Record<string, unknown> | null = null,
|
||||||
brandId?: string
|
brandId?: string
|
||||||
): Promise<{ success: boolean; logId?: string }> {
|
): Promise<{ success: boolean; logId?: string }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`,
|
`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`,
|
||||||
@@ -1041,8 +1041,8 @@ export async function getRecentWebhookActivity(brandId: string, limit = 10): Pro
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
response: string | null;
|
response: string | null;
|
||||||
}>> {
|
}>> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL!;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY ?? process.env.NEXT_PUBLIC_API_ANON_KEY!;
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/wholesale_sync_log?brand_id=eq.${brandId}&order=created_at.desc&limit=${limit}`,
|
`${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;
|
let adminUsersResult: string | null = null;
|
||||||
|
|
||||||
if (rcAuthUid) {
|
if (rcAuthUid) {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
const serviceKey = process.env.POSTGREST_SERVICE_KEY;
|
||||||
if (supabaseUrl && serviceKey) {
|
if (supabaseUrl && serviceKey) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { AdminUserRow } from "@/actions/admin/users";
|
import { AdminUserRow } from "@/actions/admin/users";
|
||||||
import { logUserActivity } from "@/actions/admin/audit";
|
import { logUserActivity } from "@/actions/admin/audit";
|
||||||
|
import { updateAdminProfileAction } from "@/actions/admin/profile";
|
||||||
|
|
||||||
type ProfilePageProps = {
|
type ProfilePageProps = {
|
||||||
currentUser: AdminUserRow;
|
currentUser: AdminUserRow;
|
||||||
@@ -26,13 +27,13 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const { error: rpcError } = await supabase.rpc("update_admin_user", {
|
const result = await updateAdminProfileAction(
|
||||||
p_id: currentUser.id,
|
currentUser.id,
|
||||||
p_display_name: displayName || null,
|
displayName || null,
|
||||||
p_phone_number: phoneNumber || null,
|
phoneNumber || null
|
||||||
});
|
);
|
||||||
if (rpcError) {
|
if (!result.success) {
|
||||||
setError(rpcError.message);
|
setError(result.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await logUserActivity({
|
await logUserActivity({
|
||||||
@@ -51,11 +52,11 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
|||||||
setChangingEmail(true);
|
setChangingEmail(true);
|
||||||
setEmailError(null);
|
setEmailError(null);
|
||||||
try {
|
try {
|
||||||
const { error: updateError } = await supabase.auth.updateUser({
|
const { error: updateError } = await authClient.changeEmail({
|
||||||
email: newEmail,
|
newEmail: newEmail,
|
||||||
});
|
});
|
||||||
if (updateError) {
|
if (updateError) {
|
||||||
setEmailError(updateError.message);
|
setEmailError(updateError.message ?? "Failed to change email");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await logUserActivity({
|
await logUserActivity({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getAdminOrders } from "@/actions/orders";
|
|||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
import { PageHeader } from "@/components/admin/design-system";
|
import { PageHeader } from "@/components/admin/design-system";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
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)
|
// 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 }> = [];
|
let brandProducts: Array<{ id: string; name: string; price: number; type?: string | null; active?: boolean }> = [];
|
||||||
try {
|
try {
|
||||||
let prodQuery = supabase
|
let prodQuery = api
|
||||||
.from("products")
|
.from("products")
|
||||||
.select("id, name, price, type, active")
|
.select("id, name, price, type, active")
|
||||||
.eq("active", true)
|
.eq("active", true)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { isFeatureEnabled } from "@/lib/feature-flags";
|
import { isFeatureEnabled } from "@/lib/feature-flags";
|
||||||
import { getBillingOverview } from "@/actions/billing/billing-overview";
|
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".
|
// (0/0/0) usage values and contradicts the billing page's "Products 1/25".
|
||||||
let dashboardBrandId: string | null = adminUser?.brand_id ?? null;
|
let dashboardBrandId: string | null = adminUser?.brand_id ?? null;
|
||||||
if (!dashboardBrandId && adminUser?.role === "platform_admin") {
|
if (!dashboardBrandId && adminUser?.role === "platform_admin") {
|
||||||
const { data: firstBrand } = await supabase
|
const { data: firstBrand } = await api
|
||||||
.from("brands")
|
.from("brands")
|
||||||
.select("id")
|
.select("id")
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import ProductEditForm from "@/components/admin/ProductEditForm";
|
import ProductEditForm from "@/components/admin/ProductEditForm";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
@@ -14,10 +14,10 @@ type ProductDetailPageProps = {
|
|||||||
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
|
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
const [{ data: product, error }, { data: brands }] = await Promise.all([
|
const [{ data: product, error }, { data: brands }] = (await Promise.all([
|
||||||
supabase.from("products").select("*, brands(name, slug)").eq("id", id).single(),
|
api.from("products").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();
|
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 { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
import ProductsClient from "@/components/admin/ProductsClient";
|
import ProductsClient from "@/components/admin/ProductsClient";
|
||||||
@@ -31,7 +31,7 @@ export default async function AdminProductsPage() {
|
|||||||
|
|
||||||
const brandId = adminUser.brand_id;
|
const brandId = adminUser.brand_id;
|
||||||
|
|
||||||
let query = supabase
|
let query = api
|
||||||
.from("products")
|
.from("products")
|
||||||
.select(`
|
.select(`
|
||||||
id,
|
id,
|
||||||
@@ -52,7 +52,7 @@ export default async function AdminProductsPage() {
|
|||||||
query = query.eq("brand_id", adminUser.brand_id);
|
query = query.eq("brand_id", adminUser.brand_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: products, error } = await query;
|
const { data: products, error } = (await query) as any;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
import ReportsDashboard from "@/components/admin/ReportsDashboard";
|
import ReportsDashboard from "@/components/admin/ReportsDashboard";
|
||||||
@@ -12,7 +12,7 @@ export default async function ReportsPage() {
|
|||||||
|
|
||||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||||
|
|
||||||
const { data: brands } = await supabase
|
const { data: brands } = await api
|
||||||
.from("brands")
|
.from("brands")
|
||||||
.select("id, name")
|
.select("id, name")
|
||||||
.order("name");
|
.order("name");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { getBillingOverview } from "@/actions/billing/billing-overview";
|
import { getBillingOverview } from "@/actions/billing/billing-overview";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
@@ -18,7 +18,7 @@ export default async function BillingPage({ params }: Props) {
|
|||||||
|
|
||||||
let resolvedBrandId = effectiveBrandId;
|
let resolvedBrandId = effectiveBrandId;
|
||||||
if (isPlatformAdmin && !resolvedBrandId) {
|
if (isPlatformAdmin && !resolvedBrandId) {
|
||||||
const { data: firstBrand } = await supabase
|
const { data: firstBrand } = await api
|
||||||
.from("brands")
|
.from("brands")
|
||||||
.select("id")
|
.select("id")
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import IntegrationsClientPage from "./IntegrationsClientPage";
|
import IntegrationsClientPage from "./IntegrationsClientPage";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
@@ -18,7 +18,7 @@ export default async function IntegrationsPage() {
|
|||||||
|
|
||||||
// Platform admins: fetch all brands for the picker
|
// Platform admins: fetch all brands for the picker
|
||||||
const brands = isPlatformAdmin
|
const brands = isPlatformAdmin
|
||||||
? (await supabase.from("brands").select("id, name").order("name")).data ?? []
|
? (await api.from("brands").select("id, name").order("name")).data ?? []
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { getShippingSettings } from "@/actions/shipping/settings";
|
import { getShippingSettings } from "@/actions/shipping/settings";
|
||||||
import ShippingSettingsForm from "@/components/admin/ShippingSettingsForm";
|
import ShippingSettingsForm from "@/components/admin/ShippingSettingsForm";
|
||||||
@@ -17,7 +17,7 @@ export default async function ShippingSettingsPage() {
|
|||||||
|
|
||||||
// Platform admins: fetch all brands for the picker
|
// Platform admins: fetch all brands for the picker
|
||||||
const brands = isPlatformAdmin
|
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 ?? "");
|
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 StopEditForm from "@/components/admin/StopEditForm";
|
||||||
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
||||||
import MessageCustomersSection from "@/components/admin/MessageCustomersSection";
|
import MessageCustomersSection from "@/components/admin/MessageCustomersSection";
|
||||||
@@ -16,14 +16,14 @@ type StopDetailPageProps = {
|
|||||||
export default async function StopDetailPage({ params }: StopDetailPageProps) {
|
export default async function StopDetailPage({ params }: StopDetailPageProps) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
const [{ data: stop, error }, { data: brands }] = await Promise.all([
|
const [{ data: stop, error }, { data: brands }] = (await Promise.all([
|
||||||
supabase
|
api
|
||||||
.from("stops")
|
.from("stops")
|
||||||
.select("*, brands(name, slug)")
|
.select("*, brands(name, slug)")
|
||||||
.eq("id", id)
|
.eq("id", id)
|
||||||
.single(),
|
.single(),
|
||||||
supabase.from("brands").select("id, name, slug"),
|
api.from("brands").select("id, name, slug"),
|
||||||
]);
|
])) as any;
|
||||||
|
|
||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
|
|
||||||
@@ -56,17 +56,17 @@ export default async function StopDetailPage({ params }: StopDetailPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ data: allProducts }, { data: productStops }] = await Promise.all([
|
const [{ data: allProducts }, { data: productStops }] = (await Promise.all([
|
||||||
supabase
|
api
|
||||||
.from("products")
|
.from("products")
|
||||||
.select("id, name, type, price")
|
.select("id, name, type, price")
|
||||||
.eq("brand_id", stop.brand_id)
|
.eq("brand_id", stop.brand_id)
|
||||||
.eq("active", true),
|
.eq("active", true),
|
||||||
supabase
|
api
|
||||||
.from("product_stops")
|
.from("product_stops")
|
||||||
.select("id, product_id, products(id, name, type, price)")
|
.select("id, product_id, products(id, name, type, price)")
|
||||||
.eq("stop_id", id),
|
.eq("stop_id", id),
|
||||||
]);
|
])) as any;
|
||||||
|
|
||||||
const assignedProducts = (productStops ?? [])
|
const assignedProducts = (productStops ?? [])
|
||||||
.map((ps: any) => ps)
|
.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 NewStopForm from "@/components/admin/NewStopForm";
|
||||||
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
@@ -32,11 +32,11 @@ export default async function NewStopPage({
|
|||||||
|
|
||||||
let duplicateFrom: Stop | null = null;
|
let duplicateFrom: Stop | null = null;
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
const { data } = await supabase
|
const { data } = (await api
|
||||||
.from("stops")
|
.from("stops")
|
||||||
.select("city, state, location, date, time, brand_id, active, address, zip, cutoff_time")
|
.select("city, state, location, date, time, brand_id, active, address, zip, cutoff_time")
|
||||||
.eq("id", duplicate)
|
.eq("id", duplicate)
|
||||||
.single();
|
.single()) as { data: Stop | null };
|
||||||
duplicateFrom = data;
|
duplicateFrom = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export default async function NewStopPage({
|
|||||||
adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||||
|
|
||||||
const [{ data: allProducts }] = await Promise.all([
|
const [{ data: allProducts }] = await Promise.all([
|
||||||
supabase
|
api
|
||||||
.from("products")
|
.from("products")
|
||||||
.select("id, name, type, price")
|
.select("id, name, type, price")
|
||||||
.eq("brand_id", brandId)
|
.eq("brand_id", brandId)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import StopTableClient from "@/components/admin/StopTableClient";
|
import StopTableClient from "@/components/admin/StopTableClient";
|
||||||
import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
|
import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
import { PageHeader } from "@/components/admin/design-system";
|
import { PageHeader } from "@/components/admin/design-system";
|
||||||
@@ -22,7 +22,7 @@ export default async function AdminStopsPage() {
|
|||||||
redirect("/admin/pickup");
|
redirect("/admin/pickup");
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = supabase
|
let query = api
|
||||||
.from("stops")
|
.from("stops")
|
||||||
.select(`
|
.select(`
|
||||||
id,
|
id,
|
||||||
@@ -49,7 +49,7 @@ export default async function AdminStopsPage() {
|
|||||||
query = query.eq("brand_id", adminUser.brand_id);
|
query = query.eq("brand_id", adminUser.brand_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: stops, error } = await query;
|
const { data: stops, error } = (await query) as any;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { api } from "@/lib/api";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
import TaxDashboard from "@/components/admin/TaxDashboard";
|
import TaxDashboard from "@/components/admin/TaxDashboard";
|
||||||
import { PageHeader } from "@/components/admin/design-system";
|
import { PageHeader } from "@/components/admin/design-system";
|
||||||
@@ -20,7 +20,7 @@ export default async function TaxesPage({ params }: Props) {
|
|||||||
|
|
||||||
let resolvedBrandId = effectiveBrandId;
|
let resolvedBrandId = effectiveBrandId;
|
||||||
if (isPlatformAdmin && !resolvedBrandId) {
|
if (isPlatformAdmin && !resolvedBrandId) {
|
||||||
const { data: firstBrand } = await supabase
|
const { data: firstBrand } = await api
|
||||||
.from("brands")
|
.from("brands")
|
||||||
.select("id")
|
.select("id")
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -54,7 +54,7 @@ export default async function TaxesPage({ params }: Props) {
|
|||||||
|
|
||||||
if (!resolvedBrandId) return <AdminAccessDenied />;
|
if (!resolvedBrandId) return <AdminAccessDenied />;
|
||||||
|
|
||||||
const { data: brands } = await supabase
|
const { data: brands } = await api
|
||||||
.from("brands")
|
.from("brands")
|
||||||
.select("id, name")
|
.select("id, name")
|
||||||
.order("name");
|
.order("name");
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ Use "recent_orders" for recent order questions.`;
|
|||||||
const queryType = parsed.queryType ?? "recent_orders";
|
const queryType = parsed.queryType ?? "recent_orders";
|
||||||
const days = parsed.days ?? 30;
|
const days = parsed.days ?? 30;
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY;
|
||||||
|
|
||||||
// Route to appropriate RPC based on query type
|
// Route to appropriate RPC based on query type
|
||||||
let rpcName = "get_recent_orders_insights";
|
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";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
const supabaseKey = process.env.POSTGREST_SERVICE_KEY;
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseKey) {
|
if (!supabaseUrl || !supabaseKey) {
|
||||||
return NextResponse.json({ error: "Missing configuration" }, { status: 500 });
|
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