From a6742a3d9f59a9c9c1461194dadf0f7ff296dbf3 Mon Sep 17 00:00:00 2001 From: "Drew Schillinger (DoctorEw)" Date: Wed, 3 Dec 2025 21:01:58 -0500 Subject: [PATCH 1/2] de-vibing and cloudflare-wrangling --- .env.example | 32 + .github/workflows/deploy.yml | 62 + .gitignore | 31 +- DOCS/spec.md | 353 +++ app/admin/submissions/page.tsx | 236 ++ app/api/admin/submissions/[id]/route.ts | 93 + app/api/admin/submissions/route.ts | 40 + app/api/resources/route.ts | 118 + app/api/submissions/route.ts | 132 + app/conferences/page.tsx | 47 +- app/meetups/page.tsx | 175 +- app/resources/page.tsx | 173 +- app/tech-hubs/page.tsx | 34 +- bun.lock | 2170 ++++++++++++++ components/conference-section.tsx | 44 +- components/meetup-section.tsx | 34 +- components/online-resources-section.tsx | 34 +- components/submit-resource-section.tsx | 63 +- components/tech-hubs-section.tsx | 34 +- db/schema.sql | 48 + hooks/use-resources.ts | 149 + lib/db.ts | 122 + lib/rate-limit.ts | 106 + lib/validations.ts | 128 + middleware.ts | 65 + package.json | 54 +- pnpm-lock.yaml | 3589 ----------------------- scripts/local-setup.sh | 93 + scripts/migrate-to-d1.ts | 106 + wrangler.toml | 25 + 30 files changed, 4537 insertions(+), 3853 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/deploy.yml create mode 100644 DOCS/spec.md create mode 100644 app/admin/submissions/page.tsx create mode 100644 app/api/admin/submissions/[id]/route.ts create mode 100644 app/api/admin/submissions/route.ts create mode 100644 app/api/resources/route.ts create mode 100644 app/api/submissions/route.ts create mode 100644 bun.lock create mode 100644 db/schema.sql create mode 100644 hooks/use-resources.ts create mode 100644 lib/db.ts create mode 100644 lib/rate-limit.ts create mode 100644 lib/validations.ts create mode 100644 middleware.ts delete mode 100644 pnpm-lock.yaml create mode 100644 scripts/local-setup.sh create mode 100644 scripts/migrate-to-d1.ts create mode 100644 wrangler.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d82c56d --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Atlanta Tech Network - Environment Variables +# Copy to .env.local for local development + +# ============================================================================= +# REQUIRED +# ============================================================================= + +# Admin password for /admin routes (HTTP Basic Auth) +# Generate a strong password: openssl rand -base64 32 +ADMIN_PASSWORD=your-strong-password-here + +# ============================================================================= +# OPTIONAL +# ============================================================================= + +# Admin email for receiving submission notifications +ADMIN_EMAIL=admin@example.com + +# Email service (if implementing notifications) +# RESEND_API_KEY=re_xxxxxxxxxxxx + +# ============================================================================= +# CLOUDFLARE (auto-injected in production) +# ============================================================================= +# DB binding is configured in wrangler.toml and injected automatically +# Do not set DB connection strings here - D1 handles this + +# ============================================================================= +# LOCAL DEVELOPMENT +# ============================================================================= +# For local dev with wrangler, D1 creates a local SQLite file automatically +# No additional config needed diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..64bd22e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,62 @@ +name: Deploy to Cloudflare Pages + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Build for Cloudflare Pages + run: bunx @cloudflare/next-on-pages + + # Run D1 migrations on push to main only + - name: Run D1 Migrations + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + bunx wrangler d1 execute DB --remote --file=./db/schema.sql --yes + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + # Deploy to Cloudflare Pages + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy .vercel/output/static --project-name=atl-tech-network + + # Comment on PR with preview URL + - name: Comment Preview URL on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `🔥 **Preview Deployed!**\n\nYour changes are live at: https://${{ github.head_ref }}.atl-tech-network.pages.dev\n\n_Deployed by Calcifer_` + }) diff --git a/.gitignore b/.gitignore index 37c2b6f..734847e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # dependencies /node_modules +/.pnp +.pnp.js # next.js /.next/ @@ -16,8 +18,12 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files -.env* +# env files (keep .env.example) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local # vercel .vercel @@ -25,3 +31,24 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Cloudflare / Wrangler +.wrangler/ +.dev.vars + +# Generated files +db/seed.sql + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Bun +bun.lockb + +# Testing +coverage/ +.nyc_output/ diff --git a/DOCS/spec.md b/DOCS/spec.md new file mode 100644 index 0000000..a15b14d --- /dev/null +++ b/DOCS/spec.md @@ -0,0 +1,353 @@ +# Code Review & Implementation Spec +## Atlanta Tech Network - De-Vibing Assessment + +**Date:** 2025-12-02 +**Status:** Critical Issues Identified +**Target Platform:** Cloudflare Pages + D1 + +--- + +## Executive Summary + +This codebase was "vibe coded" using Replit/v0.app and presents a polished UI with **zero functional backend**. User submissions are logged to the browser console and discarded. The existing `TECHNICAL_SPEC.md` describes a complete backend architecture that was never implemented. + +--- + +## 1. Critical Security Issues + +### 1.1 Form Submission is Non-Functional +**File:** `components/submit-resource-section.tsx:96` + +```typescript +console.log("Email would be sent with data:", emailData) +``` + +Users submit their name and email, receive a success toast ("Your resource suggestion has been sent to our team for review"), but the data: +- Is logged to the browser console (visible to anyone with DevTools) +- Is immediately discarded +- Never reaches any backend or database + +**Impact:** User trust violation, data loss, potential GDPR issues if EU users submit data believing it's being processed. + +### 1.2 Exposed Admin Email +**File:** `components/submit-resource-section.tsx:88` + +```typescript +to: "75devs@gmail.com" +``` + +Hardcoded in production code. Harvestable for spam/phishing campaigns. + +### 1.3 No Input Sanitization +- Email addresses not validated against RFC 5322 +- URLs not validated (could accept `javascript:` URIs) +- No XSS protection on user-submitted content +- No CSRF tokens + +### 1.4 Client-Side Only Architecture +All logic is client-side. No server-side validation exists. If a database were added, the API would be vulnerable to direct manipulation. + +--- + +## 2. Architecture Analysis + +### 2.1 Current State + +``` +┌─────────────────────────────────────┐ +│ Next.js Frontend │ +│ ┌─────────────────────────────────┐│ +│ │ Static TypeScript Array Data ││ ← 136+ resources hardcoded +│ │ (lib/sample-data.ts - 1351 ln) ││ +│ └─────────────────────────────────┘│ +│ ┌─────────────────────────────────┐│ +│ │ Form that logs to console() ││ ← Submissions vanish +│ │ then shows fake success toast ││ +│ └─────────────────────────────────┘│ +└─────────────────────────────────────┘ + │ + ▼ + ┌─────────┐ + │ NOTHING │ ← No backend, no database, no API + └─────────┘ +``` + +### 2.2 Spec vs Reality + +| TECHNICAL_SPEC.md Promises | Actual Implementation | +|----------------------------|----------------------| +| Cloudflare D1 database with `resources` + `submissions` tables | **NONE** - Data hardcoded in `lib/sample-data.ts` | +| `POST /api/submissions` endpoint | **NONE** - Form just `console.log()`s | +| `GET /api/resources` endpoint | **NONE** - Pages import static arrays | +| Admin panel at `/admin/submissions` | **NONE** - No admin pages | +| HTTP Basic Auth middleware | **NONE** - No `middleware.ts` | +| Rate limiting (10/hour) | **NONE** | +| Zod validation on API routes | Zod installed but no API routes exist | +| Data migration scripts | **NONE** - `scripts/process-csv-data.js` unused | + +### 2.3 Missing Files/Directories + +| Expected | Status | +|----------|--------| +| `app/api/` | Does not exist | +| `app/admin/` | Does not exist | +| `middleware.ts` | Does not exist | +| `lib/db.ts` | Does not exist | +| `lib/rate-limit.ts` | Does not exist | +| `wrangler.toml` | Does not exist | +| `db/schema.sql` | Does not exist | +| `.env.example` | Does not exist | + +--- + +## 3. Tech Stack Assessment + +### 3.1 Current Dependencies (package.json) + +**Frontend (Solid Foundation):** +- Next.js 15.2.4 +- React 19 +- TypeScript 5 +- Tailwind CSS 4.1.9 +- Radix UI (comprehensive component library) +- React Hook Form 7.60.0 +- Zod 3.25.67 (validation - unused) +- Date-fns 4.1.0 +- Recharts 2.15.4 + +**Missing Backend Dependencies:** +- No database driver (pg, sqlite3, d1, etc.) +- No API client (axios, ky) +- No auth library (next-auth, lucia, etc.) +- No email service (nodemailer, resend, etc.) +- No rate limiting package + +### 3.2 What's Actually Good +- UI component library is well-structured +- Tailwind + Radix is a solid choice +- Form validation schema exists (just needs backend) +- Project structure follows Next.js 15 conventions +- TypeScript is properly configured + +--- + +## 4. Cloudflare Deployment Options + +### 4.1 Recommended: Cloudflare Pages + D1 + +| Component | Cloudflare Service | Free Tier | +|-----------|-------------------|-----------| +| Frontend | Pages (Static + Edge Functions) | 500 builds/month | +| Database | D1 (SQLite at edge) | 5GB storage, 5M reads/day | +| Auth | Access (Zero Trust) | <50 users | +| Rate Limiting | KV (counter pattern) | 100K reads/day | +| Email | External (Resend/Sendgrid) | Varies | + +**Why D1:** +- SQLite semantics - simple, proven +- Data is <1000 rows - D1 handles trivially +- Free tier is generous for this scale +- Edge-deployed - fast from Atlanta + +### 4.2 Alternative: Cloudflare Pages + Turso + +- Turso = hosted libSQL (SQLite fork) +- Better dashboard/tooling than D1 +- Free tier: 500 DBs, 9GB storage, 1B row reads/month +- Slightly more latency (external connection) + +### 4.3 Alternative: Pages + KV Only + +- KV = key-value store at edge +- Store resources as JSON blobs +- No relational queries - simpler but less flexible +- Free: 100K reads/day, 1K writes/day + +### 4.4 Not Recommended + +- **MongoDB/DynamoDB** - overkill, expensive, wrong data model +- **Supabase/PlanetScale** - good tools but not Cloudflare-native +- **Plain Workers + Hono** - requires full migration from Next.js + +--- + +## 5. Database Design + +### 5.1 Recommended Schema (D1/SQLite) + +```sql +-- resources: approved, public content +CREATE TABLE resources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL CHECK(type IN ('meetup','conference','online','tech-hub')), + name TEXT NOT NULL, + description TEXT NOT NULL, + tags TEXT NOT NULL, -- JSON array: '["React","JavaScript"]' + link TEXT NOT NULL, + image TEXT, + conference_date TEXT, -- ISO 8601 + cfp_date TEXT, -- ISO 8601 + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) +); + +-- submissions: pending user submissions +CREATE TABLE submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + submission_type TEXT NOT NULL CHECK(submission_type IN ('new','edit')), + resource_type TEXT NOT NULL CHECK(resource_type IN ('meetup','conference','online','tech-hub')), + submitter_name TEXT NOT NULL, + submitter_email TEXT NOT NULL, + status TEXT DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected')), + + -- New resource fields + name TEXT, + website TEXT, + description TEXT, + tags TEXT, + + -- Edit request fields + existing_resource_name TEXT, + update_reason TEXT, + + -- Metadata + created_at TEXT DEFAULT (datetime('now')), + reviewed_at TEXT, + admin_notes TEXT +); + +-- Indexes +CREATE INDEX idx_resources_type ON resources(type); +CREATE INDEX idx_resources_name ON resources(name); +CREATE INDEX idx_submissions_status ON submissions(status); +CREATE INDEX idx_submissions_created ON submissions(created_at DESC); +``` + +### 5.2 Why SQL over NoSQL + +- Data is inherently relational (resources have types, submissions reference types) +- Need filtering, sorting, searching +- Future: tags could become separate table (many-to-many) +- NoSQL would make queries more complex for no benefit + +--- + +## 6. Implementation Roadmap + +### Phase 1: Foundation (Critical Path) + +| Task | Files to Create/Modify | +|------|----------------------| +| Add Cloudflare config | `wrangler.toml` | +| Create D1 database | CLI: `wrangler d1 create` | +| Define schema | `db/schema.sql` | +| Create D1 client wrapper | `lib/db.ts` | +| Document env vars | `.env.example` | +| Write migration script | `scripts/migrate-to-d1.ts` | + +### Phase 2: API Layer + +| Task | Files to Create/Modify | +|------|----------------------| +| GET resources endpoint | `app/api/resources/route.ts` | +| POST submissions endpoint | `app/api/submissions/route.ts` | +| Add Zod validation | `lib/validations.ts` | +| Implement rate limiting | `lib/rate-limit.ts` | +| Update frontend pages | `app/meetups/page.tsx`, etc. | +| Create data fetching hook | `hooks/use-resources.ts` | + +### Phase 3: Admin & Auth + +| Task | Files to Create/Modify | +|------|----------------------| +| Add auth middleware | `middleware.ts` | +| Create submissions list | `app/admin/submissions/page.tsx` | +| Create review API | `app/api/admin/submissions/[id]/route.ts` | +| Create resources CRUD | `app/admin/resources/page.tsx` | +| Add approval workflow | Update submission → insert resource | + +### Phase 4: Polish & Hardening + +| Task | Files to Create/Modify | +|------|----------------------| +| Fix form to POST to API | `components/submit-resource-section.tsx` | +| Remove hardcoded email | Use `process.env.ADMIN_EMAIL` | +| Add error boundaries | `app/error.tsx`, `app/global-error.tsx` | +| Add loading states | `app/*/loading.tsx` | +| Optional: email notifications | `lib/email.ts` (Resend integration) | +| Remove sample-data.ts | Delete after confirming D1 works | + +--- + +## 7. Environment Variables + +```bash +# .env.example + +# Cloudflare D1 (auto-injected in Workers/Pages) +# DB binding is configured in wrangler.toml + +# Admin authentication +ADMIN_PASSWORD= + +# Optional: Admin email for notifications +ADMIN_EMAIL=admin@example.com + +# Optional: Email service (if implementing notifications) +RESEND_API_KEY=re_xxxxxxxxxxxx +``` + +--- + +## 8. Security Checklist + +- [ ] `ADMIN_PASSWORD` is strong (20+ chars) and in env vars only +- [ ] HTTPS enforced (Cloudflare default) +- [ ] All SQL queries use parameterized statements +- [ ] Input validation with Zod on all API routes +- [ ] Rate limiting on public submission endpoint +- [ ] Admin routes protected with middleware +- [ ] No secrets in git repository +- [ ] Error messages don't leak sensitive info +- [ ] CORS configured (same-origin for admin) +- [ ] Remove `console.log` of user data + +--- + +## 9. Immediate Actions Required + +### Must Fix Before Any Public Use: + +1. **Disable or fix the submission form** - It currently lies to users +2. **Remove hardcoded email** from source code +3. **Add disclaimer** if form remains non-functional ("Coming soon") + +### Before Cloudflare Deployment: + +1. Create D1 database and run schema +2. Implement at minimum `GET /api/resources` and `POST /api/submissions` +3. Add basic rate limiting +4. Set up environment variables in Cloudflare dashboard + +--- + +## 10. Summary + +| Category | Current State | Target State | +|----------|--------------|--------------| +| UI/UX | Excellent | Keep as-is | +| Data Storage | Hardcoded arrays | D1 database | +| Form Submission | Fake (console.log) | Real API + persistence | +| Backend | None | Next.js API routes | +| Admin Panel | None | Basic CRUD interface | +| Authentication | None | HTTP Basic Auth → Cloudflare Access | +| Deployment | Vercel (static) | Cloudflare Pages + D1 | + +**Estimated Effort:** +- Phase 1-2 (functional backend): 20-30 files +- Phase 3 (admin panel): 10-15 files +- Phase 4 (polish): Varies by requirements + +--- + +*Document generated from code review session, 2025-12-02* diff --git a/app/admin/submissions/page.tsx b/app/admin/submissions/page.tsx new file mode 100644 index 0000000..0dc1147 --- /dev/null +++ b/app/admin/submissions/page.tsx @@ -0,0 +1,236 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Textarea } from '@/components/ui/textarea' +import { CheckCircle, XCircle, Clock, ExternalLink, RefreshCw } from 'lucide-react' + +interface Submission { + id: number + submission_type: 'new' | 'edit' + resource_type: string + submitter_name: string + submitter_email: string + status: 'pending' | 'approved' | 'rejected' + name: string | null + website: string | null + description: string | null + tags: string | null + existing_resource_name: string | null + update_reason: string | null + created_at: string + reviewed_at: string | null + admin_notes: string | null +} + +export default function AdminSubmissionsPage() { + const [submissions, setSubmissions] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [filter, setFilter] = useState<'pending' | 'approved' | 'rejected'>('pending') + const [reviewingId, setReviewingId] = useState(null) + const [adminNotes, setAdminNotes] = useState('') + + const fetchSubmissions = async () => { + setLoading(true) + setError(null) + try { + const response = await fetch(`/api/admin/submissions?status=${filter}`) + if (!response.ok) throw new Error('Failed to fetch') + const data = await response.json() + setSubmissions(data.data || []) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load submissions') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchSubmissions() + }, [filter]) + + const handleReview = async (id: number, status: 'approved' | 'rejected') => { + try { + const response = await fetch(`/api/admin/submissions/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status, adminNotes: adminNotes || undefined }) + }) + + if (!response.ok) throw new Error('Failed to update') + + // Remove from list + setSubmissions(prev => prev.filter(s => s.id !== id)) + setReviewingId(null) + setAdminNotes('') + } catch (err) { + alert(err instanceof Error ? err.message : 'Failed to update submission') + } + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + return ( +
+
+

Submission Review

+ +
+ + {/* Filter tabs */} +
+ {(['pending', 'approved', 'rejected'] as const).map((status) => ( + + ))} +
+ + {loading && ( +
+ Loading submissions... +
+ )} + + {error && ( +
+ {error} +
+ )} + + {!loading && !error && submissions.length === 0 && ( +
+ No {filter} submissions found. +
+ )} + +
+ {submissions.map((submission) => ( + + +
+
+ + {submission.submission_type === 'new' ? ( + New Resource + ) : ( + Edit Request + )} + {submission.resource_type} + +

+ From: {submission.submitter_name} ({submission.submitter_email}) +

+

+ Submitted: {formatDate(submission.created_at)} +

+
+
+
+ + + {submission.submission_type === 'new' ? ( +
+

Name: {submission.name}

+

+ Website:{' '} + + {submission.website} + + +

+

Description: {submission.description}

+ {submission.tags &&

Tags: {submission.tags}

} +
+ ) : ( +
+

Resource: {submission.existing_resource_name}

+

Update Reason: {submission.update_reason}

+ {submission.name &&

New Name: {submission.name}

} + {submission.website &&

New Website: {submission.website}

} + {submission.description &&

New Description: {submission.description}

} +
+ )} + + {filter === 'pending' && ( +
+ {reviewingId === submission.id ? ( +
+