This MR fixes critical authentication issues that prevented login on localhost and improves the developer experience with consolidated rebuild scripts and a working help modal keyboard shortcut. #5

Merged
ilia merged 51 commits from dev into main 2026-01-05 19:42:46 -05:00
7 changed files with 484 additions and 13 deletions
Showing only changes of commit bc4a6b93b6 - Show all commits

57
REBUILD.md Normal file
View File

@ -0,0 +1,57 @@
# 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)
## 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
```

View File

@ -1,3 +1,4 @@
import { handlers } from "@/lib/auth"
// No wrapper needed - Auth.js now handles cookies correctly via useSecureCookies
export const { GET, POST } = handlers

View File

@ -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({
<Providers>
<Navigation />
<main className="min-h-screen bg-gray-50 overflow-x-hidden">{children}</main>
<HelpModal />
</Providers>
</body>
</html>

285
components/HelpModal.tsx Normal file
View File

@ -0,0 +1,285 @@
"use client"
import { useState, useEffect } from "react"
export default function HelpModal() {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Check for Shift+? (Shift+/ produces "?")
// Also check for event.key === "?" as fallback for some keyboard layouts
const isQuestionMark =
(event.shiftKey && event.key === "/") ||
event.key === "?" ||
(event.code === "Slash" && event.shiftKey)
// Only trigger if Shift is pressed (not Ctrl/Cmd)
if (event.shiftKey && isQuestionMark && !event.ctrlKey && !event.metaKey) {
event.preventDefault()
setIsOpen((prev) => !prev)
}
// Also close on Escape key
if (event.key === "Escape" && isOpen) {
setIsOpen(false)
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [isOpen])
if (!isOpen) return null
return (
<>
{/* Overlay */}
<div
className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
onClick={() => setIsOpen(false)}
>
{/* Modal */}
<div
className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-50"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="sticky top-0 bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-6 rounded-t-lg flex justify-between items-center">
<h2 className="text-2xl font-bold">Welcome to MirrorMatch</h2>
<button
onClick={() => setIsOpen(false)}
className="p-2 hover:bg-purple-700 rounded-md transition"
aria-label="Close help"
>
<svg
className="h-6 w-6"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-6 text-gray-700">
{/* What is MirrorMatch */}
<section>
<h3 className="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<svg
className="h-5 w-5 mr-2 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
What is MirrorMatch?
</h3>
<p className="text-gray-700 leading-relaxed">
MirrorMatch is a fun and engaging photo guessing game where you can upload photos
and challenge other players to guess who is in the picture. Test your knowledge of
friends, family, or colleagues while competing for points on the leaderboard!
</p>
</section>
{/* How to Play */}
<section>
<h3 className="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<svg
className="h-5 w-5 mr-2 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
How to Play
</h3>
<ol className="list-decimal list-inside space-y-2 text-gray-700">
<li>
<strong>Upload Photos:</strong> Go to the Upload page and add photos with answer
names. You can upload files or use image URLs.
</li>
<li>
<strong>Guess Photos:</strong> Browse the Photos page and click on any photo to
make your guess. Enter the name of the person you think is in the picture.
</li>
<li>
<strong>Earn Points:</strong> Get points for each correct guess! The more you
guess correctly, the higher you'll climb on the leaderboard.
</li>
<li>
<strong>Compete:</strong> Check the Leaderboard to see how you rank against other
players.
</li>
</ol>
</section>
{/* Features */}
<section>
<h3 className="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<svg
className="h-5 w-5 mr-2 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
Key Features
</h3>
<ul className="space-y-2 text-gray-700">
<li className="flex items-start">
<span className="text-purple-600 mr-2"></span>
<span>
<strong>Photo Upload:</strong> Share photos via file upload or URL
</span>
</li>
<li className="flex items-start">
<span className="text-purple-600 mr-2"></span>
<span>
<strong>Guessing System:</strong> Submit guesses and get instant feedback
</span>
</li>
<li className="flex items-start">
<span className="text-purple-600 mr-2"></span>
<span>
<strong>Email Notifications:</strong> Get notified when new photos are
uploaded
</span>
</li>
<li className="flex items-start">
<span className="text-purple-600 mr-2"></span>
<span>
<strong>Leaderboard:</strong> Track rankings and compete with others
</span>
</li>
<li className="flex items-start">
<span className="text-purple-600 mr-2"></span>
<span>
<strong>Profile Management:</strong> View your points and manage your account
</span>
</li>
</ul>
</section>
{/* Keyboard Shortcuts */}
<section>
<h3 className="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<svg
className="h-5 w-5 mr-2 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
Keyboard Shortcuts
</h3>
<div className="bg-gray-50 rounded-md p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="text-gray-700">
<kbd className="px-2 py-1 bg-white border border-gray-300 rounded text-sm font-mono">
Shift
</kbd>
{" + "}
<kbd className="px-2 py-1 bg-white border border-gray-300 rounded text-sm font-mono">
?
</kbd>
</span>
<span className="text-gray-600">Open/Close this help window</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-700">
<kbd className="px-2 py-1 bg-white border border-gray-300 rounded text-sm font-mono">
Esc
</kbd>
</span>
<span className="text-gray-600">Close help window</span>
</div>
</div>
</section>
{/* Tips */}
<section>
<h3 className="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<svg
className="h-5 w-5 mr-2 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
Tips
</h3>
<ul className="space-y-2 text-gray-700">
<li className="flex items-start">
<span className="text-purple-600 mr-2">💡</span>
<span>Guesses are case-insensitive, so don't worry about capitalization</span>
</li>
<li className="flex items-start">
<span className="text-purple-600 mr-2">💡</span>
<span>You can't guess your own photos, but you can still view them</span>
</li>
<li className="flex items-start">
<span className="text-purple-600 mr-2">💡</span>
<span>
You'll receive email notifications when other users upload new photos
</span>
</li>
<li className="flex items-start">
<span className="text-purple-600 mr-2">💡</span>
<span>Check the leaderboard regularly to see your ranking</span>
</li>
</ul>
</section>
</div>
{/* Footer */}
<div className="bg-gray-50 p-4 rounded-b-lg border-t border-gray-200">
<p className="text-sm text-gray-600 text-center">
Press <kbd className="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-mono">Esc</kbd> or click outside to close
</p>
</div>
</div>
</div>
</>
)
}

View File

@ -3,16 +3,41 @@ import Credentials from "next-auth/providers/credentials"
import { prisma } from "./prisma"
import bcrypt from "bcryptjs"
import { logger } from "./logger"
import { SESSION_COOKIE_NAME } from "./constants"
const nextAuthSecret = process.env.NEXTAUTH_SECRET
if (!nextAuthSecret) {
throw new Error("NEXTAUTH_SECRET is not set. Define it to enable authentication.")
}
// Determine if we should use secure cookies based on AUTH_URL/NEXTAUTH_URL
// Auth.js v5 derives this from the origin it detects, so we need to be explicit
const authUrl = process.env.AUTH_URL || process.env.NEXTAUTH_URL || "http://localhost:3000"
const isDev = process.env.NODE_ENV === "development"
const isHttp = authUrl.startsWith("http://")
// Explicitly control useSecureCookies - only true when URL is https://
// This prevents Auth.js from auto-detecting HTTPS and adding prefixes on HTTP
const useSecureCookies = !isHttp
// Log cookie configuration for debugging (only in development)
if (isDev) {
logger.debug("NextAuth cookie configuration", {
authUrl,
isDev,
isHttp,
useSecureCookies,
nodeEnv: process.env.NODE_ENV,
hasVercelEnv: !!process.env.VERCEL,
hasAuthTrustHost: !!process.env.AUTH_TRUST_HOST,
})
}
export const { handlers, auth, signIn, signOut } = NextAuth({
// trustHost must be true for NextAuth v5 to work, even on localhost
// We control HTTPS detection via cookie configuration instead
trustHost: true,
debug: process.env.NODE_ENV !== "production",
basePath: "/api/auth",
providers: [
Credentials({
name: "Credentials",
@ -115,16 +140,39 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
cookies: {
sessionToken: {
name: SESSION_COOKIE_NAME,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: true, // Always secure in production (HTTPS required)
},
},
},
// Explicitly configure cookies for HTTP (localhost)
// For HTTPS, let Auth.js defaults handle it (prefixes + Secure)
cookies: isHttp
? {
// localhost / pure HTTP: no prefixes, no Secure
sessionToken: {
name: "authjs.session-token",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: false,
},
},
csrfToken: {
name: "authjs.csrf-token",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: false,
},
},
callbackUrl: {
name: "authjs.callback-url",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: false,
},
},
}
: undefined, // Let Auth.js defaults handle HTTPS envs (prefixes + Secure)
secret: nextAuthSecret,
})

View File

@ -5,5 +5,6 @@
/**
* NextAuth session cookie name
* Must match the cookie name defined in lib/auth.ts
* For HTTP (localhost), no prefix. For HTTPS, Auth.js will add __Secure- prefix automatically.
*/
export const SESSION_COOKIE_NAME = "__Secure-authjs.session-token"
export const SESSION_COOKIE_NAME = "authjs.session-token"

77
rebuild.sh Executable file
View File

@ -0,0 +1,77 @@
#!/bin/bash
# Complete clean rebuild and start script for MirrorMatch
# Usage: ./rebuild.sh [dev|prod]
# dev - Development mode (hot reload, foreground)
# prod - Production mode (optimized, background with logging)
set -e
MODE=${1:-prod}
echo "========================================="
echo "MirrorMatch - Clean Rebuild & Start"
echo "Mode: ${MODE}"
echo "========================================="
# Step 1: Kill everything
echo ""
echo "Step 1: Killing all processes..."
sudo fuser -k 3000/tcp 2>/dev/null || true
killall -9 node 2>/dev/null || true
pkill -f "next" 2>/dev/null || true
sleep 2
echo "✓ All processes killed"
# Step 2: Free ports
echo ""
echo "Step 2: Freeing ports..."
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
lsof -ti:3003 | xargs kill -9 2>/dev/null || true
sleep 1
echo "✓ Ports freed"
# Step 3: Clean build artifacts
echo ""
echo "Step 3: Cleaning build artifacts..."
cd /home/beast/Code/mirrormatch
rm -rf .next node_modules/.cache .next/cache .next/dev/lock 2>/dev/null || true
echo "✓ Build artifacts cleaned"
# Step 4: Rebuild (only for production)
if [ "$MODE" = "prod" ]; then
echo ""
echo "Step 4: Rebuilding application..."
npm run build
echo "✓ Build complete"
fi
# Step 5: Start server
echo ""
echo "Step 5: Starting server..."
echo "========================================="
if [ "$MODE" = "dev" ]; then
echo "Development mode - logs will appear below:"
echo "Press Ctrl+C to stop"
echo "========================================="
echo ""
export NODE_ENV=development
unset AUTH_TRUST_HOST
npm run dev
else
echo "Production mode - server running in background"
echo "View logs: tail -f /tmp/mirrormatch-server.log"
echo "========================================="
echo ""
export NODE_ENV=production
unset AUTH_TRUST_HOST
npm run start > /tmp/mirrormatch-server.log 2>&1 &
echo "Server PID: $!"
echo ""
sleep 3
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -q "200\|307"; then
echo "✓ Server is running on http://localhost:3000"
else
echo "⚠ Server may still be starting. Check logs: tail -f /tmp/mirrormatch-server.log"
fi
fi