← back to Codex Review 2026 04 30
Norma/review.md
36 lines
## Snapshot
- Multi-tenant nonprofit intelligence/CRM platform with outreach, Gmail, news, petitions, credentials, and social agents.
- Stack: TypeScript, Next.js App Router, React, Tailwind, Node scripts/agents, PostgreSQL, Gemini/Gmail APIs.
- How it's run: PM2 via `ecosystem.config.js`; cron-like PM2 restarts for scrapers/credential health; dev via `npm run dev -p 7400`.
- Status: live; prod IP, PM2 config, 2026 ops docs.
## Top Risks (P0 — fix this week)
- `.env.local:1`, `.env.local:3-5`, `.env.local:14`, `.env.local:21-22`, `CLAUDE.md:12-13`, `README.md:21`: live-looking DB/admin/cron/session/Gemini/Gmail secrets are committed/documented. Rotate all exposed credentials, remove secrets from repo/docs, purge history if shared.
- `middleware.ts:55-89` + `app/api/social/ai-image/route.ts:20`, `app/api/social/bulk-schedule/route.ts:66`, `app/api/social/bulk-schedule/route.ts:88`, `app/api/social/posts/approve/route.ts:10`, `app/api/social/posts/pending/route.ts:10`: middleware only checks token structure/expiry, while these routes do no `requireRole`; forged cookies can generate images, schedule/approve/read social posts. Add `requireRole` to every non-public route or crypto-verify in middleware.
- `app/api/pulse/petitions/route.ts:111-182`: unauthenticated public POST creates immediately active petitions with no rate limit, moderation, captcha, or abuse controls. Default to draft/moderation and add IP/user rate limiting.
- `app/api/pulse/petitions/[id]/sign/route.ts:13-44`, `app/api/pulse/petitions/[id]/sign/route.ts:51-52`: public signing has no rate limit and does not check `allow_signatures`; any active petition can be spam-signed. Check `allow_signatures`, add rate limiting/captcha/email verification.
- `app/api/nonprofit/enrich/route.ts:179-184`, `app/api/nonprofit/enrich/route.ts:227-235`, `app/api/email-analyzer/fetch-url/route.ts:202-215`, `lib/social-crawl.ts:111-117`: server-side URL fetch accepts arbitrary HTTP(S), follows redirects, and does not block private/link-local IPs. Add SSRF guard: DNS/IP validation, redirect revalidation, size limits, and deny internal ranges.
- `lib/auth.ts:5-8`, `lib/auth.ts:179-189`, `lib/cron-auth.ts:8`, `app/api/webhooks/pulse-topic/route.ts:6`: secrets fall back to predictable values and auth cookie is only `Secure` when `USE_HTTPS=true`. Fail closed in production and always set `Secure` behind HTTPS.
## Notable Issues (P1 — fix this month)
- `next.config.ts:5-10`: production builds ignore TypeScript and ESLint errors. Remove both ignores once current errors are fixed; this can ship broken routes.
- `app/api/auth/google/route.ts:15-24`, `app/api/auth/google/callback/route.ts:10-21`: Google OAuth has no `state`/nonce validation. Add signed state cookie to prevent login CSRF/session confusion.
- `app/api/nonprofit/signup/route.ts:170-188`: duplicate email check then insert is race-prone; no unique index for `LOWER(contact_email)` observed. Add DB unique index and handle `23505`.
- `db/schema.sql:252-257`: base schema references `nonprofit_accounts`, but no table definition was observed in `db/*.sql`; README bootstrap may fail from scratch. Add the migration/table or update bootstrap order.
- `app/api/social/bulk-schedule/route.ts:108-123`: no upper bound on `posts` or platform count; one request can create huge transaction fanout. Cap rows/platforms and validate post body lengths.
- `app/api/email-analyzer/fetch-url/route.ts:241-262`: reads entire response before truncating content. Stream/cap response bytes before `res.text()`.
## Polish (P2 — when convenient)
- `README.md:27` says Next.js 16, but `package.json:30` pins Next `^15.3.3`; align docs/deps.
- `package.json:5-10`: no test script observed; add focused auth/route tests for the P0 paths.
- `scripts/*` and `agents/*`: many hardcoded DB/API fallbacks; consolidate env loading and delete legacy one-off scripts.
## Strengths
- Most reviewed SQL uses parameter placeholders instead of string-concatenating user values.
- `lib/auth.ts` supports bcrypt migration and HMAC-signed sessions.
- API-key auth/rate-limit helper exists for `/api/v1/*` and is cleanly factored.
## Open questions for the owner
- Is this repo private-only, or has it ever been pushed/shared externally?
- Is `45.61.58.125:7400` still the production host?
- Should public Pulse allow user-created petitions, or should every petition require staff approval?