From ea38775a82e73eb0a4318b577e48bf4cc6db4a59 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Fri, 14 Mar 2025 19:33:13 +0000 Subject: [PATCH] fixed a bit of stuff the current but API is broken again for logging in so will be fixing that with the next push --- app/(auth)/login/login.module.css | 44 +++ app/(auth)/login/page.js | 163 ++++----- app/(main)/[teamId]/page.js | 94 ++---- app/(main)/clue/[clueId]/page.js | 94 +++--- app/(main)/leaderboard/page.js | 55 +++ app/(main)/map/map.module.css | 178 ++++++++++ app/(main)/map/page.js | 497 ++++++++++------------------ app/(main)/team/[teamId]/page.js | 39 ++- app/api/auth/[...nextauth]/route.js | 456 ++++++------------------- app/api/clues/[clueId]/route.js | 65 ++-- app/api/clues/route.js | 119 ++++--- app/api/hunt-status/route.js | 124 ++++--- app/api/hunt/start/route.js | 132 ++++++++ app/api/user/team/route.js | 94 ++++++ app/providers/auth-provider.js | 14 +- app/waiting/page.js | 87 +++++ clue_hunt.db | Bin 53248 -> 69632 bytes hooks/useHuntStatusCheck.js | 58 ++++ public/images/fallback-map.jpg | 1 + public/images/map.svg | 356 ++++++++++++++++++++ 20 files changed, 1643 insertions(+), 1027 deletions(-) create mode 100644 app/(auth)/login/login.module.css create mode 100644 app/(main)/leaderboard/page.js create mode 100644 app/(main)/map/map.module.css create mode 100644 app/api/hunt/start/route.js create mode 100644 app/api/user/team/route.js create mode 100644 app/waiting/page.js create mode 100644 hooks/useHuntStatusCheck.js create mode 100644 public/images/fallback-map.jpg create mode 100644 public/images/map.svg diff --git a/app/(auth)/login/login.module.css b/app/(auth)/login/login.module.css new file mode 100644 index 0000000..d1cf62e --- /dev/null +++ b/app/(auth)/login/login.module.css @@ -0,0 +1,44 @@ +/* Login page specific styles */ +.loginForm { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 8px; /* Reduced spacing (was 15px) */ +} + +.input-container { + margin-bottom: 10px; /* Add consistent spacing between input fields */ +} + +.errorMessage { + color: #e53935; + margin-top: 10px; + margin-bottom: 10px; /* Add space after error message */ + font-size: 0.875rem; +} + +.register-button-container { + margin-top: 5px; /* Reduced spacing (was 10px) */ +} + +.auth-button { + padding: 10px; + border: none; + border-radius: 4px; + background-color: #68543c; /* Changed back to dark brown */ + color: white; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.auth-button:hover { + background-color: #53402d; /* Darker brown on hover */ +} + +.auth-button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +/* You can also add other component-specific styles here */ diff --git a/app/(auth)/login/page.js b/app/(auth)/login/page.js index a03cb51..490b3c8 100644 --- a/app/(auth)/login/page.js +++ b/app/(auth)/login/page.js @@ -1,4 +1,5 @@ "use client"; + import { useState } from "react"; import { signIn } from "next-auth/react"; import { useRouter } from "next/navigation"; @@ -19,84 +20,39 @@ export default function Login() { try { console.log("Attempting to sign in with:", username); - // First try using the NextAuth signIn method - try { - const result = await signIn("credentials", { - redirect: false, - username, - password, - callbackUrl: "/team-selection", - }); + const result = await signIn("credentials", { + redirect: false, + username, + password, + callbackUrl: `${window.location.origin}/team-selection`, + }); - if (result?.error) { - throw new Error(result.error); - } + console.log("Sign in result:", result); - // If the user has a team, redirect directly to the team page - if (result?.url) { - // Extract the teamId from the URL or session data if available - // This part depends on how your auth system returns the result - router.push(`/${session?.user?.teamId || ""}`); - } else { - // Otherwise go to team selection - router.push("/team-selection"); - } - return; - } catch (signInError) { - console.error("Error with signIn method:", signInError); - console.log("Falling back to direct API call"); - - // Fall back to direct fetch approach with explicit JSON content-type - const loginResponse = await fetch("/api/auth/callback/credentials", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username, - password, - callbackUrl: "/team-selection", - }), - credentials: "include", // Important for cookie handling - }); - - if (!loginResponse.ok) { - const errorData = await loginResponse - .json() - .catch(() => ({ error: "Failed to parse error response" })); - throw new Error( - errorData.error || - `Login failed with status ${loginResponse.status}` - ); - } - - // On successful login - const userData = await loginResponse - .json() - .catch(() => ({ url: "/team-selection" })); - - // Allow time for cookie to be set - setTimeout(() => { - router.push(userData.url || "/team-selection"); - }, 100); + if (result?.error) { + throw new Error(result.error); } - } catch (err) { - setError(err.message || "An unexpected error occurred"); - console.error("Login error:", err); + + if (result?.ok) { + console.log("Sign in successful, redirecting..."); + // Use router.push with { replace: true } to avoid history issues + router.push("/team-selection", { replace: true }); + return; + } + } catch (error) { + console.error("Login error:", error); + setError(error.message || "Failed to login. Please try again."); + } finally { setIsLoading(false); } }; return (
-
-
Login
-
-
-
-
+

Login to Treasure Hunt

+
{error &&
{error}
} - - - + + {/* Add separate div for register button with margin-top */} +
+ + + +
+ {/* Update styles to include spacing between buttons */}
diff --git a/app/(main)/[teamId]/page.js b/app/(main)/[teamId]/page.js index 05da71a..c9672ff 100644 --- a/app/(main)/[teamId]/page.js +++ b/app/(main)/[teamId]/page.js @@ -194,8 +194,6 @@ export default function TeamPage({ params }) { ) : (
No team members found
)} - - {/* Remove the add member button - it was here before */}
@@ -243,61 +241,31 @@ export default function TeamPage({ params }) { try { setLoading(true); - if (!huntActive) { - // If hunt is not active, redirect to the "not started" page + // Use the dedicated start hunt API endpoint + const response = await fetch("/api/hunt/start", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + credentials: "include", + }); + + const data = await response.json(); + + if (data.success && data.active) { + // Redirect to map page using window.location for a clean reload + window.location.href = data.redirectUrl || "/map"; + } else if (!data.active) { + // Hunt is not active router.push(`/hunt-not-started?teamId=${teamId}`); } else { - // If hunt is active, try to start the hunt - const response = await fetch( - `/api/teams/${teamId}/start-hunt`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - } - ); - - if (response.ok) { - fetchTeamData(); - } else { - const error = await response.json(); - throw new Error(error.message || "Failed to start hunt"); - } + // Some other error + throw new Error(data.error || "Failed to start hunt journey"); } } catch (err) { - setError(err.message); - setLoading(false); - } - }} - > - Start Hunt - - )} - - {/* Keep the admin start button which directly starts the hunt */} - {session?.user?.role === "admin" && ( -
); } if (error) { return ( -
-
Error: {error}
- +
+

Error

+

{error}

+
); } - if (clues.length === 0) { + if (mapError) { return ( -
-
No clues available yet.
- +
+

Error Loading Map

+

We couldn't load the treasure map. Please try again.

+
); } return ( -
-

Treasure Map

-
- + {/* Map Banner with team info and timer */} +
+
+ {teamData?.team?.name || "Your Team"} +
+
+ {elapsedTime} +
+
{teamData?.team?.code || ""}
+
+ + {/* Main map content area */} +
+
+ {/* Use Image component with fill layout for better SVG handling */} + Treasure Map { + // Fallback to a static image if SVG fails to load + setMapError(true); + }} + /> +
+
+ + {/* Navigation bar - now sticky */} +
- - {/* Clue list */} -
-

Clues

-
    - {cluePositions.map((clue, index) => ( -
  • - - {index + 1}. {clue.title} - -
  • - ))} -
-
- - - - +
🏠
+
Home
+ + +
πŸ“Š
+
Stats
+ + +
πŸ‘₯
+
Team
+ + +
πŸ—ΊοΈ
+
Map
+ +
); } diff --git a/app/(main)/team/[teamId]/page.js b/app/(main)/team/[teamId]/page.js index 424459d..bd77353 100644 --- a/app/(main)/team/[teamId]/page.js +++ b/app/(main)/team/[teamId]/page.js @@ -234,27 +234,39 @@ export default function TeamPage({ params }) { try { setLoading(true); - // Check hunt status first + // Use try-catch with robust error handling const response = await fetch("/api/hunt-status", { method: "GET", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, credentials: "include", }); - if (response.ok) { - const data = await response.json(); - if (data.active) { - // If hunt is active, navigate to map page - router.push("/map"); - } else { - // If hunt is not active, show not started page - router.push(`/hunt-not-started?teamId=${teamId}`); - } + if (!response.ok) { + const errorText = await response.text(); + console.error("Hunt status error response:", errorText); + throw new Error( + `Failed to check hunt status: ${response.status}` + ); + } + + const data = await response.json(); + console.log("Hunt status response:", data); + + // If hunt is active, go directly to the map page using window.location + if (data.active) { + // Use window.location.href for a full page reload to avoid caching issues + window.location.href = "/map"; } else { - throw new Error("Failed to check hunt status"); + // If hunt is not active, show the not started page + router.push(`/hunt-not-started?teamId=${teamId}`); } } catch (err) { - setError(err.message); + console.error("Error checking hunt status:", err); + setError(`Failed to check hunt status: ${err.message}`); + } finally { setLoading(false); } }} @@ -483,6 +495,7 @@ export default function TeamPage({ params }) { .error-icon { font-size: 48px; margin-bottom: 20px; + color: #d32f2f; } .error-message { diff --git a/app/api/auth/[...nextauth]/route.js b/app/api/auth/[...nextauth]/route.js index e534075..6064fea 100644 --- a/app/api/auth/[...nextauth]/route.js +++ b/app/api/auth/[...nextauth]/route.js @@ -1,378 +1,83 @@ -export const runtime = "nodejs"; - +import NextAuth from "next-auth"; // <-- Changed from "next-auth/next" +import CredentialsProvider from "next-auth/providers/credentials"; import { validateCredentials } from "@/actions/auth"; +import jwt from "jsonwebtoken"; import { cookies } from "next/headers"; -import { redirect } from "next/navigation"; -// Define hardcoded values for NextAuth -const NEXTAUTH_SECRET = "banana"; -const NEXTAUTH_URL = "http://localhost:3000"; +// Use a consistent secret for JWT signing and verification +const JWT_SECRET = process.env.NEXTAUTH_SECRET || "banana"; +console.log("Using NextAuth Secret:", JWT_SECRET); -// Use hardcoded values instead of environment variables -console.log("Using NextAuth Secret: banana"); -console.log(`Using NextAuth URL: ${NEXTAUTH_URL}`); - -// Function to create a signed token -function createToken(payload) { - const encodedData = Buffer.from(JSON.stringify(payload)).toString("base64"); - const signature = Buffer.from(`${encodedData}.${NEXTAUTH_SECRET}`).toString( - "base64" - ); - return `${encodedData}.${signature}`; -} - -// Function to verify a token -function verifyToken(token) { - try { - if (!token) return null; - - const [encodedData, signature] = token.split("."); - const expectedSignature = Buffer.from( - `${encodedData}.${NEXTAUTH_SECRET}` - ).toString("base64"); - - if (signature !== expectedSignature) return null; - - const data = JSON.parse( - Buffer.from(encodedData, "base64").toString("utf-8") - ); - - // Check if token is expired - if (data.exp && data.exp < Math.floor(Date.now() / 1000)) return null; - - return data; - } catch (error) { - console.error("Token verification error:", error); - return null; - } -} - -// Create the handler functions directly without calling NextAuth function -export async function GET(request) { - try { - // Safely parse URL with error handling - let pathname = "/"; - let searchParams = new URLSearchParams(); - - try { - if (request && request.url) { - const url = new URL(request.url); - pathname = url.pathname; - searchParams = url.searchParams; - } else { - console.warn("Request or request.url is undefined"); - } - } catch (urlError) { - console.error("Failed to parse URL:", urlError); - pathname = request.nextUrl?.pathname || "/api/auth/session"; - } - - console.log("Auth GET request path:", pathname); - - // Handle session requests - if (pathname.includes("/session")) { - try { - const cookieStore = await cookies(); - const sessionToken = cookieStore.get("next-auth.session-token")?.value; - - if (!sessionToken) { - return Response.json({ user: null }); - } - - const userData = verifyToken(sessionToken); - - return Response.json({ - user: userData - ? { - id: userData.id, - name: userData.username, - email: `${userData.username}@example.com`, // Next-auth expects an email - image: null, - role: userData.role, - teamId: userData.teamId, - } - : null, - }); - } catch (cookieError) { - console.error("Error accessing cookies:", cookieError); - return Response.json({ user: null }); - } - } - - // For CSRF token requests - IMPORTANT: Return JSON, don't redirect - if (pathname.includes("/csrf")) { - const csrfToken = Buffer.from( - `csrf-${Date.now()}-${NEXTAUTH_SECRET}` - ).toString("base64"); - return new Response(JSON.stringify({ csrfToken }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } - - // Check if this is a signin request - if (pathname.includes("/signin")) { - // Redirect to the login page with callbackUrl - const callbackUrl = searchParams.get("callbackUrl") || "/"; - return Response.redirect( - `${NEXTAUTH_URL}/login?callbackUrl=${encodeURIComponent(callbackUrl)}` - ); - } - - // Handle providers request - if (pathname.includes("/providers")) { - return Response.json({ - credentials: { - id: "credentials", - name: "Credentials", - type: "credentials", - signinUrl: `${NEXTAUTH_URL}/api/auth/signin/credentials`, - callbackUrl: `${NEXTAUTH_URL}/api/auth/callback/credentials`, - }, - }); - } - - // Default: redirect to login - return Response.redirect(`${NEXTAUTH_URL}/login`); - } catch (error) { - console.error("Auth GET error:", error); - return Response.json({ error: "Internal server error" }, { status: 500 }); - } -} - -export async function POST(request) { - try { - // Debug the actual URL value before trying to parse it - console.log("Raw request URL:", request.url); - - // More robust URL parsing - let pathname = ""; - let requestUrl = null; - - try { - // Add protocol and host if the URL doesn't have them - const urlString = request.url; - - // Only try to parse if we have a string - if (typeof urlString === "string" && urlString) { - // Check if URL has proper protocol - const hasProtocol = - urlString.startsWith("http://") || urlString.startsWith("https://"); - - if (hasProtocol) { - requestUrl = new URL(urlString); - } else { - // Add protocol and host to handle relative URLs - requestUrl = new URL(urlString, "http://localhost:3000"); - } - pathname = requestUrl.pathname; - console.log("Successfully parsed URL:", pathname); - } else { - console.log("URL is not valid, using fallback methods"); - } - } catch (urlError) { - console.error("Invalid URL in request:", urlError); - } - - // Use nextUrl as fallback if available - if (!pathname && request.nextUrl?.pathname) { - pathname = request.nextUrl.pathname; - console.log("Using nextUrl pathname:", pathname); - } - - const contentType = request.headers.get("content-type") || ""; - console.log("Content-Type:", contentType); - - // Check for callback/credentials without relying on URL parsing - const isCallbackRequest = - pathname.includes("/callback/credentials") || - (request.url && request.url.includes("/callback/credentials")); - - if (isCallbackRequest) { - // Extract credentials from the appropriate source based on content type - let username, password, callbackUrl; - - if (contentType.includes("application/json")) { - // Handle JSON request - const jsonData = await request.json(); - username = jsonData.username; - password = jsonData.password; - callbackUrl = jsonData.callbackUrl || "/team-selection"; - console.log("Parsed JSON credentials from callback request"); - } else if ( - contentType.includes("application/x-www-form-urlencoded") || - contentType.includes("multipart/form-data") - ) { - // Handle form data - const formData = await request.formData(); - username = formData.get("username"); - password = formData.get("password"); - callbackUrl = formData.get("callbackUrl") || "/team-selection"; - console.log("Parsed form credentials from callback request"); - } else { - return Response.json( - { error: "Unsupported content type for authentication" }, - { status: 400 } - ); - } - - console.log(`Credentials callback for user: ${username || "(unknown)"}`); - - // Validate credentials - const user = await validateCredentials(username, password); - - if (!user) { - return Response.json({ error: "Invalid credentials" }, { status: 401 }); - } - - // Create token with user data - const token = { - id: user.id, - username: user.username, - role: user.role, - teamId: user.teamId, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days - }; - - // Create JWT-like token - const jwtToken = createToken(token); - - // Calculate expiration date for cookie - const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() + 30); - - // Return success with cookie and redirect URL - return new Response(JSON.stringify({ url: callbackUrl }), { - status: 200, - headers: { - "Content-Type": "application/json", - "Set-Cookie": `next-auth.session-token=${jwtToken}; Path=/; HttpOnly; SameSite=Lax; Expires=${expiryDate.toUTCString()}`, - }, - }); - } - - // Handle regular form submission - let username, password, callbackUrl; - - // Handle form data (application/x-www-form-urlencoded) - if (contentType.includes("application/x-www-form-urlencoded")) { - // Clone request to read the body - const clonedRequest = request.clone(); - const formText = await clonedRequest.text(); - console.log("Form data:", formText); - - // Parse form data manually - const params = new URLSearchParams(formText); - username = params.get("username"); - password = params.get("password"); - callbackUrl = params.get("callbackUrl") || "/team-selection"; - - console.log("Parsed credentials:", { - username, - password: password ? "[REDACTED]" : undefined, - }); - } - // Handle JSON - else if (contentType.includes("application/json")) { - const jsonData = await request.json(); - username = jsonData.username; - password = jsonData.password; - callbackUrl = jsonData.callbackUrl || "/team-selection"; - } - // Try form data as a last resort - else { - try { - const formData = await request.formData(); - username = formData.get("username"); - password = formData.get("password"); - callbackUrl = formData.get("callbackUrl") || "/team-selection"; - } catch (formError) { - console.error("Form data parsing error:", formError); - // Instead of returning an error, log and continue - the request might be something else - } - } - - if (!username || !password) { - // If no credentials were found, this might be a different kind of request - // Log it and return an appropriate error - console.error("Missing credentials in request"); - return Response.json( - { error: "Username and password required" }, - { status: 400 } - ); - } - - // Validate credentials - const user = await validateCredentials(username, password); - - if (!user) { - return Response.json({ error: "Invalid credentials" }, { status: 401 }); - } - - // Create token with user data - const token = { - id: user.id, - username: user.username, - role: user.role, - teamId: user.teamId, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days - }; - - // Create JWT-like token - const jwtToken = createToken(token); - - // Calculate expiration date for cookie - const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() + 30); - - // Return success with cookie and redirect URL - return new Response( - JSON.stringify({ - ok: true, - url: callbackUrl, - }), - { - status: 200, - headers: { - "Content-Type": "application/json", - "Set-Cookie": `next-auth.session-token=${jwtToken}; Path=/; HttpOnly; SameSite=Lax; Expires=${expiryDate.toUTCString()}`, - }, - } - ); - } catch (error) { - console.error("Auth POST error:", error); - return Response.json({ error: "Internal server error" }, { status: 500 }); - } -} - -// Export authOptions for compatibility with other code using getServerSession export const authOptions = { providers: [ + // Correct syntax: no function call, just reference the imported object { id: "credentials", name: "Credentials", type: "credentials", - authorize: async (credentials) => { - if (!credentials?.username || !credentials?.password) return null; - return await validateCredentials( - credentials.username, - credentials.password - ); + credentials: { + username: { label: "Username", type: "text" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + try { + if (!credentials?.username || !credentials?.password) { + console.log("Missing credentials"); + return null; + } + + // Validate credentials against our database + const user = await validateCredentials( + credentials.username, + credentials.password + ); + + if (user) { + console.log("User authenticated:", user.username); + return user; + } + + console.log("Invalid credentials for:", credentials.username); + return null; + } catch (error) { + console.error("Authentication error:", error); + return null; + } }, }, ], + + session: { + strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + + jwt: { + secret: JWT_SECRET, + encode: async ({ secret, token }) => { + return jwt.sign(token, secret); + }, + decode: async ({ secret, token }) => { + try { + return jwt.verify(token, secret); + } catch (error) { + console.error("JWT decode error:", error); + return null; + } + }, + }, + callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; token.username = user.username; - token.teamId = user.teamId; + token.teamId = user.team_id || user.teamId; } return token; }, + async session({ session, token }) { if (token) { session.user = session.user || {}; @@ -384,9 +89,46 @@ export const authOptions = { return session; }, }, - session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60 }, - secret: NEXTAUTH_SECRET, + + pages: { + signIn: "/login", + error: "/login?error=true", + }, + + // Add debugging to help identify issues with API responses + debug: process.env.NODE_ENV === "development", + + // Add logger to catch issues + logger: { + error(code, metadata) { + console.error(`NextAuth error: ${code}`, metadata); + }, + warn(code) { + console.warn(`NextAuth warning: ${code}`); + }, + debug(code, metadata) { + console.log(`NextAuth debug: ${code}`, metadata); + }, + }, }; -// Export the token functions directly for use in other files -export { createToken, verifyToken }; +// Token helpers +export function createToken(payload) { + return jwt.sign(payload, JWT_SECRET, { expiresIn: "30d" }); +} + +export function verifyToken(token) { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + console.error("Token verification error:", error); + return null; + } +} + +// Set session runtime to nodejs (not edge for better compatibility) +export const runtime = "nodejs"; + +// Create the handler with correct NextAuth instantiation +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; diff --git a/app/api/clues/[clueId]/route.js b/app/api/clues/[clueId]/route.js index 980f059..6538cbc 100644 --- a/app/api/clues/[clueId]/route.js +++ b/app/api/clues/[clueId]/route.js @@ -9,38 +9,41 @@ export async function GET(request, { params }) { const { clueId } = params; try { - // Verify the user is authenticated - const cookieStore = await cookies(); - const sessionToken = cookieStore.get("next-auth.session-token")?.value; - - if (!sessionToken) { - return Response.json( - { error: "Unauthorized", clue: null }, - { status: 401 } - ); - } - - const userData = verifyToken(sessionToken); - if (!userData) { - return Response.json( - { error: "Invalid session", clue: null }, - { status: 401 } - ); - } - - // Check if the hunt is active + // Check if the hunt is active first const huntActiveSetting = await queryOne( "SELECT value FROM settings WHERE key = 'hunt_active'" ); - const huntActive = huntActiveSetting && huntActiveSetting.value === "true"; + // Only verify authentication when hunt is not active if (!huntActive) { - // Return a valid JSON response with clue set to null - return Response.json({ - error: "Hunt is not active", - clue: null, - }); + const cookieStore = await cookies(); + const sessionToken = + cookieStore.get("next-auth.session-token")?.value || + cookieStore.get("__Secure-next-auth.session-token")?.value; + + if (!sessionToken) { + return Response.json( + { error: "Unauthorized", clue: null }, + { status: 401, headers: { "Content-Type": "application/json" } } + ); + } + + const userData = verifyToken(sessionToken); + if (!userData) { + return Response.json( + { error: "Invalid session", clue: null }, + { status: 401, headers: { "Content-Type": "application/json" } } + ); + } + + // Admin can see clues even if hunt is not active + if (userData.role !== "admin") { + return Response.json( + { error: "Hunt is not active", clue: null }, + { headers: { "Content-Type": "application/json" } } + ); + } } // Fetch the specific clue @@ -49,17 +52,19 @@ export async function GET(request, { params }) { if (!clue) { return Response.json( { error: "Clue not found", clue: null }, - { status: 404 } + { status: 404, headers: { "Content-Type": "application/json" } } ); } - return Response.json({ clue }); + return Response.json( + { clue }, + { headers: { "Content-Type": "application/json" } } + ); } catch (error) { console.error("Error fetching clue details:", error); - // Always return a valid JSON response even on error return Response.json( { error: "Failed to fetch clue details: " + error.message, clue: null }, - { status: 500 } + { status: 500, headers: { "Content-Type": "application/json" } } ); } } diff --git a/app/api/clues/route.js b/app/api/clues/route.js index 23759c3..5b3d7c0 100644 --- a/app/api/clues/route.js +++ b/app/api/clues/route.js @@ -4,21 +4,18 @@ import { verifyToken } from "@/app/api/auth/[...nextauth]/route"; export const dynamic = "force-dynamic"; // Disable caching -// Helper function to ensure the clues table exists (if not already created elsewhere) +// Helper function to ensure the clues table exists async function ensureCluesTableExists() { try { - // Create the clues table if it doesn't exist await run(` CREATE TABLE IF NOT EXISTS clues ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT NOT NULL, location TEXT, - order_number INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); - return true; } catch (error) { console.error("Error ensuring clues table exists:", error); @@ -26,79 +23,105 @@ async function ensureCluesTableExists() { } } +// Helper function to extract numeric value from clue title +function extractClueNumber(title) { + // Look for patterns like "Clue 1", "Clue 10", etc. + const match = title.match(/Clue\s+(\d+)/i); + // If we find a match, return the number, otherwise return Infinity (to sort to the end) + return match ? parseInt(match[1], 10) : Infinity; +} + // GET endpoint to retrieve all clues export async function GET(request) { try { - // Verify the user is authenticated - const cookieStore = await cookies(); - const sessionToken = cookieStore.get("next-auth.session-token")?.value; + // First check if the hunt is active + const huntActiveSetting = await queryOne( + "SELECT value FROM settings WHERE key = 'hunt_active'" + ); + const huntActive = huntActiveSetting && huntActiveSetting.value === "true"; - if (!sessionToken) { - return Response.json( - { error: "Unauthorized", clues: [] }, - { status: 401 } - ); - } + // If hunt is active, allow access without authentication + if (!huntActive) { + // Only verify authentication when hunt is not active + const cookieStore = await cookies(); + const sessionToken = + cookieStore.get("next-auth.session-token")?.value || + cookieStore.get("__Secure-next-auth.session-token")?.value; - const userData = verifyToken(sessionToken); - if (!userData) { - return Response.json( - { error: "Invalid session", clues: [] }, - { status: 401 } - ); + if (!sessionToken) { + return Response.json( + { error: "Unauthorized", clues: [] }, + { status: 401, headers: { "Content-Type": "application/json" } } + ); + } + + const userData = verifyToken(sessionToken); + if (!userData) { + return Response.json( + { error: "Invalid session", clues: [] }, + { status: 401, headers: { "Content-Type": "application/json" } } + ); + } + + // Admin can see clues even if hunt is not active + if (userData.role !== "admin") { + return Response.json( + { error: "Hunt is not active", clues: [] }, + { headers: { "Content-Type": "application/json" } } + ); + } } // Ensure the clues table exists await ensureCluesTableExists(); - // Check if the hunt is active - const huntActiveSetting = await queryOne( - "SELECT value FROM settings WHERE key = 'hunt_active'" - ); - - const huntActive = huntActiveSetting && huntActiveSetting.value === "true"; - - if (!huntActive) { - // Return empty clues array instead of error status to avoid redirect issues - return Response.json({ - error: "Hunt is not active", - clues: [], - }); - } - - // Fetch all clues - const clues = await query("SELECT * FROM clues ORDER BY order_number ASC"); + // Fetch all clues without sorting in SQL + const clues = await query("SELECT * FROM clues"); // If no clues are found, insert sample clues (for testing purposes) if (!clues || clues.length === 0) { // Insert sample clues await run( - "INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)", - ["First Clue", "This is the first clue description", "Location 1", 1] + "INSERT INTO clues (title, description, location) VALUES (?, ?, ?)", + ["Clue 1", "This is the first clue description", "Location 1"] ); await run( - "INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)", - ["Second Clue", "This is the second clue description", "Location 2", 2] + "INSERT INTO clues (title, description, location) VALUES (?, ?, ?)", + ["Clue 2", "This is the second clue description", "Location 2"] ); await run( - "INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)", - ["Final Clue", "This is the final clue description", "Location 3", 3] + "INSERT INTO clues (title, description, location) VALUES (?, ?, ?)", + ["Clue 3", "This is the final clue description", "Location 3"] ); // Fetch the inserted clues - const sampleClues = await query( - "SELECT * FROM clues ORDER BY order_number ASC" + const sampleClues = await query("SELECT * FROM clues"); + + // Sort clues based on numbering in title + sampleClues.sort( + (a, b) => extractClueNumber(a.title) - extractClueNumber(b.title) + ); + + return Response.json( + { clues: sampleClues }, + { headers: { "Content-Type": "application/json" } } ); - return Response.json({ clues: sampleClues }); } - return Response.json({ clues }); + // Sort clues based on numbering in title + clues.sort( + (a, b) => extractClueNumber(a.title) - extractClueNumber(b.title) + ); + + return Response.json( + { clues }, + { headers: { "Content-Type": "application/json" } } + ); } catch (error) { console.error("Error fetching clues:", error); - // Always return a valid JSON response even on error return Response.json( { error: "Failed to fetch clues: " + error.message, clues: [] }, - { status: 500 } + { status: 500, headers: { "Content-Type": "application/json" } } ); } } diff --git a/app/api/hunt-status/route.js b/app/api/hunt-status/route.js index 29ea176..a014e7d 100644 --- a/app/api/hunt-status/route.js +++ b/app/api/hunt-status/route.js @@ -4,59 +4,99 @@ import { verifyToken } from "@/app/api/auth/[...nextauth]/route"; export const dynamic = "force-dynamic"; // Disable caching +// Helper function to ensure the hunt_status table exists +async function ensureHuntStatusTableExists() { + try { + // Create hunt_status table if it doesn't exist + await run(` + CREATE TABLE IF NOT EXISTS hunt_status ( + id INTEGER PRIMARY KEY CHECK (id = 1), + is_active BOOLEAN DEFAULT 0, + start_time TEXT, + end_time TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Check if we need to insert the initial record + const status = await queryOne("SELECT * FROM hunt_status LIMIT 1"); + if (!status) { + await run( + "INSERT INTO hunt_status (id, is_active, start_time) VALUES (1, 0, NULL)" + ); + console.log("Initialized hunt_status table with default record"); + } + return true; + } catch (error) { + console.error("Error ensuring hunt_status table:", error); + return false; + } +} + +// GET endpoint to check if the hunt is active export async function GET(request) { try { - // Verify the user is authenticated + // First ensure the table exists + await ensureHuntStatusTableExists(); + + // Get the token from cookies - properly awaited const cookieStore = await cookies(); const sessionToken = cookieStore.get("next-auth.session-token")?.value; - if (!sessionToken) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); + // Check hunt status even if user is not authenticated + // to prevent auth errors from interfering with status checks + + // First check the hunt_status table for direct status + const huntStatus = await queryOne( + "SELECT is_active, start_time FROM hunt_status LIMIT 1" + ).catch((err) => { + console.warn("Error querying hunt_status:", err.message); + return null; + }); + + // Then check the settings table as a backup + const huntActiveSetting = await queryOne( + "SELECT value FROM settings WHERE key = 'hunt_active'" + ).catch((err) => { + console.warn("Error querying settings:", err.message); + return null; + }); + + // Also check the hunts table if it exists (for backward compatibility) + const oldHuntRecord = await queryOne( + "SELECT active, start_time FROM hunts ORDER BY id DESC LIMIT 1" + ).catch((err) => { + // Ignore errors if table doesn't exist + return null; + }); + + // Determine active status from all available sources + let isActive = false; + let startTime = null; + + // First priority: hunt_status.is_active + if (huntStatus && huntStatus.is_active !== null) { + isActive = Boolean(huntStatus.is_active); + startTime = huntStatus.start_time; + } + // Second priority: settings table + else if (huntActiveSetting) { + isActive = huntActiveSetting.value === "true"; + } + // Third priority: old hunts table + else if (oldHuntRecord) { + isActive = Boolean(oldHuntRecord.active); + startTime = oldHuntRecord.start_time; } - const userData = verifyToken(sessionToken); - if (!userData) { - return Response.json({ error: "Invalid session" }, { status: 401 }); - } + console.log("Hunt status check:", { isActive, startTime }); - let huntActive = false; - - try { - // First try to create the settings table if it doesn't exist - await run(` - CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ) - `); - - // Check if hunt_active setting exists - const huntSetting = await queryOne( - "SELECT value FROM settings WHERE key = 'hunt_active'" - ); - - // If the setting exists, use its value - if (huntSetting) { - huntActive = huntSetting.value === "true"; - } else { - // If setting doesn't exist, create it with default value "false" - await run( - "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", - ["hunt_active", "false"] - ); - } - } catch (dbError) { - console.error("Database error in hunt-status:", dbError); - // Continue with default value if there's a DB error - } - - // Return the active status return Response.json({ - active: huntActive, - timestamp: new Date().toISOString(), + active: isActive, + startTime: startTime || null, }); } catch (error) { - console.error("Hunt status check error:", error); + console.error("Error checking hunt status:", error); return Response.json( { error: "Failed to check hunt status", active: false }, { status: 500 } diff --git a/app/api/hunt/start/route.js b/app/api/hunt/start/route.js new file mode 100644 index 0000000..07398b9 --- /dev/null +++ b/app/api/hunt/start/route.js @@ -0,0 +1,132 @@ +import { getServerSession } from "next-auth/next"; +import { authOptions, verifyToken } from "../../auth/[...nextauth]/route"; +import { query, queryOne, run } from "../../../../lib/db"; +import { cookies } from "next/headers"; + +export const dynamic = "force-dynamic"; // Disable caching + +// Helper function to ensure the hunt_status table exists +async function ensureHuntStatusTableExists() { + try { + // Create hunt_status table if it doesn't exist + await run(` + CREATE TABLE IF NOT EXISTS hunt_status ( + id INTEGER PRIMARY KEY CHECK (id = 1), + is_active BOOLEAN DEFAULT 0, + start_time TEXT, + end_time TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Check if we need to insert the initial record + const status = await queryOne("SELECT * FROM hunt_status LIMIT 1"); + if (!status) { + await run( + "INSERT INTO hunt_status (id, is_active, start_time) VALUES (1, 0, NULL)" + ); + } + return true; + } catch (error) { + console.error("Error ensuring hunt_status table:", error); + return false; + } +} + +// Fallback function to get user data when session fails +async function getUserFromToken() { + try { + const cookieStore = await cookies(); + const sessionToken = cookieStore.get("next-auth.session-token")?.value; + + if (!sessionToken) return null; + + const userData = verifyToken(sessionToken); + if (!userData) return null; + + // Verify that the user exists and get their role + const user = await queryOne("SELECT id, role FROM users WHERE id = ?", [ + userData.id || userData.sub, + ]); + + return user; + } catch (error) { + console.error("Error getting user from token:", error); + return null; + } +} + +export async function POST(request) { + try { + // First ensure the table exists + await ensureHuntStatusTableExists(); + + // Try to get session, with fallback to token-based auth + let user = null; + let session = null; + + try { + session = await getServerSession(authOptions); + user = session?.user; + } catch (sessionError) { + console.warn( + "Session error, trying fallback auth:", + sessionError.message + ); + user = await getUserFromToken(); + } + + // Verify the user is authenticated + if (!user) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Check if user is admin + if (user.role !== "admin") { + return Response.json( + { error: "Only admins can start the hunt" }, + { status: 403 } + ); + } + + // Check if hunt is already in progress + const huntStatus = await queryOne("SELECT * FROM hunt_status LIMIT 1"); + + if (huntStatus && huntStatus.is_active) { + return Response.json( + { error: "Hunt is already in progress" }, + { status: 400 } + ); + } + + // Start the hunt by updating the status + const now = new Date().toISOString(); + + // Update the hunt_status table + await run( + `UPDATE hunt_status SET is_active = ?, start_time = ? WHERE id = 1`, + [true, now] + ); + + // Also update settings table for compatibility + try { + await run("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [ + "hunt_active", + "true", + ]); + } catch (err) { + console.log( + "Could not update settings table (might not exist yet):", + err.message + ); + } + + return Response.json( + { success: true, message: "Hunt started successfully", startTime: now }, + { status: 200 } + ); + } catch (error) { + console.error("Start hunt error:", error); + return Response.json({ error: "Failed to start hunt" }, { status: 500 }); + } +} diff --git a/app/api/user/team/route.js b/app/api/user/team/route.js new file mode 100644 index 0000000..5dab45b --- /dev/null +++ b/app/api/user/team/route.js @@ -0,0 +1,94 @@ +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../../auth/[...nextauth]/route"; +import { query, queryOne } from "../../../../lib/db"; + +export async function GET(request) { + try { + // Get the user's session with better error handling + let session; + try { + session = await getServerSession(authOptions); + } catch (sessionError) { + console.error("Session fetch error:", sessionError); + return Response.json( + { error: "Authentication error", details: sessionError.message }, + { status: 401 } + ); + } + + // Check if user is authenticated + if (!session || !session.user) { + return Response.json({ error: "You must be logged in" }, { status: 401 }); + } + + // Check if user is part of a team + if (!session.user.teamId) { + return Response.json( + { error: "You are not part of a team" }, + { status: 404 } + ); + } + + // Fetch team data + const team = await queryOne("SELECT * FROM teams WHERE id = ?", [ + session.user.teamId, + ]); + + if (!team) { + return Response.json({ error: "Team not found" }, { status: 404 }); + } + + // Fetch team members + const members = await query( + "SELECT id, username FROM users WHERE team_id = ? ORDER BY username ASC", + [session.user.teamId] + ); + + // Fetch hunt status to provide start time - safely check multiple places + let huntStartTime = null; + + try { + // Try hunt_status table first + const huntStatus = await queryOne("SELECT * FROM hunt_status LIMIT 1"); + if (huntStatus) { + huntStartTime = huntStatus.start_time; + } else { + // Fallback to hunts table if it exists + const oldHuntRecord = await queryOne( + "SELECT start_time FROM hunts ORDER BY id DESC LIMIT 1" + ); + if (oldHuntRecord) { + huntStartTime = oldHuntRecord.start_time; + } + } + } catch (err) { + console.log("Error fetching hunt status (non-critical):", err.message); + // Continue even if hunt status check fails + } + + // Add cache-busting headers + return Response.json( + { + success: true, + team, + members, + huntStartTime: huntStartTime, + refreshed: new Date().toISOString(), + }, + { + status: 200, + headers: { + "Cache-Control": "no-store, must-revalidate, max-age=0", + Pragma: "no-cache", + Expires: "0", + }, + } + ); + } catch (error) { + console.error("Team fetch error:", error); + return Response.json( + { error: "Failed to fetch team data" }, + { status: 500 } + ); + } +} diff --git a/app/providers/auth-provider.js b/app/providers/auth-provider.js index 8662db2..2aad67a 100644 --- a/app/providers/auth-provider.js +++ b/app/providers/auth-provider.js @@ -1,7 +1,15 @@ -'use client'; +"use client"; import { SessionProvider } from "next-auth/react"; export function NextAuthProvider({ children }) { - return {children}; -} \ No newline at end of file + // Add options to help debug the issue + return ( + + {children} + + ); +} diff --git a/app/waiting/page.js b/app/waiting/page.js new file mode 100644 index 0000000..c7b2f7f --- /dev/null +++ b/app/waiting/page.js @@ -0,0 +1,87 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import styles from "./waiting.module.css"; + +export default function WaitingPage() { + const router = useRouter(); + const { status } = useSession(); + const [huntActive, setHuntActive] = useState(false); + const [loading, setLoading] = useState(true); + + // Check hunt status + useEffect(() => { + // Function to check hunt status + async function checkHuntStatus() { + try { + const response = await fetch("/api/hunt-status", { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store, no-store, must-revalidate", + }, + credentials: "include", + }); + + if (response.ok) { + const data = await response.json(); + console.log("Hunt status from waiting page:", data); + + // If hunt is active, redirect to map page + if (data.active) { + console.log("Hunt is active! Redirecting to map page"); + router.push("/map"); + } + + // Update local state + setHuntActive(data.active); + } + + setLoading(false); + } catch (err) { + console.error("Error checking hunt status:", err); + setLoading(false); + } + } + + // Check on load and every 5 seconds + checkHuntStatus(); + const intervalId = setInterval(checkHuntStatus, 5000); + + return () => clearInterval(intervalId); + }, [router]); + + // Redirect to login if not authenticated + useEffect(() => { + if (status === "unauthenticated") { + router.push("/login"); + } + }, [status, router]); + + if (loading) { + return ( +
+
+

Loading...

+
+ ); + } + + return ( +
+

Waiting for Hunt to Start

+

+ The treasure hunt has not started yet. Please wait for the administrator + to start the hunt. +

+
⏳
+ {huntActive && ( +
+

The hunt has started!

+ +
+ )} +
+ ); +} diff --git a/clue_hunt.db b/clue_hunt.db index cc18ffd5af1f96020e3cdb5dc15fc05a5e02afb2..14aa51cc35f8e14ea7813c0b7a02d8bbe682cd61 100644 GIT binary patch literal 69632 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 diff --git a/hooks/useHuntStatusCheck.js b/hooks/useHuntStatusCheck.js new file mode 100644 index 0000000..fb36e2e --- /dev/null +++ b/hooks/useHuntStatusCheck.js @@ -0,0 +1,58 @@ +"use client"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; + +export function useHuntStatusCheck(checkIntervalMs = 5000) { + const [huntActive, setHuntActive] = useState(null); // Start with null to indicate unknown status + const router = useRouter(); + + useEffect(() => { + // Initial check + checkHuntStatus(); + + // Set up interval for periodic checks + const intervalId = setInterval(checkHuntStatus, checkIntervalMs); + + // Clean up on unmount + return () => clearInterval(intervalId); + + // Function to check hunt status + async function checkHuntStatus() { + try { + const response = await fetch("/api/hunt-status", { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + credentials: "include", + }); + + if (response.ok) { + const data = await response.json(); + console.log("Hunt status check:", data); // Debug log + + // If hunt was active but is now inactive, redirect to leaderboard + if (huntActive === true && data.active === false) { + console.log("Hunt has ended! Redirecting to leaderboard..."); + window.location.href = "/leaderboard?huntEnded=true"; + return; + } + + // If hunt was not active but is now active, let user know they can go to map page + if (huntActive === false && data.active === true) { + console.log("Hunt has started! Access to map is now available"); + // Can show a notification here if desired + } + + // Update hunt status state + setHuntActive(data.active); + } + } catch (err) { + console.error("Failed to check hunt status:", err); + } + } + }, [huntActive, checkIntervalMs, router]); + + return huntActive; +} diff --git a/public/images/fallback-map.jpg b/public/images/fallback-map.jpg new file mode 100644 index 0000000..091c876 --- /dev/null +++ b/public/images/fallback-map.jpg @@ -0,0 +1 @@ +// This would be a new image file you need to add to your project diff --git a/public/images/map.svg b/public/images/map.svg new file mode 100644 index 0000000..3224099 --- /dev/null +++ b/public/images/map.svg @@ -0,0 +1,356 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +