- Add duplicate photo detection (file hash and URL checking) - Add max attempts per photo with UI counter - Simplify penalty system (auto-enable when points > 0) - Prevent scores from going below 0 - Add admin photo deletion functionality - Improve navigation with always-visible logout - Prevent users from guessing their own photos
11 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
│ ├── auth.ts # NextAuth configuration
│ ├── email.ts # Email sending utilities
│ └── utils.ts # Helper functions (hashing, etc.)
├── 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
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: middleware.ts
Public Routes:
/login/api/auth/*(NextAuth endpoints)
Protected Routes:
- All other routes require authentication
/admin/*routes additionally requirerole === "ADMIN"
Implementation:
- Uses NextAuth
withAuthmiddleware - Checks JWT token on each request
- Redirects unauthenticated users to
/login - Redirects non-admin users trying to access admin routes to home
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 - Enters photo URL and answer name
- Form submits to
POST /api/photos - API route:
- Verifies session
- Creates Photo record with
uploaderId,url,answerName - Queries all other users (excluding uploader)
- Sends email notifications to all other users (async, non-blocking)
- User redirected to photo detail page
API Route: app/api/photos/route.ts
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)
- Normalizes guess text and answer (trim, lowercase)
- Compares normalized strings
- Creates Guess record with
correctboolean - If correct: increments user's points by 1
- 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)
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.