A private, structured Industry Problem Intelligence Database. Professionals submit operational pain points through a structured form (not open forum posts) categorized by department, cost impact, frequency, and urgency, so the data can be aggregated into searchable dashboards and paid intelligence reports.
Built with Next.js (App Router) + TypeScript + MongoDB (Mongoose).
- ✅ Phase 1 — Foundation: project scaffold, validated env config, MongoDB connection layer, core data models.
- ✅ Phase 2 — Auth & security: registration + email OTP verification, login, JWT access tokens + rotating opaque refresh tokens, logout, Zod validation on every endpoint, bcrypt hashing, rate limiting, RBAC.
- ✅ Phase 3 — Structured problem submission: searchable/filterable dashboard backend, full-text search, Cloudinary uploads, anonymity controls, admin moderation.
- ✅ Phase 4 — Monetization: paid reports with subscription-tier paywall, one-off purchases (mock payment fallback), company registry, admin stats + user management.
- ✅ Phase 5 — Seeding & polish: realistic, idempotent seed script across 6 industries, real homepage.
- ✅ Phase 6 — Frontend UI: full page set on top of the API — register/verify/login, searchable dashboard with filters, submission form with file upload, problem detail, reports list + paywalled detail with purchase flow, "my submissions", and an admin panel (stats, moderation queue, user management).
- ✅ Phase 7 — Company flow + email notifications: inline
"search or create a company" picker in the submission form (backed by
/api/companies), plus automated emails (via the same SendGrid/console fallback) when a submission is approved/rejected/archived and when a report purchase completes. - ✅ Phase 8 — Production-readiness pass: password reset flow
(
/forgot-password→/reset-password, OTP-based, revokes all sessions), change password + profile settings (/settings), an admin audit log (who approved/rejected/changed what, when), toast notifications in the UI, security response headers, centralized API error handling (every route wrapped so an unexpected error returns clean JSON instead of crashing), a CSV export endpoint gated to paying tiers, and custom 404/error pages. - ✅ Phase 9 — Analytics, admin data tables, and session management:
a full analytics dashboard with 7 charts (Recharts) and today/week/
month/year/custom date filtering, backed by a single cached
$facetaggregation query for speed; dedicated/admin/usersand/admin/problemspages with search, multi-field sort, filters, real pagination, bulk moderation actions, and CSV/PDF export that always matches the current on-screen filters; and multi-device session management (view every active login, revoke one or all others) in/settings. - ✅ Phase 10 — Two-factor authentication (TOTP): RFC 6238-compliant
TOTP implemented directly on Node's
crypto(no dependency, verified against the official RFC 4226 test vector), QR-code setup flow, single-use backup codes, and a proper two-step login (a password-only "pending" token can never be used as a real session — this is enforced by an explicit, checkedpurposefield on every JWT, which a test caught missing and this fixes). - ✅ Phase 11 — Remaining basic + advanced features: in-app
notification bell, saved searches with email/in-app alert digests
(the concrete build-out of the "Compliance Alert" / "Vendor
Matchmaking" ideas from the original product brief), API keys for
programmatic/vendor access (
Authorization: Bearer cg_live_...), self-service account deletion (anonymizes rather than hard-deletes, so shared submission history stays intact), company profile pages, Terms/Privacy pages with signup acceptance, and robots.txt/sitemap.xml. - ✅ Phase 12 — Theme system + homepage/SEO overhaul: the entire app
(public pages, dashboard, settings, admin — everything, via the one
shared layout) now supports 4 themes (Dark, Light, Midnight, High
contrast), switchable from a toggle in the navbar with no flash of the
wrong theme on load. Every hardcoded color in the codebase was migrated
to CSS custom properties (
app/globals.css); charts (Recharts) resolve the live theme's actual color values via auseThemeColors()hook since chart libraries can't consume Tailwind classes directly. The homepage was rebuilt (hero, how-it-works, features, insights preview, FAQ withFAQPageJSON-LD, final CTA,Organization/WebSiteJSON-LD) and a real, statically-generated blog (/blog) was added with 4 full articles for SEO. Also fixed: a duplicate-React-key console error on the old homepage, and a nested-<form>-inside-<form>hydration bug in the company-creation flow that was silently breaking new company creation.
Toggle is in the navbar (works identically on every page, including
admin, since there's one shared <Nav>). Persisted to localStorage,
applied via a data-theme attribute on <html> with an inline
before-paint script so there's no flash of the wrong theme. To add a
fifth theme: add a [data-theme="yourname"] { ... } block in
app/globals.css (copy an existing block's variable list) and add the
name to THEMES in lib/client/theme.tsx.
POST /api/cron/check-saved-searches finds newly-approved problems
matching each active saved search and emails a digest. This app has no
built-in scheduler (Next.js routes are request-driven), so you need to
call this endpoint on a schedule yourself once deployed — e.g.:
- Vercel: add a Vercel Cron entry
in
vercel.jsonhitting this route daily. - Anywhere else: a system crontab or GitHub Actions scheduled workflow
running
curl -X POST https://yourdomain.com/api/cron/check-saved-searches -H "x-cron-secret: $CRON_SECRET".
Set CRON_SECRET in your environment and send it as the x-cron-secret
header — the endpoint refuses to run in production without it.
| Page | What it does |
|---|---|
/admin/analytics |
8 summary stats + 7 charts (submissions over time, user growth, status/department/cost-impact/urgency breakdowns, top industries), date range presets + custom range, 30s server-side cache per query so repeat views are instant |
/admin/users |
Search by name/email, filter by role/tier/active, sort any column, paginated, CSV/PDF export of the exact filtered view |
/admin/problems |
Same pattern plus department/cost/urgency/industry filters, date range, checkbox multi-select with bulk approve/reject (one audit log entry + per-submitter emails) |
/settings → Active sessions |
Every device/browser currently logged in, with IP + last-seen, one-click revoke per device or "log out all others" |
| Feature | Where |
|---|---|
| Forgot/reset password | /forgot-password, /reset-password, POST /api/auth/forgot-password, POST /api/auth/reset-password |
| Change password (logged in) | /settings, PATCH /api/auth/change-password — revokes your other sessions, keeps this one |
| Profile update | /settings, PATCH /api/auth/me |
| Audit log | GET /api/admin/audit-log, shown in /admin — records moderation actions, role/tier/active changes, report publish/delete, industry creation |
| CSV export | GET /api/problems/export — same filters as the dashboard, gated to pro/enterprise/admin |
| Toasts | lib/client/toast.tsx, wired into admin actions, submission, report purchase |
| Security headers | next.config.ts — X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy |
| Centralized error handling | withErrorHandling() in lib/api/respond.ts, applied to every route |
| Path | Purpose |
|---|---|
/ |
Homepage |
/blog, /blog/[slug] |
SEO-oriented insights articles (statically generated) |
/register, /verify, /login, /forgot-password, /reset-password |
Auth flow (OTP verification, password reset) |
/dashboard |
Searchable/filterable problem intelligence feed |
/submit |
Structured problem submission form (with file upload) |
/problems/[id] |
Problem detail |
/companies/[id] |
Company profile + its public submissions |
/mine |
Your own submissions, any status |
/saved-searches |
Manage saved search alerts |
/settings |
Profile, password, 2FA, API keys, sessions, account deletion |
/reports, /reports/[id] |
Paid reports list + paywalled detail with purchase |
/admin, /admin/analytics, /admin/users, /admin/problems |
Stats, charts, full data tables, moderation (admin only) |
npm install
cp .env.example .env.local
# edit .env.local — at minimum set MONGODB_URI to a real MongoDB Atlas
# (or local) connection string, and set real JWT secrets:
# openssl rand -base64 48
npm run devThen:
- Visit
http://localhost:3000/api/health— confirms MongoDB is connected. - Run
npm run seed— populates industries, demo users, ~18 realistic problem submissions, and 3 published reports. - Log in as the seeded admin: email from
SEED_ADMIN_EMAIL(defaultadmin@causegrid.dev), password fromSEED_ADMIN_PASSWORD(defaultChangeMe123!— change this in.env.local). - Or log in as a demo user:
demo.user1@causegrid.dev/Demo1234!.
SendGrid, Cloudinary, and Stripe keys are all optional. If they're missing, nothing crashes — each falls back to safe dev behavior instead:
| Service | Without credentials |
|---|---|
| SendGrid (email/OTP) | Full email content, including OTP codes, is printed to the server console |
| Cloudinary (uploads) | Returns a labeled placeholder image URL instead of erroring |
| Stripe (payments) | Report purchases are auto-approved as a clearly-logged mock charge |
This means you can develop and test the entire flow — register, verify by reading the OTP off the console, submit a problem with an "attachment", buy a report — before paying for any third-party service.
All request bodies are JSON (except /api/uploads, which is
multipart/form-data) and validated with Zod. Auth uses httpOnly cookies
set by login/register/refresh — no need to manually attach a bearer token
if you're calling from a browser-based frontend on the same origin.
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /api/auth/register |
— | Creates unverified account, sends OTP |
| POST | /api/auth/verify-otp |
— | Verifies email, logs in (sets cookies) |
| POST | /api/auth/resend-otp |
— | Resends OTP (60s cooldown) |
| POST | /api/auth/login |
— | Logs in (sets cookies) |
| POST | /api/auth/refresh |
refresh cookie | Rotates tokens |
| POST | /api/auth/logout |
— | Revokes refresh token, clears cookies |
| GET | /api/auth/me |
required | Current user |
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/problems |
optional | Search/filter approved problems (q, department, costImpact, frequency, urgency, industry, sort, page, limit) |
| POST | /api/problems |
required | Submit a structured problem (→ pending_review) |
| GET | /api/problems/[id] |
optional | Detail (404 if not visible to you) |
| PATCH | /api/problems/[id] |
admin | Moderate: approve/reject/archive |
| DELETE | /api/problems/[id] |
owner (pending only) / admin | Delete |
| GET | /api/problems/mine |
required | Your own submissions, any status |
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /api/uploads |
required | Multipart file field, ≤5MB, PNG/JPEG/WEBP/PDF |
| GET | /api/industries |
— | Taxonomy list |
| POST | /api/industries |
admin | Add an industry |
| GET | /api/companies |
— | List/search companies (?q=) |
| POST | /api/companies |
required | Register a company |
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/reports |
optional | List published reports (previews for non-payers) |
| POST | /api/reports |
admin | Bundle approved problems into a report |
| GET | /api/reports/[id] |
optional | Full content if you have access, preview otherwise |
| PATCH | /api/reports/[id] |
admin | Edit / publish |
| DELETE | /api/reports/[id] |
admin | Delete |
| POST | /api/reports/[id]/purchase |
required | Buy access (auto-unlocked for pro/enterprise subscribers) |
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/admin/stats |
admin | User/problem/report/revenue dashboard numbers |
| GET | /api/admin/users |
admin | Paginated user list |
| PATCH | /api/admin/users/[id] |
admin | Change role/subscription/active status |
app/
page.tsx # homepage
api/ # all routes described above
lib/
config/env.ts # Zod-validated environment config
db/connectDB.ts # cached MongoDB connection
models/ # Mongoose schemas (User, Company, Industry,
# Problem, Report, OtpToken, RefreshToken)
auth/ # password hashing, JWT, cookies, rate
# limiting, session management, RBAC guard
services/ # email, OTP, Cloudinary, payment, and the
# anonymity/paywall serializers
validations/ # Zod schemas per resource
api/respond.ts # shared response + body-parsing helpers
types/index.ts # shared enums (roles, departments, etc.)
scripts/seed.ts # idempotent database seed script
.env.example
A page/route audit and a targeted code-level security review were done.
Page/route audit: every href/router.push in the frontend and
every apiGet/apiPost/etc. call was cross-checked against actual
page.tsx and route.ts files — no missing pages or dead links found.
One real functional bug was found and fixed: the client never called
/api/auth/refresh, so every logged-in session would silently break
15 minutes after login with no recovery. Fixed with automatic, de-duped,
silent refresh-and-retry on a 401 (lib/client/api.ts).
Vulnerabilities found and fixed:
- ReDoS / regex injection —
/api/companies(unauthenticated) and/api/admin/userssearch built a MongoDB$regexfilter directly from raw user input, allowing catastrophic-backtracking patterns to stall queries, and letting a "search box" act as a live regex engine. Fixed withescapeRegex()inlib/validations/shared.ts, applied at both call sites, verified with a test that the fix actually neutralizes a real ReDoS pattern. - No rate limiting on the core public read endpoints —
GET /api/problems(the heaviest query in the app: aggregation +$lookupjoins) plus problem/report detail, reports list, industries, and company profile had zero rate limiting. Beyond generic DoS/cost risk, this meant the entire structured dataset — the thing the business model is built on selling access to — could be scraped in bulk for free. All now rate-limited per-IP. - File upload MIME-type spoofing —
/api/uploadsonly checked the client-suppliedContent-Type, which is trivially spoofable (rename any file, set the form field's declared type toimage/png). Addedlib/services/fileSignature.ts, which checks the actual binary signature (PNG/JPEG/WEBP/PDF magic bytes) and rejects anything whose real content doesn't match an allowed type, regardless of what the client claimed. - JWT purpose confusion (found during Phase 10, still noted here for
completeness) — the 2FA "pending" token and a real access token were
signed with the same secret with no discriminator, so a pending token
(issued after only a password check) could have been replayed as a
full session, bypassing the second factor entirely. Fixed with an
explicit, checked
purposefield on every JWT.
Reviewed and confirmed solid (no changes needed): every mutating
route requires auth except the intentionally pre-auth ones
(login/register/OTP/refresh/logout/cron); every per-resource [id] route
(saved searches, API keys, sessions, notifications, problems, reports)
scopes its query to the requesting user or checks ownership before
mutating; no dangerouslySetInnerHTML anywhere (React's default escaping
handles stored-content XSS); no secrets logged; cookies are
httpOnly + sameSite=lax + secure in production; every request body
goes through Zod validation (no route reads raw request.json()
unvalidated); .env* is gitignored.
Known, accepted gaps (documented rather than silently left):
npm audit flags high-severity CVEs in postcss/sharp, both bundled
inside next itself — not directly installable fixes, and not
reachable through this app's actual usage (next/image is never used,
so sharp is dormant; postcss only processes our own build-time CSS,
never user input). Will resolve automatically on the next Next.js
release that bumps its bundled versions. CSRF relies on sameSite=lax
cookies rather than a double-submit token — a reasonable default for this
kind of app, but worth upgrading if you're handling real payments beyond
the current mock fallback. Rate limiting is in-memory/per-process — noted
elsewhere in this README as needing Redis if you scale horizontally.
- Database: use MongoDB Atlas (free tier is fine to start). Whitelist
your deployment platform's IPs (or
0.0.0.0/0for simplicity, with a strong DB user password). - Secrets: generate real
JWT_ACCESS_SECRET/JWT_REFRESH_SECRETwithopenssl rand -base64 48— do not use the defaults in production (the app will still boot with them, but that's insecure). - Cookies:
secureis automatically enabled whenNODE_ENV=production, which requires HTTPS — make sure your deployment serves over HTTPS. - Rate limiting: currently in-memory per Node process. Fine for a
single instance; if you deploy multiple instances/serverless functions
behind a load balancer, swap
lib/auth/rateLimit.tsfor a Redis-backed limiter (e.g. Upstash) — call sites don't need to change. - Seeding production:
npm run seedrefuses to run whenNODE_ENV=productionunlessSEED_ALLOW_PROD=trueis explicitly set, to prevent accidentally seeding demo data into a real database. - Recommended host: Vercel (zero-config for Next.js) with MongoDB Atlas.