diff --git a/app/(main)/[teamId]/page.js b/app/(main)/[teamId]/page.js index 7af59a3..1fec933 100644 --- a/app/(main)/[teamId]/page.js +++ b/app/(main)/[teamId]/page.js @@ -5,6 +5,9 @@ import { useRouter } from "next/navigation"; import Link from "next/link"; import { use } from "react"; // Add this import for React.use() import NavBar from "@/components/NavBar"; +import { getHuntTimer } from "@/utils/timerService"; +// Remove the CSS module import +// import styles from "./teamId.module.css"; export default function TeamPage({ params }) { const { status, data: session } = useSession(); @@ -13,6 +16,7 @@ export default function TeamPage({ params }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [huntActive, setHuntActive] = useState(false); + const [huntInProgress, setHuntInProgress] = useState(false); const router = useRouter(); // Unwrap the params Promise using React.use() @@ -133,6 +137,13 @@ export default function TeamPage({ params }) { } }, [teamId, status, fetchTeamData, router]); + // Check if hunt is already in progress on component mount + useEffect(() => { + // Get timer data and check if a hunt has already been started + const timerData = getHuntTimer(); + setHuntInProgress(timerData && timerData.startTime); + }, []); + if (status === "loading" || loading) { return (
@@ -166,8 +177,13 @@ export default function TeamPage({ params }) { ); } + const handleStartHunt = () => { + router.push("/map"); + }; + return (
+ {/* Changed from styles.container to team-page */}

Team Name: {team.name}

@@ -235,45 +251,12 @@ export default function TeamPage({ params }) { {/* New Start Hunt button with conditional behavior */} - {session?.user?.role !== "admin" && ( - - )} +
{/* Replace the outdated navbar with the correct one */} diff --git a/app/(main)/[teamId]/teamId.module.css b/app/(main)/[teamId]/teamId.module.css new file mode 100644 index 0000000..ea5320a --- /dev/null +++ b/app/(main)/[teamId]/teamId.module.css @@ -0,0 +1,14 @@ +.container { + display: flex; + flex-direction: column; + min-height: 100vh; + background-color: #f4f4f4; + font-family: Arial, sans-serif; + position: relative; + overflow-x: hidden; + width: 100%; + padding-bottom: 70px; +} + +/* Note: Most styles are defined inline in the component, + but we're creating this file to fix the build error */ diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index a83d252..96b428e 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -375,11 +375,11 @@ export default function MapPage() { ) } style={{ - left: "230px", // Moved from 150px to 160px (10px right) - top: "212px", // Moved from 200px to 203px (3px down) + left: "230px", + top: "212px", }} > - Clue {firstClue.id} + {/* Clue {firstClue.id} */} )} @@ -394,11 +394,12 @@ export default function MapPage() { ) } style={{ - left: "320px", // 168px + 50px = 218px - top: "290px", // 208px + 30px = 238px + left: "320px", + top: "285px", }} > - Clue {clues[1].id} + {/* Clue {clues[1].id} */} + {/* If clues are to be named uncomment the line above */} )} @@ -413,11 +414,11 @@ export default function MapPage() { ) } style={{ - left: "265px", // 168px + 50px = 218px - top: "400px", // 208px + 30px = 238px + left: "265px", + top: "400px", }} > - Clue {clues[2].id} + {/* Clue {clues[2].id} */} )} diff --git a/app/(main)/team-selection/page.js b/app/(main)/team-selection/page.js index 6768f41..dece12b 100644 --- a/app/(main)/team-selection/page.js +++ b/app/(main)/team-selection/page.js @@ -1,11 +1,11 @@ "use client"; import { useState, useEffect } from "react"; -import { useSession } from "next-auth/react"; +import { useSession, signOut } from "next-auth/react"; import { useRouter } from "next/navigation"; import Link from "next/link"; export default function TeamSelection() { - const { data: session, status } = useSession(); + const { data: session, status, update: updateSession } = useSession(); const [teamCode, setTeamCode] = useState(""); const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(true); @@ -53,28 +53,110 @@ export default function TeamSelection() { setIsLoading(true); setError(""); + // Validate team code before submitting + if (!teamCode || teamCode.trim() === "") { + setError("Please enter a team code"); + setIsLoading(false); + return; + } + + // Clean the team code of any whitespace + const cleanedTeamCode = teamCode.trim().toUpperCase(); + console.log("Joining team with code:", cleanedTeamCode); + try { - const response = await fetch("/api/join-team", { + // First check if we have a valid session + if (status !== "authenticated" || !session?.user) { + console.log("Session not authenticated, refreshing session..."); + await updateSession(); + + // If still not authenticated after refresh, redirect to login + if (status !== "authenticated") { + throw new Error( + "Your session appears to be invalid. Please log in again." + ); + } + } + + // Now attempt to join the team + const response = await fetch("/api/teams/join", { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ teamCode }), + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + body: JSON.stringify({ teamCode: cleanedTeamCode }), + credentials: "include", }); - const data = await response.json(); + // Improved error handling for response + let data; + try { + // First try to parse as JSON + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + data = await response.json(); + } else { + // If not JSON, get as text + const text = await response.text(); + console.log("Non-JSON response:", text); + throw new Error( + `Unexpected response format: ${text.substring(0, 100)}...` + ); + } + } catch (parseError) { + console.error("Error parsing response:", parseError); + throw new Error( + "Failed to parse server response: " + parseError.message + ); + } + // Now check if the response was successful if (!response.ok) { - throw new Error(data.error || "Failed to join team"); + console.error("Join team failed:", data); + throw new Error( + data.error || `Failed to join team (Status: ${response.status})` + ); } // Add validation before constructing the URL if (!data.teamId || isNaN(parseInt(data.teamId))) { - throw new Error("Invalid team ID received"); + throw new Error("Invalid team ID received from server"); } - // Navigate to the team page using the correct URL format - window.location.href = `/${data.teamId}`; + console.log("Join team successful:", data); + + // Update the session with new team info + if (data.token) { + try { + await updateSession(); + console.log("Session updated successfully"); + } catch (sessionErr) { + console.warn("Session update failed:", sessionErr); + } + } + + // Navigate to the team page + router.push(`/${data.teamId}`); } catch (err) { - setError(err.message); + console.error("Join team error:", err); + + // If it's an "Invalid session" error, suggest reauthentication + if ( + err.message.includes("session") || + err.message.includes("unauthorized") || + err.message.includes("Unauthorized") + ) { + setError("Your session has expired. Please log in again."); + // Attempt to sign out and redirect to login + await signOut({ redirect: false }); + setTimeout(() => { + router.push("/login?error=session_expired"); + }, 2000); + } else { + setError(err.message); + } + setIsLoading(false); } }; @@ -100,7 +182,7 @@ export default function TeamSelection() { type="text" placeholder="Team Code" value={teamCode} - onChange={(e) => setTeamCode(e.target.value)} + onChange={(e) => setTeamCode(e.target.value.toUpperCase())} required className="auth-input" /> @@ -118,8 +200,6 @@ export default function TeamSelection() {
- {/* Navbar removed from here */} -
); diff --git a/app/api/auth/[...nextauth]/route.js b/app/api/auth/[...nextauth]/route.js index f314989..b55bc6e 100644 --- a/app/api/auth/[...nextauth]/route.js +++ b/app/api/auth/[...nextauth]/route.js @@ -54,13 +54,26 @@ export const authOptions = { token.username = user.username; token.teamId = user.team_id || user.teamId; } + // If teamId is not set on the token, query DB for the latest value. if (!token.teamId) { - const freshUser = await queryOne( - "SELECT team_id FROM users WHERE id = ?", - [token.id] - ); - token.teamId = freshUser?.team_id; + try { + const freshUser = await queryOne( + "SELECT team_id FROM users WHERE id = ?", + [token.id] + ); + token.teamId = freshUser?.team_id; + } catch (error) { + console.error("Error refreshing teamId from database:", error); + } + } + + // Ensure both sub and id exist for maximum compatibility + if (token.id && !token.sub) { + token.sub = token.id.toString(); + } + if (token.sub && !token.id) { + token.id = parseInt(token.sub, 10); } return token; }, @@ -101,11 +114,42 @@ export function createToken(payload) { } export function verifyToken(token) { - if (!token) return null; try { - return jwt.verify(token, JWT_SECRET); + if (!token) { + console.error("No token provided"); + return null; + } + + console.log("Verifying token:", token.substring(0, 10) + "..."); + + const secret = + process.env.NEXTAUTH_SECRET || + "your-fallback-secret-should-be-at-least-32-chars"; + + const decoded = jwt.verify(token, secret); + + // Validate that we have the minimum required fields + if (!decoded.sub && !decoded.id) { + console.error("Token missing required user identifier"); + return null; + } + + // Log successful token verification with user ID + console.log( + "Token verified successfully for user:", + decoded.sub || decoded.id + ); + + return decoded; } catch (error) { - console.error("Token verification error:", error); + // Provide more detailed error logs + if (error.name === "TokenExpiredError") { + console.error("Token expired at:", error.expiredAt); + } else if (error.name === "JsonWebTokenError") { + console.error("Invalid token:", error.message); + } else { + console.error("Token verification error:", error.message); + } return null; } } diff --git a/app/api/teams/join/route.js b/app/api/teams/join/route.js new file mode 100644 index 0000000..8806592 --- /dev/null +++ b/app/api/teams/join/route.js @@ -0,0 +1,133 @@ +import { cookies } from "next/headers"; +import { verifyToken, createToken, JWT_SECRET } from "@/lib/auth"; +import { query, queryOne, run } from "@/lib/db"; +import { getToken } from "next-auth/jwt"; + +export const dynamic = "force-dynamic"; // Disable caching for this route + +export async function POST(request) { + try { + // Parse the request body first to get the team code + const body = await request.json().catch((err) => { + console.error("Failed to parse request body:", err); + return {}; + }); + + const { teamCode } = body; + const cleanedCode = teamCode?.trim().toUpperCase(); + + if (!cleanedCode) { + console.log("No team code provided"); + return Response.json({ error: "Team code is required" }, { status: 400 }); + } + + // Now verify authentication after we've parsed the request + const token = await getToken({ + req: request, + secret: JWT_SECRET, + }); + + if (!token) { + console.log("No authentication token found"); + return Response.json({ error: "Invalid session" }, { status: 401 }); + } + + const userData = token; + const userId = userData.id || userData.sub; + + if (!userId) { + console.log("No user ID in token"); + return Response.json({ error: "Invalid user data" }, { status: 401 }); + } + + console.log( + `User ${userId} attempting to join team with code ${cleanedCode}` + ); + + // Check if the user exists in the database + const userExists = await queryOne("SELECT id FROM users WHERE id = ?", [ + userId, + ]); + + if (!userExists) { + console.error(`User ${userId} not found in database`); + return Response.json( + { error: "User not found. Please log in again." }, + { status: 404 } + ); + } + + // Check if the team with this code exists + const team = await queryOne("SELECT * FROM teams WHERE code = ?", [ + cleanedCode, + ]); + + if (!team) { + console.log(`Team with code "${cleanedCode}" not found`); + return Response.json({ error: "Invalid team code" }, { status: 400 }); + } + + console.log(`Found team: ${team.id} - ${team.name}`); + + // Check if user is already in a team + const user = await queryOne("SELECT team_id FROM users WHERE id = ?", [ + userId, + ]); + + if (user && user.team_id) { + // User already in a team + if (user.team_id === team.id) { + return Response.json( + { error: "You are already a member of this team" }, + { status: 400 } + ); + } else { + // Leave current team first + await run("UPDATE users SET team_id = NULL WHERE id = ?", [userId]); + } + } + + // Add user to the team + await run("UPDATE users SET team_id = ? WHERE id = ?", [team.id, userId]); + + console.log(`User ${userId} added to team ${team.id}`); + + // Create a new token with updated user data + const newUserData = { + ...userData, + teamId: team.id, // Update team association in token + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days + }; + + // Use the createToken function to generate a new token + const newToken = createToken(newUserData); + + // Return success with the token included + return Response.json( + { + teamId: team.id, + teamName: team.name, + token: newToken, // Include the token in the response + success: true, + }, + { + status: 200, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store, no-cache, must-revalidate", + }, + } + ); + } catch (error) { + console.error("Error joining team:", error); + return Response.json( + { + error: + "An error occurred while joining the team: " + + (error.message || "Unknown error"), + }, + { status: 500 } + ); + } +} diff --git a/clue_hunt.db b/clue_hunt.db index e678017..670ba50 100644 Binary files a/clue_hunt.db and b/clue_hunt.db differ diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 0000000..074b4ac --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,55 @@ +import jwt from "jsonwebtoken"; + +// Get JWT secret from environment or use fallback +const JWT_SECRET = + process.env.NEXTAUTH_SECRET || + process.env.JWT_SECRET || + "your-fallback-secret-should-be-at-least-32-chars"; + +// Create a new JWT token +export function createToken(payload) { + // Check if payload already has an expiration date set + const options = payload.exp ? {} : { expiresIn: "30d" }; + + return jwt.sign(payload, JWT_SECRET, options); +} + +// Verify a JWT token +export function verifyToken(token) { + try { + if (!token) { + console.error("No token provided"); + return null; + } + + console.log("Verifying token:", token.substring(0, 10) + "..."); + + const decoded = jwt.verify(token, JWT_SECRET); + + // Validate that we have the minimum required fields + if (!decoded.sub && !decoded.id) { + console.error("Token missing required user identifier"); + return null; + } + + // Log successful token verification with user ID + console.log( + "Token verified successfully for user:", + decoded.sub || decoded.id + ); + + return decoded; + } catch (error) { + // Provide detailed error logs + if (error.name === "TokenExpiredError") { + console.error("Token expired at:", error.expiredAt); + } else if (error.name === "JsonWebTokenError") { + console.error("Invalid token:", error.message); + } else { + console.error("Token verification error:", error.message); + } + return null; + } +} + +export { JWT_SECRET };