);
}
- 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
-
-
-
-
- {/* 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 (
+
+ );
+ }
+
+ 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 cc18ffd..14aa51c 100644
Binary files a/clue_hunt.db and b/clue_hunt.db differ
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 @@
+