diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5408a5c..389ea92 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -26,16 +26,19 @@ mirrormatch/ │ ├── Navigation.tsx # Navigation bar (client component) │ └── [others] # Form components, UI components ├── lib/ # Utility libraries and helpers -│ ├── prisma.ts # Prisma client singleton +│ ├── prisma.ts # Prisma client singleton (lazy initialization) │ ├── auth.ts # NextAuth configuration │ ├── email.ts # Email sending utilities -│ └── utils.ts # Helper functions (hashing, etc.) +│ ├── utils.ts # Helper functions (hashing, etc.) +│ └── activity-log.ts # Activity logging utility ├── prisma/ # Database schema and migrations │ ├── schema.prisma # Prisma schema definition │ └── seed.ts # Database seeding script ├── types/ # TypeScript type definitions │ └── next-auth.d.ts # NextAuth type extensions -└── middleware.ts # Next.js middleware for route protection +├── proxy.ts # Next.js proxy/middleware for route protection (Next.js 16) +└── lib/ + └── activity-log.ts # Activity logging utility ``` ## Data Model @@ -153,21 +156,31 @@ model Guess { ### Route Protection -**Location:** `middleware.ts` +**Location:** `proxy.ts` (Next.js 16 uses `proxy.ts` instead of `middleware.ts`) **Public Routes:** - `/login` - `/api/auth/*` (NextAuth endpoints) +- `/uploads/*` (uploaded files - legacy, now served via API) +- `/api/uploads/*` (uploaded files served via API route) **Protected Routes:** - All other routes require authentication - `/admin/*` routes additionally require `role === "ADMIN"` **Implementation:** -- Uses NextAuth `withAuth` middleware -- Checks JWT token on each request -- Redirects unauthenticated users to `/login` +- Uses NextAuth `getToken` from `next-auth/jwt` in Edge runtime +- Checks JWT token on each request via `proxy.ts` +- Explicitly specifies cookie name: `__Secure-authjs.session-token` +- Redirects unauthenticated users to `/login` with `callbackUrl` - Redirects non-admin users trying to access admin routes to home +- Logs all user activity (page visits, API calls) + +**Activity Logging:** +- All authenticated requests are logged with user info, IP, path, method +- Unauthenticated access attempts are also logged +- Photo uploads and guess submissions have dedicated activity logs +- Logs format: `[ACTIVITY] timestamp | method path | User: email (role) | IP: ip` ## Application Flows @@ -219,16 +232,37 @@ model Guess { **Flow:** 1. User navigates to `/upload` -2. Enters photo URL and answer name -3. Form submits to `POST /api/photos` +2. Uploads photo file or enters photo URL and answer name +3. Form submits to `POST /api/photos/upload` (file upload) or `POST /api/photos` (URL) 4. API route: - Verifies session - - Creates Photo record with `uploaderId`, `url`, `answerName` + - For file uploads: + - Validates file type and size (max 10MB) + - Calculates SHA256 hash for duplicate detection + - Generates unique filename: `timestamp-randomstring.extension` + - Saves file to `public/uploads/` directory + - Sets photo URL to `/api/uploads/[filename]` + - For URL uploads: + - Validates URL format + - Checks for duplicate URLs + - Uses provided URL directly + - Creates Photo record with `uploaderId`, `url`, `answerName`, `fileHash` - Queries all other users (excluding uploader) - Sends email notifications to all other users (async, non-blocking) + - Logs activity: `[PHOTO_UPLOAD]` 5. User redirected to photo detail page -**API Route:** `app/api/photos/route.ts` +**API Routes:** +- `app/api/photos/upload/route.ts` - File upload endpoint +- `app/api/photos/route.ts` - URL upload endpoint (legacy) +- `app/api/uploads/[filename]/route.ts` - Serves uploaded files + +**File Storage:** +- Files stored in `public/uploads/` directory +- Served via `/api/uploads/[filename]` API route +- Files verified after write to ensure successful save +- Duplicate detection via SHA256 hash + **Email:** `lib/email.ts` - `sendNewPhotoEmail()` **Page:** `app/upload/page.tsx` (client component) @@ -260,10 +294,13 @@ model Guess { 4. API route: - Verifies session - Checks if user already has correct guess (prevent duplicate points) + - Prevents users from guessing their own photos - Normalizes guess text and answer (trim, lowercase) - Compares normalized strings - Creates Guess record with `correct` boolean - - If correct: increments user's points by 1 + - If correct: increments user's points by photo's `points` value + - If wrong and penalty enabled: deducts penalty points + - Logs activity: `[GUESS_SUBMIT]` with result 5. Page refreshes to show feedback **API Route:** `app/api/photos/[photoId]/guess/route.ts` @@ -274,6 +311,7 @@ model Guess { - Case-insensitive comparison - Trims whitespace - Exact match required (no fuzzy matching) +- Points awarded based on photo's `points` field (default: 1) ### 7. Leaderboard diff --git a/README.md b/README.md index 6516ee5..649043d 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,26 @@ npm run build npm start ``` +### Deployment Notes + +**Important Configuration:** +- Ensure `NODE_ENV=production` is set in production +- Set `NEXTAUTH_URL` to your production domain (e.g., `https://yourdomain.com`) +- Set `AUTH_TRUST_HOST=true` if using reverse proxy +- Ensure `DATABASE_URL` points to your production database +- Files are stored in `public/uploads/` directory - ensure this directory persists across deployments + +**File Uploads:** +- Photos are uploaded to `public/uploads/` directory +- Files are served via `/api/uploads/[filename]` API route +- Ensure the uploads directory has proper write permissions +- Files are stored on the filesystem (not in database) + +**Monitoring Activity:** +- User activity is logged to console/systemd logs +- Watch logs in real-time: `sudo journalctl -u app-backend -f | grep -E "\[ACTIVITY\]|\[PHOTO_UPLOAD\]|\[GUESS_SUBMIT\]"` +- Activity logs include: page visits, photo uploads, guess submissions + ## Database Commands - `npm run db:generate` - Generate Prisma Client @@ -161,6 +181,18 @@ npm start - `npm run db:studio` - Open Prisma Studio (database GUI) - `npm run db:seed` - Seed database with initial admin user +### Querying the Database + +**Get all photo answers:** +```bash +psql "postgresql://user:password@host:5432/database" -c "SELECT \"answerName\" FROM \"Photo\" ORDER BY \"createdAt\" DESC;" +``` + +**Get answers with uploader info:** +```bash +psql "postgresql://user:password@host:5432/database" -c "SELECT p.\"answerName\", p.url, u.name as uploader, p.\"createdAt\" FROM \"Photo\" p JOIN \"User\" u ON p.\"uploaderId\" = u.id ORDER BY p.\"createdAt\" DESC;" +``` + ## Creating the First Admin User The seed script automatically creates an admin user. If you need to create one manually: @@ -210,9 +242,12 @@ mirrormatch/ - View user points and roles ### Photo Upload (`/upload`) -- Upload photos via URL +- Upload photos via file upload or URL +- Files are stored in `public/uploads/` directory +- Files are served via `/api/uploads/[filename]` API route - Set answer name for guessing - Automatically sends email notifications to all other users +- Duplicate file detection (SHA256 hash) ### Photo Guessing (`/photos/[id]`) - View photo and uploader info @@ -269,7 +304,20 @@ Set up SMTP credentials in `.env`: ### Authentication Issues - Verify `NEXTAUTH_SECRET` is set - Check `NEXTAUTH_URL` matches your app URL +- Set `AUTH_TRUST_HOST=true` if using reverse proxy - Clear browser cookies if needed +- Check middleware logs: `sudo journalctl -u app-backend | grep "Middleware"` + +### Photo Upload Issues +- Verify `public/uploads/` directory exists and has write permissions +- Check file upload logs: `sudo journalctl -u app-backend | grep "UPLOAD"` +- Ensure files are being saved: check `public/uploads/` directory +- Files are served via `/api/uploads/[filename]` - verify API route is accessible + +### Build Issues +- If build fails with `DATABASE_URL not set`, this is expected - Prisma initialization is lazy +- Ensure all environment variables are set in production +- Check for TypeScript errors: `npm run type-check` ## Documentation