# Merge Request: Production Deployment Fixes and Enhancements ## Summary This MR includes critical fixes for production deployment, authentication improvements, file upload serving, and monitoring capabilities. All changes have been tested and are ready for production. ## 🐛 Critical Fixes ### 1. Authentication & Session Management - **Fixed TypeScript error in session callback** (`lib/auth.ts`) - Removed `return null` that caused build failures - Session callback now always returns a valid session object - **Fixed login redirect loop** (`app/login/page.tsx`) - Changed from `router.push()` to `window.location.href` for full page reload - Ensures session cookie is available before middleware checks - **Created proper middleware** (`proxy.ts`) - Next.js 16 requires `proxy.ts` instead of `middleware.ts` - Fixed authentication checks in Edge runtime - Explicitly specifies cookie name for `getToken` ### 2. Build & Deployment - **Fixed Prisma initialization** (`lib/prisma.ts`) - Made Prisma client initialization lazy to fix build without DATABASE_URL - Uses Proxy pattern for on-demand initialization - Prevents build failures when DATABASE_URL not set ### 3. File Upload & Serving - **Fixed photo upload serving** (`app/api/uploads/[filename]/route.ts`) - Created dedicated API route to serve uploaded files - Files now served via `/api/uploads/[filename]` instead of static `/uploads/` - Ensures files are accessible regardless of filesystem location - Added file existence verification and proper error handling - **Updated upload routes** to use new API endpoint - `app/api/photos/upload/route.ts` - Updated to use `/api/uploads/` URLs - `app/api/photos/upload-multiple/route.ts` - Updated to use `/api/uploads/` URLs - **Fixed photo display components** - `components/PhotoThumbnail.tsx` - Uses regular `img` tag for uploads - `components/PhotoImage.tsx` - Uses regular `img` tag for uploads - Avoids Next.js Image component issues with dynamically uploaded files ### 4. Middleware & Route Protection - **Updated proxy middleware** (`proxy.ts`) - Added `/uploads` and `/api/uploads` to public routes - Added comprehensive activity logging - Improved error handling and logging ## ✨ New Features ### Activity Logging - **Created activity logging utility** (`lib/activity-log.ts`) - Structured logging for user actions - Tracks: page visits, photo uploads, guess submissions - Includes user info, IP, timestamps, and action details - **Added activity logging to key routes** - `proxy.ts` - Logs all page visits and API calls - `app/api/photos/upload/route.ts` - Logs photo uploads - `app/api/photos/[photoId]/guess/route.ts` - Logs guess submissions ### Monitoring - **Activity monitoring commands** - Watch logs: `sudo journalctl -u app-backend -f | grep -E "\[ACTIVITY\]|\[PHOTO_UPLOAD\]|\[GUESS_SUBMIT\]"` - Filter by user, action type, or time range ## 📝 Documentation Updates - **README.md** - Added deployment notes section - Added file upload details and troubleshooting - Added activity monitoring commands - Added database query examples - Updated troubleshooting section - **ARCHITECTURE.md** - Updated middleware references (proxy.ts instead of middleware.ts) - Added activity logging documentation - Updated photo upload flow with file upload details - Added file serving architecture - Updated guess submission flow - **CLEANUP.md** (new) - Created cleanup checklist for future improvements - Documents debug code and verbose logging - Provides recommendations for optimization ## 🔧 Technical Changes ### Files Modified - `lib/auth.ts` - Fixed session callback return type - `app/login/page.tsx` - Fixed redirect to use full page reload - `proxy.ts` - Created/updated middleware with activity logging - `lib/prisma.ts` - Made initialization lazy - `app/api/photos/upload/route.ts` - Updated file serving, added logging - `app/api/photos/upload-multiple/route.ts` - Updated file serving - `components/PhotoThumbnail.tsx` - Fixed image display - `components/PhotoImage.tsx` - Fixed image display ### Files Created - `app/api/uploads/[filename]/route.ts` - File serving API route - `lib/activity-log.ts` - Activity logging utility - `CLEANUP.md` - Cleanup checklist ## ✅ Testing - [x] Authentication flow tested (login, session persistence) - [x] Photo upload tested (file and URL uploads) - [x] Photo display tested (uploaded files visible to all users) - [x] Guess submission tested - [x] Build tested (no TypeScript errors) - [x] Middleware tested (route protection working) - [x] Activity logging verified ## 🚀 Deployment Notes ### Environment Variables Required - `NODE_ENV=production` - `NEXTAUTH_URL` - Production domain - `NEXTAUTH_SECRET` - Secret key - `AUTH_TRUST_HOST=true` (if using reverse proxy) - `DATABASE_URL` - Production database connection ### Post-Deployment 1. Verify `public/uploads/` directory exists and has write permissions 2. Test photo upload and verify files are accessible 3. Monitor activity logs to ensure logging is working 4. Verify authentication flow works correctly ### Monitoring - Watch activity logs: `sudo journalctl -u app-backend -f | grep -E "\[ACTIVITY\]|\[PHOTO_UPLOAD\]|\[GUESS_SUBMIT\]"` - Check for errors: `sudo journalctl -u app-backend --since "1 hour ago" | grep -i error` ## 🔄 Breaking Changes **None** - All changes are backward compatible. Existing photos with `/uploads/` URLs may need to be updated to `/api/uploads/` if files are not accessible, but the system will continue to work. ## 📋 Migration Notes ### For Existing Photos - Photos uploaded before this change use `/uploads/` URLs - New photos use `/api/uploads/` URLs - Old photos will continue to work if files exist in `public/uploads/` - Consider migrating old photo URLs if needed (optional) ## 🎯 Next Steps (Future) See `CLEANUP.md` for recommended cleanup tasks: - Reduce verbose logging in production - Add log levels (DEBUG, INFO, WARN, ERROR) - Protect debug endpoints - Optimize activity logging --- **Ready for Production:** ✅ Yes **Breaking Changes:** ❌ No **Requires Migration:** ⚠️ Optional (old photo URLs) Reviewed-on: #3
13 KiB
MirrorMatch Architecture
Overview
MirrorMatch is a photo guessing game built with Next.js App Router, PostgreSQL, and NextAuth. Users upload photos with answer names, and other users guess to earn points.
System Architecture
Application Structure
mirrormatch/
├── app/ # Next.js App Router
│ ├── api/ # API route handlers (Next.js route handlers)
│ │ ├── admin/ # Admin-only API endpoints
│ │ ├── auth/ # NextAuth routes
│ │ ├── photos/ # Photo-related APIs
│ │ └── profile/ # User profile APIs
│ ├── admin/ # Admin panel (server component)
│ ├── leaderboard/ # Leaderboard page (server component)
│ ├── login/ # Login page (client component)
│ ├── photos/ # Photo listing and detail pages
│ ├── profile/ # User profile page (server component)
│ └── upload/ # Photo upload page (client component)
├── components/ # Reusable React components
│ ├── Navigation.tsx # Navigation bar (client component)
│ └── [others] # Form components, UI components
├── lib/ # Utility libraries and helpers
│ ├── prisma.ts # Prisma client singleton (lazy initialization)
│ ├── auth.ts # NextAuth configuration
│ ├── email.ts # Email sending utilities
│ ├── 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
├── proxy.ts # Next.js proxy/middleware for route protection (Next.js 16)
└── lib/
└── activity-log.ts # Activity logging utility
Data Model
Database Schema
User Model
model User {
id String @id @default(cuid())
name String
email String @unique
passwordHash String
role Role @default(USER)
points Int @default(0)
createdAt DateTime @default(now())
uploadedPhotos Photo[] @relation("PhotoUploader")
guesses Guess[]
}
Fields:
id: Unique identifier (CUID)name: User's display nameemail: Unique email address (used for login)passwordHash: Bcrypt-hashed password (never exposed)role: Either "ADMIN" or "USER"points: Accumulated points from correct guessescreatedAt: Account creation timestamp
Relations:
uploadedPhotos: All photos uploaded by this userguesses: All guesses made by this user
Photo Model
model Photo {
id String @id @default(cuid())
uploaderId String
uploader User @relation("PhotoUploader", fields: [uploaderId], references: [id])
url String
answerName String
createdAt DateTime @default(now())
guesses Guess[]
}
Fields:
id: Unique identifier (CUID)uploaderId: Foreign key to User who uploadedurl: URL to the photo imageanswerName: The correct answer users should guesscreatedAt: Upload timestamp
Relations:
uploader: The User who uploaded this photoguesses: All guesses made for this photo
Guess Model
model Guess {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
photoId String
photo Photo @relation(fields: [photoId], references: [id])
guessText String
correct Boolean @default(false)
createdAt DateTime @default(now())
@@index([userId])
@@index([photoId])
}
Fields:
id: Unique identifier (CUID)userId: Foreign key to User who made the guessphotoId: Foreign key to Photo being guessedguessText: The user's guess textcorrect: Whether the guess matches the answer (case-insensitive)createdAt: Guess timestamp
Relations:
user: The User who made this guessphoto: The Photo being guessed
Indexes:
- Indexed on
userIdfor fast user guess queries - Indexed on
photoIdfor fast photo guess queries
Authentication Flow
NextAuth Configuration
Location: lib/auth.ts
Provider: Credentials Provider (email + password)
Flow:
- User submits email and password on
/login - NextAuth calls
authorizefunction - System looks up user by email in database
- Compares provided password with stored
passwordHashusing bcrypt - If valid, creates JWT session with user data (id, email, name, role)
- Session stored in JWT (no database session table)
Session Data:
id: User IDemail: User emailname: User namerole: User role (ADMIN | USER)
Route Protection
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 requirerole === "ADMIN"
Implementation:
- Uses NextAuth
getTokenfromnext-auth/jwtin Edge runtime - Checks JWT token on each request via
proxy.ts - Explicitly specifies cookie name:
__Secure-authjs.session-token - Redirects unauthenticated users to
/loginwithcallbackUrl - 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
1. Admin Creates User
Flow:
- Admin navigates to
/admin - Fills out user creation form (name, email, password, role)
- Form submits to
POST /api/admin/users - API route:
- Verifies admin session
- Checks if email already exists
- Hashes password with bcrypt
- Creates User record in database
- Admin sees new user in user list
API Route: app/api/admin/users/route.ts
Component: components/CreateUserForm.tsx
2. User Login
Flow:
- User navigates to
/login - Enters email and password
- Client calls NextAuth
signIn("credentials", ...) - NextAuth validates credentials via
lib/auth.ts - On success, redirects to
/photos - Session stored in JWT cookie
Page: app/login/page.tsx (client component)
3. User Changes Password
Flow:
- User navigates to
/profile - Enters current password and new password
- Form submits to
POST /api/profile/change-password - API route:
- Verifies session
- Validates current password against stored hash
- Hashes new password
- Updates User record
- User sees success message
API Route: app/api/profile/change-password/route.ts
Component: components/ChangePasswordForm.tsx
4. Photo Upload
Flow:
- User navigates to
/upload - Uploads photo file or enters photo URL and answer name
- Form submits to
POST /api/photos/upload(file upload) orPOST /api/photos(URL) - API route:
- Verifies session
- 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]
- User redirected to photo detail page
API Routes:
app/api/photos/upload/route.ts- File upload endpointapp/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)
5. Email Notifications
Implementation: lib/email.ts
Development Mode:
- Uses Ethereal Email (test SMTP service)
- Provides preview URLs in console
- Falls back to console transport if Ethereal unavailable
Production Mode:
- Uses SMTP server (configured via env vars)
- Sends HTML and plaintext emails
- Includes link to photo guess page
Email Content:
- Subject: "New Photo Ready to Guess!"
- Body: Includes uploader name, link to
/photos/[id] - Sent asynchronously (doesn't block photo creation)
6. Guess Submission
Flow:
- User views photo at
/photos/[id] - User enters guess text
- Form submits to
POST /api/photos/[photoId]/guess - 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
correctboolean - If correct: increments user's points by photo's
pointsvalue - If wrong and penalty enabled: deducts penalty points
- Logs activity:
[GUESS_SUBMIT]with result
- Page refreshes to show feedback
API Route: app/api/photos/[photoId]/guess/route.ts
Component: components/GuessForm.tsx
Page: app/photos/[id]/page.tsx (server component)
Guess Matching:
- Case-insensitive comparison
- Trims whitespace
- Exact match required (no fuzzy matching)
- Points awarded based on photo's
pointsfield (default: 1)
7. Leaderboard
Flow:
- User navigates to
/leaderboard - Server component queries all users
- Orders by
points DESC - Renders table with rank, name, email, points
- Highlights current user's row
Page: app/leaderboard/page.tsx (server component)
Query: prisma.user.findMany({ orderBy: { points: "desc" } })
Code Organization Guidelines
Where to Put Code
Server Actions:
- Use Next.js route handlers (
app/api/*/route.ts) for API endpoints - Consider server actions (
app/actions.ts) for form submissions if preferred pattern
Route Handlers:
- All API endpoints in
app/api/*/route.ts - Use
GET,POST,PUT,DELETEexports as needed - Always verify session and authorization
- Return JSON responses
Shared Utilities:
- Database access:
lib/prisma.ts(Prisma client singleton) - Auth config:
lib/auth.ts - Email:
lib/email.ts - General helpers:
lib/utils.ts(hashing, string normalization, etc.)
Components:
- Reusable UI components:
components/ - Page-specific components can live in
app/[page]/if not reused - Prefer server components, use
"use client"only when needed
Type Definitions:
- NextAuth types:
types/next-auth.d.ts - Shared types:
types/directory - Component prop types: inline or in component file
Database Access Pattern
Always use Prisma:
import { prisma } from "@/lib/prisma"
// Example query
const user = await prisma.user.findUnique({
where: { email: "user@example.com" }
})
Never:
- Use raw SQL queries
- Use other ORMs
- Access database directly without Prisma
Security Considerations
Password Security
- All passwords hashed with bcrypt (10 rounds)
- Password hashes never exposed in API responses
- Password changes require current password verification
Authorization
- All mutations check user session
- Admin routes verify
role === "ADMIN" - Users can only modify their own data (except admins)
- Photo uploads require authentication
- Guess submissions require authentication
Input Validation
- Validate all user inputs
- Sanitize before database operations
- Use Prisma's type safety to prevent SQL injection
- Normalize guess text before comparison
Important Notes
- Always read this document and README.md before making architectural changes
- Update this document when adding new features or changing data flows
- Keep documentation in sync with code changes
- Follow established patterns for consistency
Last Updated: When architecture changes, update this file and notify the team.