diff --git a/.gitignore b/.gitignore
index b7a560c..4215494 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,6 @@ next-env.d.ts
# Test coverage
/coverage
/.nyc_output
+
+# Application logs
+*.log
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 389ea92..d48bfae 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -233,7 +233,7 @@ model Guess {
**Flow:**
1. User navigates to `/upload`
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)
+3. Form submits to `POST /api/photos/upload` (supports both file and URL uploads)
4. API route:
- Verifies session
- For file uploads:
@@ -253,8 +253,8 @@ model Guess {
5. User redirected to photo detail page
**API Routes:**
-- `app/api/photos/upload/route.ts` - File upload endpoint
-- `app/api/photos/route.ts` - URL upload endpoint (legacy)
+- `app/api/photos/upload/route.ts` - Single photo upload endpoint (supports both file and URL uploads)
+- `app/api/photos/upload-multiple/route.ts` - Multiple photo upload endpoint
- `app/api/uploads/[filename]/route.ts` - Serves uploaded files
**File Storage:**
diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md
new file mode 100644
index 0000000..6133715
--- /dev/null
+++ b/IMPROVEMENTS.md
@@ -0,0 +1,335 @@
+# MirrorMatch - Improvement Ideas & Future Enhancements
+
+This document contains ideas, suggestions, and potential enhancements to make MirrorMatch better, faster, and more professional.
+
+## ๐ Performance Optimizations
+
+### Database & Query Optimization
+- [ ] **Add database indexes** - Create indexes on frequently queried fields (`Photo.createdAt`, `User.points`) to speed up queries
+ - **Why:** As your database grows, queries without indexes become exponentially slower. Indexes can make queries 10-100x faster, especially for leaderboard and photo listing queries that run frequently.
+
+- [ ] **Implement pagination** - Add pagination to photos list to handle large datasets efficiently
+ - **Why:** Loading all photos at once becomes slow and memory-intensive as you scale. Pagination improves load times, reduces server load, and provides better UX by showing manageable chunks of data.
+
+- [ ] **Database connection pooling** - Configure connection pooling to optimize database connections
+ - **Why:** Without pooling, each request creates a new database connection, which is expensive. Connection pooling reuses connections, reducing latency and allowing your app to handle more concurrent users.
+
+- [ ] **Cache leaderboard data** - Use Redis or in-memory cache with TTL (1-5 minutes) to reduce database load
+ - **Why:** The leaderboard is queried frequently but changes infrequently. Caching it reduces database load by 90%+ and makes the page load instantly, improving user experience significantly.
+
+- [ ] **Lazy loading for photos** - Load images as user scrolls to improve initial page load
+ - **Why:** Loading all images upfront is slow and wastes bandwidth. Lazy loading makes pages load 3-5x faster and reduces server costs, especially important for mobile users.
+
+- [ ] **Query optimization** - Use `select` to fetch only needed fields, reducing data transfer
+ - **Why:** Fetching entire records when you only need a few fields wastes bandwidth and memory. This simple change can reduce data transfer by 50-80% and speed up queries.
+
+### Frontend Performance
+- [ ] **Image optimization** - Use Next.js Image component with proper sizing and automatic format optimization
+ - **Why:** Unoptimized images can be 5-10x larger than needed. Next.js Image automatically serves WebP/AVIF formats and resizes images, reducing bandwidth by 60-80% and improving page load times dramatically.
+
+- [ ] **Image lazy loading** - Implement lazy loading for photo thumbnails to improve page load times
+ - **Why:** Loading all thumbnails at once slows initial page load. Lazy loading defers off-screen images, making pages load 2-3x faster and improving Core Web Vitals scores.
+
+- [ ] **Virtual scrolling** - Use virtual scrolling for large photo lists to maintain performance
+ - **Why:** Rendering hundreds of DOM elements causes lag. Virtual scrolling only renders visible items, keeping the UI smooth even with thousands of photos.
+
+- [ ] **Code splitting** - Implement route-based code splitting to reduce initial bundle size
+ - **Why:** Large JavaScript bundles slow initial page load. Code splitting loads only what's needed per route, reducing initial bundle by 40-60% and improving Time to Interactive.
+
+- [ ] **React.memo() optimization** - Wrap expensive components with React.memo() to prevent unnecessary re-renders
+ - **Why:** Unnecessary re-renders waste CPU and cause UI lag. Memoization prevents re-renders when props haven't changed, making the UI more responsive, especially on lower-end devices.
+
+- [ ] **Bundle size optimization** - Analyze and optimize bundle size using webpack-bundle-analyzer
+ - **Why:** Large bundles slow page loads and hurt SEO. Identifying and removing unused code can reduce bundle size by 20-40%, improving load times and user experience.
+
+- [ ] **Progressive image loading** - Show blur placeholder while images load, then transition to full image
+ - **Why:** Blank spaces while images load feel slow. Progressive loading provides immediate visual feedback, making the app feel faster and more polished, improving perceived performance.
+
+### API & Backend
+- [ ] **Rate limiting** - Add rate limiting to prevent abuse and protect API endpoints
+ - **Why:** Without rate limiting, malicious users can overwhelm your server or abuse features. Rate limiting protects against DDoS, prevents spam, and ensures fair resource usage for all users.
+
+- [ ] **Request caching** - Implement caching for static data and frequently accessed endpoints
+ - **Why:** Many API calls return the same data repeatedly. Caching reduces database queries by 70-90%, lowers server costs, and makes responses 10-100x faster.
+
+- [ ] **Compression middleware** - Add gzip/brotli compression to reduce response sizes
+ - **Why:** Text responses (JSON, HTML) compress by 70-90%. Compression reduces bandwidth costs, speeds up transfers (especially on mobile), and improves user experience with minimal effort.
+
+- [ ] **Email queue system** - Use Bull/BullMQ to queue email sending and prevent blocking
+ - **Why:** Sending emails synchronously blocks requests and can cause timeouts. A queue system makes email sending non-blocking, improves response times, and provides retry logic for failed emails.
+
+- [ ] **Batch photo uploads** - Optimize bulk photo upload operations for better performance
+ - **Why:** Uploading photos one-by-one is slow and inefficient. Batch uploads reduce overhead, improve upload speeds by 2-3x, and provide better progress feedback to users.
+
+- [ ] **CDN integration** - Add CDN for static assets and uploaded images to reduce server load
+ - **Why:** Serving files from your server uses bandwidth and slows responses. A CDN serves files from locations closer to users, reducing latency by 50-80% and offloading server load.
+
+## ๐จ UI/UX Enhancements
+
+### User Interface
+- [ ] **Dark mode support** - Add dark mode toggle with user preference persistence
+ - **Why:** Many users prefer dark mode for reduced eye strain, especially in low-light. It's become an expected feature and can increase user satisfaction and retention.
+
+- [ ] **Mobile responsiveness** - Improve mobile experience with better touch targets and swipe gestures
+ - **Why:** A significant portion of users access from mobile. Poor mobile UX leads to frustration and abandonment. Better touch targets and gestures make the app feel native and professional.
+
+- [ ] **Loading skeletons** - Replace blank screens with loading skeletons for better perceived performance
+ - **Why:** Blank screens make the app feel broken or slow. Skeletons show structure immediately, making wait times feel shorter and the app more responsive, improving user confidence.
+
+- [ ] **Toast notifications** - Implement toast notifications for user feedback (success, error, info)
+ - **Why:** Users need clear feedback for their actions. Toast notifications are non-intrusive, provide immediate feedback, and improve UX by clearly communicating success/error states.
+
+- [ ] **Smooth page transitions** - Add smooth transitions between pages for better user experience
+ - **Why:** Abrupt page changes feel jarring. Smooth transitions create a polished, professional feel and make navigation feel faster and more cohesive.
+
+- [ ] **Inline form validation** - Show validation errors inline as user types for better feedback
+ - **Why:** Waiting until form submission to show errors is frustrating. Inline validation provides immediate feedback, reduces errors, and improves form completion rates.
+
+- [ ] **Keyboard navigation** - Add full keyboard navigation support for accessibility
+ - **Why:** Keyboard navigation is essential for accessibility (WCAG compliance) and power users. It makes your app usable for people with disabilities and improves efficiency for all users.
+
+- [ ] **Drag-and-drop uploads** - Allow users to drag and drop photos for easier uploads
+ - **Why:** Drag-and-drop is faster and more intuitive than clicking "Choose File". It's a modern UX pattern users expect and can increase upload engagement.
+
+- [ ] **Empty states** - Create better empty states for when there are no photos, guesses, etc.
+ - **Why:** Empty screens confuse users. Well-designed empty states guide users on what to do next, reduce confusion, and make the app feel more polished and helpful.
+
+- [ ] **Animations and micro-interactions** - Add subtle animations to improve user engagement
+ - **Why:** Subtle animations provide visual feedback, make interactions feel responsive, and create a premium, polished feel that increases user satisfaction and engagement.
+
+- [ ] **Infinite scroll** - Implement infinite scroll for photos list instead of pagination
+ - **Why:** Infinite scroll feels more modern and seamless, especially on mobile. It reduces friction and can increase engagement by making it easier to browse through photos.
+
+- [ ] **Photo filters and search** - Add filtering and search functionality to find photos easily
+ - **Why:** As the photo library grows, finding specific photos becomes difficult. Filters and search help users quickly find what they're looking for, improving usability and engagement.
+
+### User Experience
+- [ ] **Onboarding tour** - Create interactive tour for new users explaining key features
+ - **Why:** New users often don't understand how to use the app. An onboarding tour reduces confusion, increases feature discovery, and improves user retention by helping users get value quickly.
+
+- [ ] **Remember me option** - Add "Remember me" checkbox to login for longer session duration
+ - **Why:** Frequent logouts frustrate users. A "Remember me" option improves convenience, reduces friction, and increases user satisfaction, especially for regular users.
+
+- [ ] **Guess history display** - Show user's guess history for each photo (correct/wrong attempts)
+ - **Why:** Users want to see their past attempts. Showing guess history provides context, helps users learn, and adds transparency that increases trust and engagement.
+
+- [ ] **Already guessed indicator** - Display clear indicator when user has already guessed a photo
+ - **Why:** Without indicators, users waste time trying to guess photos they've already attempted. Clear indicators prevent confusion and improve efficiency.
+
+- [ ] **Photo tags system** - Allow users to tag photos for better organization and filtering
+ - **Why:** Tags help organize and categorize photos, making them easier to find and filter. This improves discoverability and allows for better content organization as the library grows.
+
+- [ ] **Photo descriptions/clues** - Add optional descriptions or clues to photos for hints
+ - **Why:** Some photos are too difficult without context. Optional clues make the game more accessible, increase engagement, and allow for more creative photo challenges.
+
+- [ ] **Confirmation dialogs** - Add confirmation dialogs for destructive actions (delete, etc.)
+ - **Why:** Accidental deletions are frustrating and can't be undone. Confirmation dialogs prevent mistakes, protect user data, and reduce support requests from accidental actions.
+
+- [ ] **Photo filtering** - Add filters to show: unguessed photos, correct guesses, wrong guesses, and photos by specific users
+ - **Why:** Users want to focus on specific subsets of photos. Filtering helps users find unguessed photos to play, review their performance, or follow specific uploaders, significantly improving usability.
+
+## ๐ Security Improvements
+
+- [ ] **CSRF protection** - Implement CSRF tokens for all state-changing operations
+ - **Why:** CSRF attacks can trick users into performing unwanted actions. CSRF protection is a security best practice that prevents unauthorized state changes and protects user accounts.
+
+- [ ] **Input sanitization** - Sanitize all user inputs to prevent XSS attacks
+ - **Why:** Malicious scripts in user input can steal data or hijack sessions. Input sanitization prevents XSS attacks, protects user data, and is essential for any app accepting user input.
+
+- [ ] **File type validation** - Validate actual file content (not just extension) to prevent malicious uploads
+ - **Why:** Attackers can rename malicious files with safe extensions. Content validation ensures only legitimate image files are uploaded, preventing security vulnerabilities and server compromise.
+
+- [ ] **Virus scanning** - Add virus scanning for uploaded files before storing
+ - **Why:** Malicious files can harm your server or other users. Virus scanning protects your infrastructure and users, especially important if files are shared or downloaded by others.
+
+- [ ] **Session timeout** - Implement automatic session timeout for inactive users
+ - **Why:** Long-lived sessions increase security risk if devices are compromised. Session timeouts reduce risk of unauthorized access and are a security best practice for sensitive applications.
+
+- [ ] **Security headers** - Add security headers (CSP, HSTS, X-Frame-Options, etc.)
+ - **Why:** Security headers prevent common attacks (clickjacking, XSS, MITM). They're easy to implement, provide significant security benefits, and are recommended by security standards.
+
+- [ ] **Audit logging** - Log all admin actions for security auditing and compliance
+ - **Why:** Admin actions can have significant impact. Audit logs provide accountability, help detect unauthorized access, and are essential for security compliance and incident investigation.
+
+## ๐ Features & Functionality
+
+### Game Mechanics
+- [ ] **Streak tracking** - Track consecutive correct guesses and display current streak
+ - **Why:** Streaks add excitement and encourage daily engagement. They create a sense of achievement and competition, increasing user retention and making the game more addictive.
+
+- [ ] **Achievements/badges system** - Create achievement system with badges for milestones
+ - **Why:** Achievements provide goals and recognition. They increase engagement, encourage exploration of features, and create a sense of progression that keeps users coming back.
+
+- [ ] **Hints system** - Allow optional hints for photos (earn fewer points when using hints)
+ - **Why:** Some photos are too difficult, leading to frustration. Hints make the game more accessible while still rewarding skill, increasing engagement and reducing abandonment.
+
+### Social Features
+- [ ] **Photo comments** - Allow users to comment on photos
+ - **Why:** Comments add social interaction and context. They increase engagement, create community, and make photos more interesting by adding discussion and stories.
+
+- [ ] **Reactions/emojis** - Add emoji reactions to photos for quick feedback
+ - **Why:** Reactions are quick, low-friction ways to engage. They increase interaction rates, add fun, and provide feedback to uploaders without requiring comments.
+
+- [ ] **User mentions** - Support @username mentions in comments
+ - **Why:** Mentions notify users and create conversations. They increase engagement, enable discussions, and help build community by connecting users.
+
+- [ ] **Activity feed** - Display recent activity feed (guesses, uploads, achievements)
+ - **Why:** Activity feeds show what's happening in real-time. They increase engagement, help users discover new content, and create a sense of community and activity.
+
+### Admin Features
+- [ ] **User activity logs dashboard** - Create admin dashboard showing user activity logs
+ - **Why:** Admins need visibility into user behavior. Activity logs help identify issues, understand usage patterns, detect abuse, and make data-driven decisions about the platform.
+
+- [ ] **Analytics dashboard** - Build analytics dashboard showing uploads, guesses, engagement metrics
+ - **Why:** Data-driven decisions require visibility. Analytics help understand what's working, identify trends, measure growth, and optimize features based on actual usage.
+
+- [ ] **System health monitoring** - Add system health monitoring and status page
+ - **Why:** Proactive monitoring prevents downtime. Health checks help catch issues before users notice, reduce support requests, and provide transparency about system status.
+
+- [ ] **Backup/export functionality** - Create tools to backup and export data
+ - **Why:** Data loss is catastrophic. Backups protect against disasters, and export functionality helps with migrations, compliance, and gives users control over their data.
+
+### Photo Management
+- [ ] **Photo editing tools** - Add basic editing (crop, rotate, filters) before upload
+ - **Why:** Users often need to adjust photos before uploading. Built-in editing reduces friction, improves photo quality, and eliminates the need for external editing tools.
+
+- [ ] **Improved duplicate detection** - Enhance duplicate photo detection beyond hash matching
+ - **Why:** Current hash matching only catches exact duplicates. Better detection (visual similarity) prevents near-duplicates, keeps content fresh, and improves user experience.
+
+- [ ] **Photo tagging system** - Implement tagging system for better organization
+ - **Why:** Tags enable better organization and discovery. They make it easier to find photos, create themed collections, and improve search functionality as content grows.
+
+- [ ] **Photo search functionality** - Add full-text search for photos by tags, descriptions, answers
+ - **Why:** As the library grows, finding photos becomes difficult. Search enables quick discovery, improves usability, and is essential for a good user experience at scale.
+
+- [ ] **Photo archiving** - Allow archiving old photos while keeping them accessible
+ - **Why:** Old photos clutter the main view but shouldn't be deleted. Archiving keeps content organized, maintains history, and improves the browsing experience for active content.
+
+- [ ] **Photo approval workflow** - Add optional admin approval workflow (can be toggled on/off)
+ - **Why:** Content moderation ensures quality and safety. An approval workflow lets admins control what goes public, prevents inappropriate content, and maintains content standards.
+
+## ๐งช Testing & Quality
+
+- [ ] **Increase test coverage** - Expand test coverage to all critical paths and edge cases
+ - **Why:** Bugs in production are expensive and damage trust. Comprehensive tests catch issues early, reduce regressions, and give confidence when making changes.
+
+- [ ] **E2E tests** - Add end-to-end tests using Playwright or Cypress
+ - **Why:** Unit tests don't catch integration issues. E2E tests verify the full user flow works, catch real-world bugs, and prevent breaking changes from reaching users.
+
+- [ ] **Visual regression testing** - Implement visual regression testing to catch UI changes
+ - **Why:** UI bugs are easy to miss in code review. Visual regression tests catch unintended visual changes, ensure consistency, and save time in QA.
+
+- [ ] **Performance testing** - Add Lighthouse CI for continuous performance monitoring
+ - **Why:** Performance degrades over time. Automated performance testing catches regressions early, ensures good Core Web Vitals, and maintains fast user experience.
+
+- [ ] **Load testing** - Create load testing scenarios to test system under stress
+ - **Why:** Systems behave differently under load. Load testing reveals bottlenecks, ensures the app can handle traffic spikes, and prevents crashes during high usage.
+
+- [ ] **API integration tests** - Add comprehensive integration tests for all API routes
+ - **Why:** API bugs affect all clients. Integration tests verify API contracts, catch breaking changes, and ensure APIs work correctly with the database and auth.
+
+- [ ] **Accessibility testing** - Implement automated accessibility testing (a11y)
+ - **Why:** Accessibility is a legal requirement and moral obligation. Automated testing catches a11y issues early, ensures WCAG compliance, and makes the app usable for everyone.
+
+- [ ] **Error boundaries** - Add React error boundaries to gracefully handle component errors
+ - **Why:** One component error shouldn't crash the entire app. Error boundaries isolate failures, show helpful error messages, and keep the rest of the app functional.
+
+- [ ] **Error logging and monitoring** - Create comprehensive error logging and monitoring system
+ - **Why:** You can't fix what you can't see. Error monitoring catches production bugs, provides context for debugging, and helps prioritize fixes based on impact.
+
+## ๐ง Technical Improvements
+
+### Architecture
+- [ ] **Microservices migration** - Consider migrating to microservices if scale requires it
+ - **Why:** Monoliths become hard to scale and maintain at large scale. Microservices enable independent scaling, faster deployments, and better fault isolation, but only if needed.
+
+- [ ] **Event-driven architecture** - Implement event-driven architecture for notifications
+ - **Why:** Tight coupling makes systems brittle. Event-driven architecture decouples components, improves scalability, and makes it easier to add new features without modifying existing code.
+
+- [ ] **Message queue** - Add message queue (RabbitMQ, Redis Queue) for async operations
+ - **Why:** Synchronous operations block requests. Message queues enable async processing, improve response times, provide retry logic, and handle traffic spikes better.
+
+- [ ] **GraphQL API** - Consider GraphQL API for more flexible querying
+ - **Why:** REST APIs often over-fetch or under-fetch data. GraphQL lets clients request exactly what they need, reduces bandwidth, and simplifies frontend development.
+
+- [ ] **API versioning** - Implement API versioning for backward compatibility
+ - **Why:** API changes break clients. Versioning allows gradual migration, prevents breaking changes, and enables supporting multiple client versions simultaneously.
+
+- [ ] **Health check endpoints** - Add health check endpoints for monitoring
+ - **Why:** Monitoring systems need to check if the app is healthy. Health endpoints enable automated monitoring, load balancer health checks, and quick status verification.
+
+- [ ] **Monitoring and alerting** - Create comprehensive monitoring and alerting system
+ - **Why:** Issues caught early are easier to fix. Monitoring provides visibility, alerts notify of problems immediately, and helps maintain uptime and performance.
+
+### Code Quality
+- [ ] **ESLint strict rules** - Add stricter ESLint rules for better code quality
+ - **Why:** Code quality issues lead to bugs. Stricter linting catches problems early, enforces best practices, and maintains consistent code style across the team.
+
+- [ ] **Prettier formatting** - Implement Prettier for consistent code formatting
+ - **Why:** Inconsistent formatting wastes time in reviews. Prettier automates formatting, eliminates style debates, and ensures consistent code across the codebase.
+
+- [ ] **Pre-commit hooks** - Add Husky pre-commit hooks to run linters and tests
+ - **Why:** Catching issues before commit saves time. Pre-commit hooks prevent bad code from entering the repo, enforce quality standards, and reduce CI failures.
+
+- [ ] **Component library/storybook** - Create component library with Storybook documentation
+ - **Why:** Reusable components reduce duplication. Storybook documents components, enables isolated development, and helps maintain design consistency.
+
+- [ ] **TypeScript strict mode** - Enable TypeScript strict mode for better type safety
+ - **Why:** Type errors cause runtime bugs. Strict mode catches more errors at compile time, improves code quality, and provides better IDE support and refactoring safety.
+
+- [ ] **Error tracking** - Integrate error tracking (Sentry, LogRocket) for production monitoring
+ - **Why:** Production errors are hard to debug without context. Error tracking provides stack traces, user context, and helps prioritize and fix bugs quickly.
+
+- [ ] **Performance monitoring** - Add performance monitoring (New Relic, Datadog)
+ - **Why:** Slow performance hurts user experience. Performance monitoring identifies bottlenecks, tracks metrics over time, and helps optimize critical paths.
+
+### DevOps & Infrastructure
+- [ ] **CI/CD pipeline** - Set up continuous integration and deployment pipeline
+ - **Why:** Manual deployments are error-prone and slow. CI/CD automates testing and deployment, reduces human error, enables faster releases, and improves confidence in changes.
+
+- [ ] **Automated deployments** - Implement automated deployments on merge to main
+ - **Why:** Manual deployments delay releases and can be forgotten. Automation ensures consistent deployments, reduces downtime, and enables rapid iteration.
+
+- [ ] **Blue-green deployments** - Set up blue-green deployment strategy for zero downtime
+ - **Why:** Deployments cause downtime and risk. Blue-green deployments enable zero-downtime updates, instant rollbacks, and reduce deployment risk significantly.
+
+- [ ] **Database migration strategy** - Create proper database migration strategy and rollback plan
+ - **Why:** Database changes are risky and hard to undo. A migration strategy ensures safe, reversible changes and prevents data loss or corruption during updates.
+
+- [ ] **Staging environment** - Create staging environment for testing before production
+ - **Why:** Testing in production is dangerous. Staging mirrors production, allows safe testing, and catches issues before they affect real users.
+
+- [ ] **Automated backups** - Implement automated database and file backups
+ - **Why:** Data loss is catastrophic. Automated backups protect against disasters, enable recovery, and are essential for any production system.
+
+- [ ] **Monitoring and logging** - Set up ELK stack or Grafana for centralized logging and monitoring
+ - **Why:** Logs are essential for debugging and monitoring. Centralized logging makes logs searchable, enables alerting, and provides visibility into system behavior.
+
+- [ ] **Container orchestration** - Set up Docker and Kubernetes for container orchestration
+ - **Why:** Containers provide consistency and scalability. Kubernetes enables easy scaling, rolling updates, and manages infrastructure, making deployment and operations easier.
+
+---
+
+## Priority Recommendations
+
+### High Priority (Quick Wins)
+1. **Image Optimization** - Use Next.js Image component for automatic optimization
+3. **Loading States** - Add loading skeletons for better UX
+4. **Toast Notifications** - Implement toast notifications for user feedback
+5. **Error Boundaries** - Add error boundaries for graceful error handling
+6. **Rate Limiting** - Add rate limiting to prevent abuse
+
+### Medium Priority (Significant Impact)
+1. **Caching** - Implement Redis caching for leaderboard and frequently accessed data
+2. **Dark Mode** - Add dark mode support for user preference
+3. **Search Functionality** - Add photo search and filtering capabilities
+4. **Achievements System** - Implement achievements/badges for increased engagement
+5. **Analytics Dashboard** - Create admin analytics dashboard for insights
+
+### Low Priority (Nice to Have)
+1. **Social Features** - Add comments, reactions, and activity feed
+2. **Advanced Gamification** - Implement levels, ranks, and advanced gamification features
+
+---
+
+**Note:** This is a living document. Add new ideas as they come up, and mark items as completed when implemented.
diff --git a/README.md b/README.md
index 649043d..daf4387 100644
--- a/README.md
+++ b/README.md
@@ -166,12 +166,17 @@ npm start
- Photos are uploaded to `public/uploads/` directory
- Files are served via `/api/uploads/[filename]` API route
- Ensure the uploads directory has proper write permissions
+
+**Upload Endpoints:**
+- `POST /api/photos/upload` - Single photo upload (supports both file and URL uploads)
+- `POST /api/photos/upload-multiple` - Multiple photo uploads in batch (used by upload page)
- 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
+- **Note:** For local development, use `./watch-activity.sh` script (if systemd/journalctl is not available, check application logs directly)
## Database Commands
diff --git a/REBUILD.md b/REBUILD.md
new file mode 100644
index 0000000..d410fd5
--- /dev/null
+++ b/REBUILD.md
@@ -0,0 +1,69 @@
+# Rebuild Scripts
+
+## Quick Start
+
+### Production Mode (Recommended for testing)
+```bash
+./rebuild.sh prod
+# or just
+./rebuild.sh
+```
+
+### Development Mode (Hot reload)
+```bash
+./rebuild.sh dev
+```
+
+## What it does
+
+1. **Kills all processes** - Stops any running Node/Next.js processes
+2. **Frees ports** - Ensures ports 3000 and 3003 are available
+3. **Cleans build artifacts** - Removes `.next`, cache files, etc.
+4. **Rebuilds** (production only) - Runs `npm run build`
+5. **Starts server** - Runs in foreground (dev) or background (prod)
+
+## Viewing Logs
+
+### Production Mode
+```bash
+tail -f /tmp/mirrormatch-server.log
+```
+
+### Development Mode
+Logs appear directly in the terminal (foreground mode)
+
+### Watching Activity Logs
+```bash
+# If using rebuild.sh (production mode)
+./watch-activity.sh /tmp/mirrormatch-server.log
+
+# If using systemd service
+./watch-activity.sh
+
+# Or specify custom log file
+./watch-activity.sh /path/to/your/logfile.log
+```
+
+## Manual Commands
+
+If you prefer to run commands manually:
+
+```bash
+# Kill everything
+sudo fuser -k 3000/tcp
+killall -9 node
+pkill -f "next"
+sleep 2
+
+# Clean
+cd /home/beast/Code/mirrormatch
+rm -rf .next node_modules/.cache
+
+# Rebuild (production)
+npm run build
+
+# Start
+NODE_ENV=production npm run start > /tmp/server.log 2>&1 &
+# or for dev
+NODE_ENV=development npm run dev
+```
diff --git a/__tests__/components/HelpModal.test.tsx b/__tests__/components/HelpModal.test.tsx
new file mode 100644
index 0000000..633d127
--- /dev/null
+++ b/__tests__/components/HelpModal.test.tsx
@@ -0,0 +1,117 @@
+import { render, screen, fireEvent, act } from "@testing-library/react"
+import HelpModal from "@/components/HelpModal"
+
+describe("HelpModal", () => {
+ it("should not render initially", () => {
+ render()
+ expect(screen.queryByText("Welcome to MirrorMatch")).not.toBeInTheDocument()
+ })
+
+ it("should open when Shift+? is pressed", () => {
+ render()
+
+ // Simulate Shift+? (Shift+/)
+ act(() => {
+ fireEvent.keyDown(window, {
+ key: "/",
+ shiftKey: true,
+ ctrlKey: false,
+ metaKey: false,
+ })
+ })
+
+ expect(screen.getByText("Welcome to MirrorMatch")).toBeInTheDocument()
+ })
+
+ it("should close when Escape is pressed", () => {
+ render()
+
+ // Open modal first
+ act(() => {
+ fireEvent.keyDown(window, {
+ key: "/",
+ shiftKey: true,
+ ctrlKey: false,
+ metaKey: false,
+ })
+ })
+
+ expect(screen.getByText("Welcome to MirrorMatch")).toBeInTheDocument()
+
+ // Close with Escape
+ act(() => {
+ fireEvent.keyDown(window, {
+ key: "Escape",
+ })
+ })
+
+ // Modal should close
+ expect(screen.queryByText("Welcome to MirrorMatch")).not.toBeInTheDocument()
+ })
+
+ it("should not open with Ctrl+? (should require Shift only)", () => {
+ render()
+
+ // Simulate Ctrl+? (Ctrl+Shift+/)
+ act(() => {
+ fireEvent.keyDown(window, {
+ key: "/",
+ shiftKey: true,
+ ctrlKey: true, // Should prevent opening
+ metaKey: false,
+ })
+ })
+
+ // Modal should not open when Ctrl is also pressed
+ expect(screen.queryByText("Welcome to MirrorMatch")).not.toBeInTheDocument()
+ })
+
+ it("should toggle when Shift+? is pressed multiple times", () => {
+ render()
+
+ // First press - should open
+ act(() => {
+ fireEvent.keyDown(window, {
+ key: "/",
+ shiftKey: true,
+ ctrlKey: false,
+ metaKey: false,
+ })
+ })
+
+ expect(screen.getByText("Welcome to MirrorMatch")).toBeInTheDocument()
+
+ // Second press - should close
+ act(() => {
+ fireEvent.keyDown(window, {
+ key: "/",
+ shiftKey: true,
+ ctrlKey: false,
+ metaKey: false,
+ })
+ })
+
+ expect(screen.queryByText("Welcome to MirrorMatch")).not.toBeInTheDocument()
+ })
+
+ it("should display help content when open", () => {
+ render()
+
+ // Open modal
+ act(() => {
+ fireEvent.keyDown(window, {
+ key: "/",
+ shiftKey: true,
+ ctrlKey: false,
+ metaKey: false,
+ })
+ })
+
+ // Check for key content sections
+ expect(screen.getByText("What is MirrorMatch?")).toBeInTheDocument()
+ expect(screen.getByText("How to Play")).toBeInTheDocument()
+ expect(screen.getByText("Key Features")).toBeInTheDocument()
+ expect(screen.getByText("Keyboard Shortcuts")).toBeInTheDocument()
+ expect(screen.getByText("Tips")).toBeInTheDocument()
+ })
+})
diff --git a/__tests__/lib/activity-log.test.ts b/__tests__/lib/activity-log.test.ts
new file mode 100644
index 0000000..7290491
--- /dev/null
+++ b/__tests__/lib/activity-log.test.ts
@@ -0,0 +1,209 @@
+import { logActivity } from '@/lib/activity-log';
+import { logger } from '@/lib/logger';
+
+// Mock the logger
+jest.mock('@/lib/logger', () => ({
+ logger: {
+ info: jest.fn(),
+ debug: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+// Helper to create a mock Request object
+function createMockRequest(headers: Record = {}): Request {
+ const mockHeaders = new Headers();
+ Object.entries(headers).forEach(([key, value]) => {
+ mockHeaders.set(key, value);
+ });
+
+ return {
+ headers: mockHeaders,
+ } as unknown as Request;
+}
+
+describe('activity-log', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('logActivity', () => {
+ it('should create activity log with all fields', () => {
+ const mockRequest = createMockRequest({
+ 'x-forwarded-for': '192.168.1.1',
+ });
+
+ const user = {
+ id: 'user-123',
+ email: 'test@example.com',
+ role: 'USER',
+ };
+
+ const details = { photoId: 'photo-456' };
+
+ const result = logActivity(
+ 'PHOTO_UPLOAD',
+ '/api/photos/upload',
+ 'POST',
+ user,
+ details,
+ mockRequest
+ );
+
+ expect(result).toMatchObject({
+ action: 'PHOTO_UPLOAD',
+ path: '/api/photos/upload',
+ method: 'POST',
+ userId: 'user-123',
+ userEmail: 'test@example.com',
+ userRole: 'USER',
+ ip: '192.168.1.1',
+ details: { photoId: 'photo-456' },
+ });
+ expect(result.timestamp).toBeDefined();
+ });
+
+ it('should handle unauthenticated users', () => {
+ const result = logActivity(
+ 'PAGE_VIEW',
+ '/photos',
+ 'GET',
+ null,
+ undefined,
+ undefined
+ );
+
+ expect(result).toMatchObject({
+ action: 'PAGE_VIEW',
+ path: '/photos',
+ method: 'GET',
+ userId: undefined,
+ userEmail: undefined,
+ userRole: undefined,
+ ip: 'unknown',
+ });
+ });
+
+ it('should extract IP from x-forwarded-for header', () => {
+ const mockRequest = createMockRequest({
+ 'x-forwarded-for': '192.168.1.1, 10.0.0.1',
+ });
+
+ const result = logActivity(
+ 'ACTION',
+ '/path',
+ 'GET',
+ undefined,
+ undefined,
+ mockRequest
+ );
+
+ expect(result.ip).toBe('192.168.1.1');
+ });
+
+ it('should extract IP from x-real-ip header when x-forwarded-for is missing', () => {
+ const mockRequest = createMockRequest({
+ 'x-real-ip': '10.0.0.1',
+ });
+
+ const result = logActivity(
+ 'ACTION',
+ '/path',
+ 'GET',
+ undefined,
+ undefined,
+ mockRequest
+ );
+
+ expect(result.ip).toBe('10.0.0.1');
+ });
+
+ it('should use "unknown" for IP when no headers are present', () => {
+ const mockRequest = createMockRequest();
+
+ const result = logActivity(
+ 'ACTION',
+ '/path',
+ 'GET',
+ undefined,
+ undefined,
+ mockRequest
+ );
+
+ expect(result.ip).toBe('unknown');
+ });
+
+ it('should call logger.info with structured data', () => {
+ const user = {
+ id: 'user-123',
+ email: 'test@example.com',
+ role: 'USER',
+ };
+
+ const details = { photoId: 'photo-456' };
+
+ logActivity(
+ 'PHOTO_UPLOAD',
+ '/api/photos/upload',
+ 'POST',
+ user,
+ details
+ );
+
+ expect(logger.info).toHaveBeenCalledWith(
+ 'Activity: PHOTO_UPLOAD',
+ expect.objectContaining({
+ method: 'POST',
+ path: '/api/photos/upload',
+ userId: 'user-123',
+ userEmail: 'test@example.com',
+ userRole: 'USER',
+ details: { photoId: 'photo-456' },
+ })
+ );
+ });
+
+ it('should not include details in logger call when details are not provided', () => {
+ const user = {
+ id: 'user-123',
+ email: 'test@example.com',
+ role: 'USER',
+ };
+
+ logActivity(
+ 'PAGE_VIEW',
+ '/photos',
+ 'GET',
+ user
+ );
+
+ expect(logger.info).toHaveBeenCalledWith(
+ 'Activity: PAGE_VIEW',
+ expect.objectContaining({
+ method: 'GET',
+ path: '/photos',
+ userId: 'user-123',
+ userEmail: 'test@example.com',
+ userRole: 'USER',
+ })
+ );
+
+ const callArgs = (logger.info as jest.Mock).mock.calls[0][1];
+ expect(callArgs).not.toHaveProperty('details');
+ });
+
+ it('should handle empty details object', () => {
+ const result = logActivity(
+ 'ACTION',
+ '/path',
+ 'GET',
+ undefined,
+ {}
+ );
+
+ expect(result.details).toEqual({});
+ expect(logger.info).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/__tests__/lib/logger.test.ts b/__tests__/lib/logger.test.ts
new file mode 100644
index 0000000..4dea221
--- /dev/null
+++ b/__tests__/lib/logger.test.ts
@@ -0,0 +1,210 @@
+import { logger, LogLevel, getLogLevel, formatLog, createLogger } from '@/lib/logger';
+
+// Mock console methods
+const originalConsole = { ...console };
+const mockConsole = {
+ log: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+};
+
+describe('Logger', () => {
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ console.log = mockConsole.log;
+ console.warn = mockConsole.warn;
+ console.error = mockConsole.error;
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ // Use type assertion to allow deletion
+ delete (process.env as { LOG_LEVEL?: string }).LOG_LEVEL;
+ delete (process.env as { LOG_FORMAT?: string }).LOG_FORMAT;
+ delete (process.env as { NODE_ENV?: string }).NODE_ENV;
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ afterEach(() => {
+ console.log = originalConsole.log;
+ console.warn = originalConsole.warn;
+ console.error = originalConsole.error;
+ });
+
+ describe('getLogLevel', () => {
+ it('should return DEBUG when LOG_LEVEL=DEBUG', () => {
+ process.env.LOG_LEVEL = 'DEBUG';
+ expect(getLogLevel()).toBe(LogLevel.DEBUG);
+ });
+
+ it('should return INFO when LOG_LEVEL=INFO', () => {
+ process.env.LOG_LEVEL = 'INFO';
+ expect(getLogLevel()).toBe(LogLevel.INFO);
+ });
+
+ it('should return WARN when LOG_LEVEL=WARN', () => {
+ process.env.LOG_LEVEL = 'WARN';
+ expect(getLogLevel()).toBe(LogLevel.WARN);
+ });
+
+ it('should return ERROR when LOG_LEVEL=ERROR', () => {
+ process.env.LOG_LEVEL = 'ERROR';
+ expect(getLogLevel()).toBe(LogLevel.ERROR);
+ });
+
+ it('should return NONE when LOG_LEVEL=NONE', () => {
+ process.env.LOG_LEVEL = 'NONE';
+ expect(getLogLevel()).toBe(LogLevel.NONE);
+ });
+
+ it('should default to DEBUG in development', () => {
+ (process.env as { NODE_ENV?: string }).NODE_ENV = 'development';
+ expect(getLogLevel()).toBe(LogLevel.DEBUG);
+ });
+
+ it('should default to INFO in production', () => {
+ (process.env as { NODE_ENV?: string }).NODE_ENV = 'production';
+ expect(getLogLevel()).toBe(LogLevel.INFO);
+ });
+
+ it('should ignore invalid LOG_LEVEL values and use defaults', () => {
+ (process.env as { LOG_LEVEL?: string }).LOG_LEVEL = 'INVALID';
+ (process.env as { NODE_ENV?: string }).NODE_ENV = 'production';
+ expect(getLogLevel()).toBe(LogLevel.INFO);
+ });
+ });
+
+ describe('formatLog', () => {
+ it('should format log in human-readable format by default', () => {
+ const result = formatLog(LogLevel.INFO, 'Test message', { key: 'value' });
+ expect(result).toContain('[INFO]');
+ expect(result).toContain('Test message');
+ expect(result).toContain('{"key":"value"}');
+ });
+
+ it('should format log as JSON when LOG_FORMAT=json', () => {
+ process.env.LOG_FORMAT = 'json';
+ const result = formatLog(LogLevel.INFO, 'Test message', { key: 'value' });
+ const parsed = JSON.parse(result);
+ expect(parsed.level).toBe('INFO');
+ expect(parsed.message).toBe('Test message');
+ expect(parsed.key).toBe('value');
+ expect(parsed.timestamp).toBeDefined();
+ });
+
+ it('should format Error objects correctly', () => {
+ const error = new Error('Test error');
+ const result = formatLog(LogLevel.ERROR, 'Error occurred', error);
+ expect(result).toContain('[ERROR]');
+ expect(result).toContain('Error occurred');
+ expect(result).toContain('Error: Error: Test error');
+ });
+
+ it('should format Error objects as JSON when LOG_FORMAT=json', () => {
+ process.env.LOG_FORMAT = 'json';
+ const error = new Error('Test error');
+ const result = formatLog(LogLevel.ERROR, 'Error occurred', error);
+ const parsed = JSON.parse(result);
+ expect(parsed.level).toBe('ERROR');
+ expect(parsed.message).toBe('Error occurred');
+ expect(parsed.error.name).toBe('Error');
+ expect(parsed.error.message).toBe('Test error');
+ expect(parsed.error.stack).toBeDefined();
+ });
+
+ it('should handle logs without context', () => {
+ const result = formatLog(LogLevel.INFO, 'Simple message');
+ expect(result).toContain('[INFO]');
+ expect(result).toContain('Simple message');
+ // Format always includes pipe separator, but no context data after it
+ expect(result).toContain('|');
+ expect(result.split('|').length).toBe(2); // timestamp | message (no context)
+ });
+ });
+
+ describe('Logger instance', () => {
+ it('should log DEBUG messages when level is DEBUG', () => {
+ process.env.LOG_LEVEL = 'DEBUG';
+ const testLogger = createLogger();
+ testLogger.debug('Debug message', { data: 'test' });
+ expect(mockConsole.log).toHaveBeenCalled();
+ });
+
+ it('should not log DEBUG messages when level is INFO', () => {
+ process.env.LOG_LEVEL = 'INFO';
+ const testLogger = createLogger();
+ testLogger.debug('Debug message');
+ expect(mockConsole.log).not.toHaveBeenCalled();
+ });
+
+ it('should log INFO messages when level is INFO', () => {
+ process.env.LOG_LEVEL = 'INFO';
+ const testLogger = createLogger();
+ testLogger.info('Info message');
+ expect(mockConsole.log).toHaveBeenCalled();
+ });
+
+ it('should log WARN messages when level is WARN', () => {
+ process.env.LOG_LEVEL = 'WARN';
+ const testLogger = createLogger();
+ testLogger.warn('Warning message');
+ expect(mockConsole.warn).toHaveBeenCalled();
+ });
+
+ it('should not log INFO messages when level is WARN', () => {
+ process.env.LOG_LEVEL = 'WARN';
+ const testLogger = createLogger();
+ testLogger.info('Info message');
+ expect(mockConsole.log).not.toHaveBeenCalled();
+ });
+
+ it('should log ERROR messages when level is ERROR', () => {
+ process.env.LOG_LEVEL = 'ERROR';
+ const testLogger = createLogger();
+ testLogger.error('Error message');
+ expect(mockConsole.error).toHaveBeenCalled();
+ });
+
+ it('should not log any messages when level is NONE', () => {
+ process.env.LOG_LEVEL = 'NONE';
+ const testLogger = createLogger();
+ testLogger.debug('Debug message');
+ testLogger.info('Info message');
+ testLogger.warn('Warning message');
+ testLogger.error('Error message');
+ expect(mockConsole.log).not.toHaveBeenCalled();
+ expect(mockConsole.warn).not.toHaveBeenCalled();
+ expect(mockConsole.error).not.toHaveBeenCalled();
+ });
+
+ it('should handle Error objects in error method', () => {
+ process.env.LOG_LEVEL = 'ERROR';
+ const testLogger = createLogger();
+ const error = new Error('Test error');
+ testLogger.error('Error occurred', error);
+ expect(mockConsole.error).toHaveBeenCalled();
+ const callArgs = mockConsole.error.mock.calls[0][0];
+ expect(callArgs).toContain('Error occurred');
+ });
+
+ it('isLevelEnabled should return correct values', () => {
+ process.env.LOG_LEVEL = 'WARN';
+ const testLogger = createLogger();
+ expect(testLogger.isLevelEnabled(LogLevel.DEBUG)).toBe(false);
+ expect(testLogger.isLevelEnabled(LogLevel.INFO)).toBe(false);
+ expect(testLogger.isLevelEnabled(LogLevel.WARN)).toBe(true);
+ expect(testLogger.isLevelEnabled(LogLevel.ERROR)).toBe(true);
+ });
+ });
+
+ describe('Default logger instance', () => {
+ it('should be available and functional', () => {
+ process.env.LOG_LEVEL = 'INFO';
+ logger.info('Test message');
+ expect(mockConsole.log).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/app/api/admin/users/[userId]/reset-password/route.ts b/app/api/admin/users/[userId]/reset-password/route.ts
index 6cf4bc4..53fc878 100644
--- a/app/api/admin/users/[userId]/reset-password/route.ts
+++ b/app/api/admin/users/[userId]/reset-password/route.ts
@@ -1,12 +1,17 @@
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
+import { logger } from "@/lib/logger"
import { hashPassword } from "@/lib/utils"
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
+
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ userId: string }> }
) {
+ let userId: string | undefined
try {
const session = await auth()
@@ -14,7 +19,7 @@ export async function POST(
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
- const { userId } = await params
+ userId = (await params).userId
const { password } = await req.json()
if (!password || password.length < 6) {
@@ -33,7 +38,10 @@ export async function POST(
return NextResponse.json({ success: true })
} catch (error) {
- console.error("Error resetting password:", error)
+ logger.error("Error resetting password", {
+ userId,
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts
index 3288412..7bb01c4 100644
--- a/app/api/admin/users/route.ts
+++ b/app/api/admin/users/route.ts
@@ -2,6 +2,10 @@ import { NextRequest, NextResponse } from "next/server"
import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
import { hashPassword } from "@/lib/utils"
+import { logger } from "@/lib/logger"
+
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
export async function POST(req: NextRequest) {
try {
@@ -53,7 +57,9 @@ export async function POST(req: NextRequest) {
{ status: 201 }
)
} catch (error) {
- console.error("Error creating user:", error)
+ logger.error("Error creating user", {
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
index 866b2be..f312a22 100644
--- a/app/api/auth/[...nextauth]/route.ts
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -1,3 +1,7 @@
import { handlers } from "@/lib/auth"
+// Mark this route as dynamic to prevent build-time data collection
+// NextAuth requires runtime environment variables and cannot be pre-rendered
+export const dynamic = "force-dynamic"
+
export const { GET, POST } = handlers
diff --git a/app/api/debug/session/route.ts b/app/api/debug/session/route.ts
index 2f9ed46..ac1416b 100644
--- a/app/api/debug/session/route.ts
+++ b/app/api/debug/session/route.ts
@@ -1,9 +1,32 @@
import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"
import { cookies } from "next/headers"
+import { SESSION_COOKIE_NAME } from "@/lib/constants"
+import { logger } from "@/lib/logger"
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
+
+/**
+ * Debug endpoint for session inspection
+ * ADMIN ONLY - Protected endpoint for debugging session issues
+ *
+ * This endpoint should only be accessible to administrators.
+ * Consider removing in production or restricting further.
+ */
export async function GET(request: Request) {
try {
+ // Require admin authentication
+ const session = await auth()
+
+ if (!session || session.user.role !== "ADMIN") {
+ logger.warn("Unauthorized access attempt to debug endpoint", {
+ userId: session?.user?.id,
+ userRole: session?.user?.role,
+ path: "/api/debug/session",
+ })
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+ }
const cookieHeader = request.headers.get("cookie") || ""
// Parse cookies from header first
@@ -16,29 +39,29 @@ export async function GET(request: Request) {
})
// Try to get session token from cookies
- const sessionTokenFromHeader = cookieMap["__Secure-authjs.session-token"] || "NOT FOUND"
+ const sessionTokenFromHeader = cookieMap[SESSION_COOKIE_NAME] || "NOT FOUND"
- // Try to call auth() - this might fail or return null
- let session = null
+ // Try to call auth() again for debugging (we already have session above, but this is for testing)
let authError = null
try {
- console.log("Debug endpoint: Calling auth()...")
- session = await auth()
- console.log("Debug endpoint: auth() returned", {
+ // Already called above, but keeping for backward compatibility in response
+ logger.debug("Debug endpoint: Session retrieved", {
hasSession: !!session,
- sessionUser: session?.user,
- sessionKeys: session ? Object.keys(session) : []
+ userId: session?.user?.id,
+ userRole: session?.user?.role,
})
} catch (err) {
authError = err instanceof Error ? err.message : String(err)
- console.error("Debug endpoint: auth() error", authError)
+ logger.error("Debug endpoint: auth() error", {
+ error: err instanceof Error ? err : new Error(String(err)),
+ })
}
// Try to get cookie from Next.js cookie store
let sessionTokenFromStore = "NOT ACCESSIBLE"
try {
const cookieStore = await cookies()
- sessionTokenFromStore = cookieStore.get("__Secure-authjs.session-token")?.value || "NOT FOUND"
+ sessionTokenFromStore = cookieStore.get(SESSION_COOKIE_NAME)?.value || "NOT FOUND"
} catch {
// Cookie store might not be accessible in all contexts
}
diff --git a/app/api/photos/[photoId]/guess/route.ts b/app/api/photos/[photoId]/guess/route.ts
index 0db93d9..53540e7 100644
--- a/app/api/photos/[photoId]/guess/route.ts
+++ b/app/api/photos/[photoId]/guess/route.ts
@@ -3,6 +3,10 @@ import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
import { normalizeString } from "@/lib/utils"
import { logActivity } from "@/lib/activity-log"
+import { logger } from "@/lib/logger"
+
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
export async function POST(
req: NextRequest,
@@ -151,7 +155,9 @@ export async function POST(
pointsChange
})
} catch (error) {
- console.error("Error submitting guess:", error)
+ logger.error("Error submitting guess", {
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/api/photos/[photoId]/route.ts b/app/api/photos/[photoId]/route.ts
index efac356..86517b6 100644
--- a/app/api/photos/[photoId]/route.ts
+++ b/app/api/photos/[photoId]/route.ts
@@ -1,10 +1,14 @@
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
+import { logger } from "@/lib/logger"
import { unlink } from "fs/promises"
import { join } from "path"
import { existsSync } from "fs"
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
+
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ photoId: string }> }
@@ -47,7 +51,10 @@ export async function DELETE(
try {
await unlink(filepath)
} catch (error) {
- console.error("Failed to delete file:", filepath, error)
+ logger.error("Failed to delete file", {
+ filepath,
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
// Continue with database deletion even if file deletion fails
}
}
@@ -60,7 +67,9 @@ export async function DELETE(
return NextResponse.json({ success: true, message: "Photo deleted successfully" })
} catch (error) {
- console.error("Error deleting photo:", error)
+ logger.error("Error deleting photo", {
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/api/photos/route.ts b/app/api/photos/route.ts
deleted file mode 100644
index e89474b..0000000
--- a/app/api/photos/route.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { NextRequest, NextResponse } from "next/server"
-import { auth } from "@/lib/auth"
-import { prisma } from "@/lib/prisma"
-import { sendNewPhotoEmail } from "@/lib/email"
-
-// Legacy endpoint for URL-based uploads (kept for backward compatibility)
-export async function POST(req: NextRequest) {
- try {
- const session = await auth()
-
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
- }
-
- const { url, answerName, points, maxAttempts } = await req.json()
-
- if (!url || !answerName) {
- return NextResponse.json(
- { error: "URL and answer name are required" },
- { status: 400 }
- )
- }
-
- // Validate points (must be positive integer, default to 1)
- const pointsValue = points ? Math.max(1, parseInt(points, 10)) : 1
- const maxAttemptsValue = maxAttempts && parseInt(maxAttempts, 10) > 0
- ? parseInt(maxAttempts, 10)
- : null
-
- // Check for duplicate URL
- const existingPhoto = await prisma.photo.findFirst({
- where: { url },
- })
-
- if (existingPhoto) {
- return NextResponse.json(
- { error: "This photo URL has already been uploaded (duplicate URL detected)" },
- { status: 409 }
- )
- }
-
- const photo = await prisma.photo.create({
- data: {
- uploaderId: session.user.id,
- url,
- answerName: answerName.trim(),
- points: pointsValue,
- maxAttempts: maxAttemptsValue,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- } as any,
- include: {
- uploader: {
- select: {
- name: true,
- },
- },
- },
- })
-
- // Send emails to all other users
- const allUsers = await prisma.user.findMany({
- where: {
- id: { not: session.user.id },
- },
- select: {
- id: true,
- email: true,
- name: true,
- },
- })
-
- // Send emails asynchronously (don't wait for them)
- Promise.all(
- allUsers.map((user: { id: string; email: string; name: string }) =>
- sendNewPhotoEmail(user.email, user.name, photo.id, photo.uploader.name).catch(
- (err) => {
- console.error("Failed to send email to:", user.email, err)
- }
- )
- )
- )
-
- return NextResponse.json({ photo }, { status: 201 })
- } catch (error) {
- console.error("Error creating photo:", error)
- return NextResponse.json(
- { error: "Internal server error" },
- { status: 500 }
- )
- }
-}
diff --git a/app/api/photos/upload-multiple/route.ts b/app/api/photos/upload-multiple/route.ts
index 207f848..629a17a 100644
--- a/app/api/photos/upload-multiple/route.ts
+++ b/app/api/photos/upload-multiple/route.ts
@@ -1,12 +1,39 @@
+/**
+ * Multiple Photo Upload Endpoint
+ *
+ * POST /api/photos/upload-multiple
+ *
+ * Uploads multiple photos in a single request. Supports both file uploads and URL-based uploads.
+ *
+ * This endpoint is used by the upload page for batch uploads. It processes multiple photos
+ * in parallel and sends email notifications for all successfully uploaded photos.
+ *
+ * Form Data:
+ * - photo_{index}_file: File object (optional, if using file upload)
+ * - photo_{index}_url: URL string (optional, if using URL upload)
+ * - photo_{index}_answerName: Answer name (required)
+ * - photo_{index}_points: Points value (optional, defaults to 1)
+ * - photo_{index}_penaltyEnabled: "true" or "false" (optional)
+ * - photo_{index}_penaltyPoints: Penalty points (optional)
+ * - photo_{index}_maxAttempts: Maximum attempts (optional)
+ * - count: Number of photos being uploaded
+ *
+ * Related endpoints:
+ * - POST /api/photos/upload - Single photo upload (supports both file and URL)
+ */
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
import { sendNewPhotoEmail } from "@/lib/email"
+import { logger } from "@/lib/logger"
import { writeFile } from "fs/promises"
import { join } from "path"
import { existsSync, mkdirSync } from "fs"
import { createHash } from "crypto"
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
+
export async function POST(req: NextRequest) {
try {
const session = await auth()
@@ -204,7 +231,11 @@ export async function POST(req: NextRequest) {
photo.id,
photo.uploader.name
).catch((err) => {
- console.error("Failed to send email to:", user.email, "for photo:", photo.id, err)
+ logger.error("Failed to send email", {
+ email: user.email,
+ photoId: photo.id,
+ error: err instanceof Error ? err : new Error(String(err)),
+ })
})
)
)
@@ -216,7 +247,9 @@ export async function POST(req: NextRequest) {
{ status: 201 }
)
} catch (error) {
- console.error("Error uploading photos:", error)
+ logger.error("Error uploading photos", {
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/api/photos/upload/route.ts b/app/api/photos/upload/route.ts
index 87d8b48..2fdc006 100644
--- a/app/api/photos/upload/route.ts
+++ b/app/api/photos/upload/route.ts
@@ -3,11 +3,15 @@ import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
import { sendNewPhotoEmail } from "@/lib/email"
import { logActivity } from "@/lib/activity-log"
+import { logger } from "@/lib/logger"
import { writeFile } from "fs/promises"
import { join } from "path"
import { existsSync, mkdirSync } from "fs"
import { createHash } from "crypto"
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
+
export async function POST(req: NextRequest) {
try {
const session = await auth()
@@ -86,7 +90,8 @@ export async function POST(req: NextRequest) {
const uploadsDir = join(process.cwd(), "public", "uploads")
if (!existsSync(uploadsDir)) {
mkdirSync(uploadsDir, { recursive: true })
- console.log(`[UPLOAD] Created uploads directory: ${uploadsDir}`)
+ // DEBUG level: directory creation is normal operation
+ logger.debug("Created uploads directory", { path: uploadsDir })
}
console.log(`[UPLOAD] Using uploads directory: ${uploadsDir} (exists: ${existsSync(uploadsDir)})`)
@@ -98,9 +103,14 @@ export async function POST(req: NextRequest) {
const { access } = await import("fs/promises")
try {
await access(filepath)
- console.log(`[UPLOAD] File saved successfully: ${filepath}`)
+ // DEBUG level: file save verification is normal operation
+ logger.debug("File saved successfully", { filepath })
} catch (error) {
- console.error(`[UPLOAD] File write verification failed: ${filepath}`, error)
+ // ERROR level: file write failure is an error condition
+ logger.error("File write verification failed", {
+ filepath,
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
throw new Error("Failed to save file to disk")
}
@@ -166,7 +176,11 @@ export async function POST(req: NextRequest) {
allUsers.map((user: { id: string; email: string; name: string }) =>
sendNewPhotoEmail(user.email, user.name, photo.id, photo.uploader.name).catch(
(err) => {
- console.error("Failed to send email to:", user.email, err)
+ logger.error("Failed to send email", {
+ email: user.email,
+ photoId: photo.id,
+ error: err instanceof Error ? err : new Error(String(err)),
+ })
}
)
)
@@ -189,7 +203,9 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ photo }, { status: 201 })
} catch (error) {
- console.error("Error uploading photo:", error)
+ logger.error("Error uploading photo", {
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/api/profile/change-password/route.ts b/app/api/profile/change-password/route.ts
index 5eb75e3..753450f 100644
--- a/app/api/profile/change-password/route.ts
+++ b/app/api/profile/change-password/route.ts
@@ -1,9 +1,13 @@
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
+import { logger } from "@/lib/logger"
import bcrypt from "bcryptjs"
import { hashPassword } from "@/lib/utils"
+// Mark this route as dynamic to prevent build-time data collection
+export const dynamic = "force-dynamic"
+
export async function POST(req: NextRequest) {
try {
const session = await auth()
@@ -44,7 +48,9 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: true })
} catch (error) {
- console.error("Error changing password:", error)
+ logger.error("Error changing password", {
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/api/uploads/[filename]/route.ts b/app/api/uploads/[filename]/route.ts
index 7fec8a4..5133aa0 100644
--- a/app/api/uploads/[filename]/route.ts
+++ b/app/api/uploads/[filename]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"
+import { logger } from "@/lib/logger"
import { readFile } from "fs/promises"
import { join } from "path"
import { existsSync } from "fs"
@@ -7,8 +8,9 @@ export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ filename: string }> }
) {
+ let filename: string | undefined
try {
- const { filename } = await params
+ filename = (await params).filename
// Sanitize filename - only allow alphanumeric, dots, hyphens
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
@@ -26,7 +28,11 @@ export async function GET(
// Check if file exists
if (!existsSync(filepath)) {
- console.error(`[UPLOAD] File not found: ${filepath} (cwd: ${process.cwd()})`)
+ logger.warn("File not found", {
+ filepath,
+ filename,
+ cwd: process.cwd(),
+ })
return NextResponse.json({ error: "File not found" }, { status: 404 })
}
@@ -49,7 +55,10 @@ export async function GET(
},
})
} catch (error) {
- console.error("[UPLOAD] Error serving file:", error)
+ logger.error("Error serving file", {
+ filename: filename || "unknown",
+ error: error instanceof Error ? error : new Error(String(error)),
+ })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
diff --git a/app/layout.tsx b/app/layout.tsx
index acac771..bee8b53 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Providers from "@/components/Providers";
import Navigation from "@/components/Navigation";
+import HelpModal from "@/components/HelpModal";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -32,6 +33,7 @@ export default function RootLayout({
{children}
+