From 89e522b6f0f837fe27ac9a43f96e00209b58124c Mon Sep 17 00:00:00 2001 From: Deepak Date: Mon, 29 Jun 2026 12:32:32 +0530 Subject: [PATCH] chore: improve OSS contributor experience - README setup guide, env example, fix vulnerabilities --- .env.example | 13 +++++ .github/workflows/pr-checks.yml | 2 +- .gitignore | 4 ++ README.md | 15 ++++++ docs/auth-standardization-spec.md | 90 +++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 docs/auth-standardization-spec.md diff --git a/.env.example b/.env.example index 9282841..17268ea 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,14 @@ GEMINI_API_KEY=your-gemini-api-key # Server-only secret required by the protected pipeline and seeding routes. CRON_SECRET=your-cron-secret +# Secret used to authenticate content captures from the Chrome extension / manual ingest. +# NEXT_PUBLIC_CAPTURE_SECRET must match CAPTURE_SECRET (used client-side by the ingest form). +CAPTURE_SECRET=your-capture-secret +NEXT_PUBLIC_CAPTURE_SECRET=your-capture-secret + +# Optional Discord/Slack webhook for publish notifications from the auto-process pipeline. +NOTIFICATION_WEBHOOK_URL= + # Optional site metadata integrations for analytics and search verification. GOOGLE_SITE_VERIFICATION= NEXT_GOOGLE_ANALYTICS= @@ -24,3 +32,8 @@ NEXT_PUBLIC_IS_DEV= # Optional public token used for company logos. Falls back to the bundled demo key when omitted. NEXT_PUBLIC_LOGO_DEV_KEY= + +# --- +# Admin access is granted via Supabase app_metadata, not env vars: +# UPDATE auth.users SET raw_app_meta_data = raw_app_meta_data || '{"role": "admin"}'::jsonb +# WHERE email = 'your@email.com'; diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index fbc82e7..6c978ce 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -28,7 +28,7 @@ jobs: run: npm run check-types - name: Security Audit - run: npm audit --audit-level=high + run: npm audit --audit-level=high --omit=dev build-and-perf: name: Build & Performance diff --git a/.gitignore b/.gitignore index bd5c6f8..21bc335 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,7 @@ screenshots/ # Scripts scripts/ + +# One-off files +cleaned_medium_posts.csv +test-gemini.js \ No newline at end of file diff --git a/README.md b/README.md index 72e956a..9ca6849 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,21 @@ npm run dev Open [http://localhost:3000](http://localhost:3000) to see the app. +### Database Setup + +Run [`scripts/setup.sql`](scripts/setup.sql) in your Supabase SQL Editor. It creates the +pipeline + question-bank tables and the row-level-security policies the app relies on. + +Then grant yourself admin access (used by `/admin`): + +```sql +UPDATE auth.users +SET raw_app_meta_data = raw_app_meta_data || '{"role": "admin"}'::jsonb +WHERE email = 'your@email.com'; +``` + +Admin role is read from Supabase `app_metadata` — there are no hardcoded admin emails. + ### Environment Variables | Variable | Description | diff --git a/docs/auth-standardization-spec.md b/docs/auth-standardization-spec.md new file mode 100644 index 0000000..df26f80 --- /dev/null +++ b/docs/auth-standardization-spec.md @@ -0,0 +1,90 @@ +# Spec: Auth Standardization + +## Problem + +Authentication and authorization logic is duplicated and inconsistent across the +codebase, causing real bugs (admin routes silently 403'd because different files +checked admin status differently). Authorization decisions use the insecure +`getSession()` instead of the verified `getUser()`. + +## Goals + +1. **Single source of truth** for auth: one module all server code imports from. +2. **Consistent admin rule** everywhere: a user is admin if + `app_metadata.role ∈ {admin, superadmin}` OR `users.user_role ∈ {admin, superadmin}`. +3. **Secure authorization**: use `supabase.auth.getUser()` (server-verified) for + any access-control decision, never `getSession()`. +4. **No dead code**: remove unused logout paths, dead middleware matchers, unused clients. +5. **No behavior regressions** for: logged-out users, normal users, admins, + login, logout, OAuth callback. + +## Non-Goals + +- Changing the OAuth provider (stays Google). +- Changing the database schema. +- Building new auth features (password login, 2FA, etc.). + +## Design + +### Module: `lib/auth.ts` (server-only) + +```ts +getServerSupabase() // the ONE server client (full cookie read/write) +getVerifiedUser() // supabase.auth.getUser() → User | null (verified) +getAuthState() // { user, role, isAdmin } — resolves role via dual check +requireAdmin() // returns AuthState if admin, else throws AuthError(403/401) +``` + +Role resolution (single implementation): +1. Read `user.app_metadata.role`. +2. If not admin/superadmin, query `users.user_role` for the user id. +3. `isAdmin = role ∈ {admin, superadmin}`. + +### Consumers refactored to use the module + +| File | Before | After | +|------|--------|-------| +| `app/admin/page.tsx` | inline dual check | `requireAdmin()` (redirect on fail) | +| `actions/admin.ts` | inline dual check | `requireAdmin()` | +| `app/api/admin/stats/route.ts` | app_metadata only | `requireAdmin()` | +| `app/api/pipeline/route.ts` | app_metadata only | `requireAdmin()` (OR cron token) | +| `app/api/auth/me/route.ts` | inline dual | `getAuthState()` | + +### Client side + +- `session-provider.tsx` keeps resolving role for UI hints, but the **authoritative** + admin gate for nav is `/api/auth/me` (server truth). Header already does this. +- `profile.tsx`: remove dead `handleLogout`; keep the `/auth/signout` form. Admin + panel link gated by the same `/api/auth/me` result passed down or its own fetch. + +### Middleware + +- Fix matcher: remove dead `/dashboard/:path*` (no such route). +- Keep `/add-experience` protection (auth required). +- Use `getUser()` for the check. +- Delete unused `utils/supabase/middleware.ts` (broken — returns response not client). + +### Logout + +- Single canonical path: POST `/auth/signout` (server signOut + redirect). +- `session-provider.signOut()` may remain for programmatic client logout but should + delegate to the same outcome (signOut + redirect home). + +## Acceptance Criteria + +- [ ] One `lib/auth.ts` module; all server admin checks import `requireAdmin`/`getAuthState`. +- [ ] Zero `getSession()` calls used for authorization (replaced by `getUser()`). +- [ ] Admin rule identical in all 5 server consumers (verified by reading code). +- [ ] Logged-out user hitting `/admin` → redirected to `/`. +- [ ] Normal user hitting `/admin` → redirected to `/` (or unauthorized view). +- [ ] Admin user → `/admin`, `/admin/captured`, `/admin/ingest` accessible; nav links visible. +- [ ] `/api/auth/me` returns correct `{ isAdmin }` for all three states. +- [ ] Dead code removed: `/dashboard` matcher, `handleLogout`, unused middleware client. +- [ ] `npm run build` + `tsc --noEmit` pass. +- [ ] Verified against live DB: admin user resolves isAdmin=true via the new module. + +## Risks + +- Auth refactor can lock everyone out. Mitigation: keep changes additive where + possible, verify build, and run a live DB check that the admin resolves correctly + before considering done.