diff --git a/app/(main)/[teamId]/page.js b/app/(main)/[teamId]/page.js index 52d018e..b3362b0 100644 --- a/app/(main)/[teamId]/page.js +++ b/app/(main)/[teamId]/page.js @@ -42,15 +42,37 @@ export default function TeamPage({ params }) { throw new Error("Invalid team ID"); } - console.log("Fetching team data for team ID:", teamId); - - const response = await fetch(`/api/teams/${teamId}`, { + // First check if the user is still part of this team by fetching current user data + const userResponse = await fetch("/api/user", { method: "GET", headers: { "Content-Type": "application/json", }, credentials: "include", - // Add cache: 'no-store' to prevent caching of team data + cache: "no-store", + }); + + const userData = await userResponse.json(); + + // If the user is not in this team anymore, redirect to team selection + if (!userData.teamId || userData.teamId != teamId) { + console.log("User no longer in team, redirecting to team selection"); + window.location.href = "/team-selection"; + return; + } + + console.log("Fetching team data for team ID:", teamId); + + // Add timestamp to URL to ensure fresh data on every request + const timestamp = new Date().getTime(); + const response = await fetch(`/api/teams/${teamId}?_t=${timestamp}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + }, + credentials: "include", cache: "no-store", }); @@ -99,8 +121,8 @@ export default function TeamPage({ params }) { console.log("Session authenticated, fetching team data"); fetchTeamData(); - // Set up an interval to refresh team data every 10 seconds - const refreshInterval = setInterval(fetchTeamData, 10000); + // Use a shorter interval (3 seconds) for more responsive updates + const refreshInterval = setInterval(fetchTeamData, 3000); // Clean up the interval when component unmounts return () => clearInterval(refreshInterval); @@ -190,10 +212,18 @@ export default function TeamPage({ params }) { }); if (response.ok) { - router.push("/team-selection"); + // Immediately remove team data from all storage + localStorage.removeItem("teamData"); + sessionStorage.removeItem("teamData"); + + // Set a flag so we know the user has left this team + localStorage.setItem("userLeftTeam", "true"); + + // Force redirect to team selection + window.location.href = "/team-selection"; } else { - const error = await response.json(); - throw new Error(error.message || "Failed to leave team"); + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to leave team"); } } catch (err) { setError(err.message); diff --git a/app/(main)/hunt-not-started/page.js b/app/(main)/hunt-not-started/page.js index 0726e09..58f8f66 100644 --- a/app/(main)/hunt-not-started/page.js +++ b/app/(main)/hunt-not-started/page.js @@ -88,9 +88,12 @@ export default function HuntNotStarted() { color: white; border: none; border-radius: 4px; - padding: 10px 20px; + padding: 12px 30px; /* Increased padding */ font-size: 16px; cursor: pointer; + min-width: 150px; /* Ensure minimum width */ + display: inline-block; /* Proper box model */ + text-align: center; /* Center text */ } .back-button:hover { diff --git a/app/(main)/team-selection/page.js b/app/(main)/team-selection/page.js index 7937c3d..c7bc71a 100644 --- a/app/(main)/team-selection/page.js +++ b/app/(main)/team-selection/page.js @@ -12,14 +12,37 @@ export default function TeamSelection() { const router = useRouter(); useEffect(() => { + // Clear any leftover team data cache from previous teams + localStorage.removeItem("teamData"); + sessionStorage.removeItem("teamData"); + localStorage.removeItem("userLeftTeam"); + if (status === "authenticated") { - if (session.user.teamId) { - window.location.href = `/${session.user.teamId}`; - } else { - setIsLoading(false); - } + // Always verify team status with a fresh API call to avoid stale session data + fetch(`/api/user?_t=${Date.now()}`, { + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }) + .then((res) => res.json()) + .then((userData) => { + if (userData.teamId) { + window.location.href = `/${userData.teamId}`; + } else { + setIsLoading(false); + } + }) + .catch((error) => { + console.error("Failed to fetch user data:", error); + setIsLoading(false); + }); } else if (status === "unauthenticated") { router.push("/login"); + } else { + // Still loading session + setIsLoading(true); } }, [status, session, router]); diff --git a/app/(main)/team/[teamId]/page.js b/app/(main)/team/[teamId]/page.js index ae30ddf..10e4887 100644 --- a/app/(main)/team/[teamId]/page.js +++ b/app/(main)/team/[teamId]/page.js @@ -37,14 +37,38 @@ export default function TeamPage({ params }) { throw new Error("Invalid team ID"); } - console.log("Fetching team data for team ID:", teamId); - - const response = await fetch(`/api/teams/${teamId}`, { + // First check if the user is still part of this team by fetching current user data + const userResponse = await fetch("/api/user", { method: "GET", headers: { "Content-Type": "application/json", }, credentials: "include", + cache: "no-store", + }); + + const userData = await userResponse.json(); + + // If the user is not in this team anymore, redirect to team selection + if (!userData.teamId || userData.teamId != teamId) { + console.log("User no longer in team, redirecting to team selection"); + window.location.href = "/team-selection"; + return; + } + + console.log("Fetching team data for team ID:", teamId); + + // Add timestamp to URL to ensure fresh data on every request + const timestamp = new Date().getTime(); + const response = await fetch(`/api/teams/${teamId}?_t=${timestamp}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + cache: "no-store", }); if (!response.ok) { @@ -90,6 +114,12 @@ export default function TeamPage({ params }) { if (status === "authenticated" && teamId) { console.log("Session authenticated, fetching team data"); fetchTeamData(); + + // Use a shorter interval (3 seconds) for more responsive updates + const refreshInterval = setInterval(fetchTeamData, 3000); + + // Clean up the interval when component unmounts + return () => clearInterval(refreshInterval); } else if (status === "unauthenticated") { console.log("User not authenticated, redirecting to login"); router.push("/login"); @@ -174,10 +204,18 @@ export default function TeamPage({ params }) { }); if (response.ok) { - router.push("/team-selection"); + // Immediately remove team data from session state + localStorage.removeItem("teamData"); + sessionStorage.removeItem("teamData"); + + // Set a flag so we know the user has left this team + localStorage.setItem("userLeftTeam", "true"); + + // Force redirect to team selection + window.location.href = "/team-selection"; } else { - const error = await response.json(); - throw new Error(error.message || "Failed to leave team"); + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to leave team"); } } catch (err) { setError(err.message); @@ -195,7 +233,6 @@ export default function TeamPage({ params }) { onClick={async () => { try { setLoading(true); - if (!huntActive) { // If hunt is not active, redirect to the "not started" page router.push(`/hunt-not-started?teamId=${teamId}`); @@ -318,6 +355,8 @@ export default function TeamPage({ params }) { .members-container { padding: 20px; flex: 1; + display: flex; + flex-direction: column; } .members-title { diff --git a/app/api/join-team/route.js b/app/api/join-team/route.js index 9ab65d2..feebc18 100644 --- a/app/api/join-team/route.js +++ b/app/api/join-team/route.js @@ -1,6 +1,6 @@ import { queryOne, run } from "@/lib/db"; import { cookies } from "next/headers"; -import { verifyToken } from "@/app/api/auth/[...nextauth]/route"; +import { verifyToken, createToken } from "@/app/api/auth/[...nextauth]/route"; export async function POST(request) { try { @@ -36,13 +36,34 @@ export async function POST(request) { userData.id, ]); - return Response.json( - { + // 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 + }; + + const newToken = createToken(newUserData); + + // Calculate expiration date for cookie + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + 30); + + // Return success with updated session token + return new Response( + JSON.stringify({ teamId: team.id, teamName: team.name, success: true, - }, - { status: 200 } + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": `next-auth.session-token=${newToken}; Path=/; HttpOnly; SameSite=Lax; Expires=${expiryDate.toUTCString()}`, + }, + } ); } catch (error) { console.error("Join team error:", error); diff --git a/app/api/teams/[teamId]/leave/route.js b/app/api/teams/[teamId]/leave/route.js new file mode 100644 index 0000000..5a1f7e3 --- /dev/null +++ b/app/api/teams/[teamId]/leave/route.js @@ -0,0 +1,89 @@ +import { run } from "@/lib/db"; +import { cookies } from "next/headers"; +import { verifyToken, createToken } from "@/app/api/auth/[...nextauth]/route"; + +export async function POST(request, context) { + try { + // Verify user authentication + const cookieStore = await cookies(); + const sessionToken = cookieStore.get("next-auth.session-token")?.value; + + if (!sessionToken) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userData = verifyToken(sessionToken); + if (!userData) { + return Response.json({ error: "Invalid session" }, { status: 401 }); + } + + // Get teamId from URL params + const params = await Promise.resolve(context.params); + const teamId = params?.teamId; + + // Validate teamId + if (!teamId || isNaN(parseInt(teamId, 10))) { + return Response.json({ error: "Invalid team ID" }, { status: 400 }); + } + + // Check if user is actually in this team + if (userData.teamId != teamId) { + return Response.json( + { error: "You are not part of this team" }, + { status: 403 } + ); + } + + // Update the user record to remove team association + await run("UPDATE users SET team_id = NULL WHERE id = ?", [userData.id]); + + // Try to update the team's last_updated timestamp but don't fail if column doesn't exist + try { + await run("UPDATE teams SET last_updated = ? WHERE id = ?", [ + new Date().toISOString(), + teamId, + ]); + } catch (updateError) { + // Log but don't fail if we can't update timestamp + console.warn( + "Failed to update last_updated timestamp:", + updateError.message + ); + } + + // Create a new token with updated user data (without teamId) + const newUserData = { + ...userData, + teamId: null, // Remove team association in token + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days + }; + + const newToken = createToken(newUserData); + + // Calculate expiration date for cookie + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + 30); + + // Return success with updated session token + return new Response( + JSON.stringify({ + success: true, + message: "Successfully left the team", + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": `next-auth.session-token=${newToken}; Path=/; HttpOnly; SameSite=Lax; Expires=${expiryDate.toUTCString()}`, + }, + } + ); + } catch (error) { + console.error("Leave team error:", error); + return Response.json( + { error: "Failed to leave team: " + error.message }, + { status: 500 } + ); + } +} diff --git a/app/api/teams/[teamId]/route.js b/app/api/teams/[teamId]/route.js index 83c20d2..6477b63 100644 --- a/app/api/teams/[teamId]/route.js +++ b/app/api/teams/[teamId]/route.js @@ -43,17 +43,21 @@ export async function GET(request, context) { const timestamp = new Date().toISOString(); // Add timestamp for debugging + // Add additional cache-busting headers return Response.json( { success: true, team, members, refreshed: timestamp, + lastUpdated: team.last_updated || timestamp, }, { status: 200, headers: { - "Cache-Control": "no-store, max-age=0", // Prevent caching + "Cache-Control": "no-store, must-revalidate, max-age=0", + Pragma: "no-cache", + Expires: "0", }, } ); diff --git a/app/api/teams/[teamId]/update-members/route.js b/app/api/teams/[teamId]/update-members/route.js new file mode 100644 index 0000000..04ae6c4 --- /dev/null +++ b/app/api/teams/[teamId]/update-members/route.js @@ -0,0 +1,72 @@ +import { query, run } from "@/lib/db"; +import { cookies } from "next/headers"; +import { verifyToken } from "@/app/api/auth/[...nextauth]/route"; + +export async function POST(request, context) { + try { + // Verify user authentication + const cookieStore = await cookies(); + const sessionToken = cookieStore.get("next-auth.session-token")?.value; + + if (!sessionToken) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userData = verifyToken(sessionToken); + if (!userData) { + return Response.json({ error: "Invalid session" }, { status: 401 }); + } + + // Get teamId from URL params + const params = await Promise.resolve(context.params); + const teamId = params?.teamId; + + // Validate teamId + if (!teamId || isNaN(parseInt(teamId, 10))) { + return Response.json({ error: "Invalid team ID" }, { status: 400 }); + } + + // Try to update the last_updated timestamp for this team + try { + await run("UPDATE teams SET last_updated = ? WHERE id = ?", [ + new Date().toISOString(), + teamId, + ]); + } catch (updateError) { + // Log but don't fail if we can't update timestamp + console.warn( + "Failed to update last_updated timestamp:", + updateError.message + ); + + // Try to alter the table to add the column + try { + await run( + "ALTER TABLE teams ADD COLUMN last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ); + console.log("Added missing last_updated column to teams table"); + + // Try the update again + await run("UPDATE teams SET last_updated = ? WHERE id = ?", [ + new Date().toISOString(), + teamId, + ]); + } catch (alterError) { + console.error("Failed to alter teams table:", alterError.message); + } + } + + // Return success response + return Response.json({ + success: true, + message: "Successfully updated team members list", + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error("Team members update error:", error); + return Response.json( + { error: "Failed to update team members: " + error.message }, + { status: 500 } + ); + } +} diff --git a/app/api/user/route.js b/app/api/user/route.js index f09c275..70a1527 100644 --- a/app/api/user/route.js +++ b/app/api/user/route.js @@ -1,9 +1,12 @@ // app/api/user/route.js +import { queryOne } from "@/lib/db"; import { cookies } from "next/headers"; +export const dynamic = "force-dynamic"; // Disable caching + export async function GET() { try { - // Get the token from cookies - Fix: await the cookies() call + // Get the token from cookies const cookieStore = await cookies(); const sessionToken = cookieStore.get("next-auth.session-token")?.value; @@ -11,7 +14,7 @@ export async function GET() { return Response.json({ error: "Unauthorized" }, { status: 401 }); } - // Import the token verification function - using a direct import + // Import the token verification function const { verifyToken } = await import("../auth/[...nextauth]/route"); try { @@ -21,12 +24,32 @@ export async function GET() { return Response.json({ error: "Invalid session" }, { status: 401 }); } - return Response.json({ - id: userData.id, - username: userData.username, - role: userData.role, - teamId: userData.teamId, - }); + // Always fetch the latest user data from the database to ensure it's accurate + const freshUserData = await queryOne( + "SELECT id, username, role, team_id FROM users WHERE id = ?", + [userData.id] + ); + + if (!freshUserData) { + return Response.json({ error: "User not found" }, { status: 404 }); + } + + return Response.json( + { + id: freshUserData.id, + username: freshUserData.username, + role: freshUserData.role, + teamId: freshUserData.team_id, + timestamp: new Date().toISOString(), // Add timestamp for debugging + }, + { + headers: { + "Cache-Control": "no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", + }, + } + ); } catch (tokenError) { console.error("Token verification error:", tokenError); return Response.json({ error: "Invalid session token" }, { status: 401 }); diff --git a/clue_hunt.db b/clue_hunt.db index 58651fa..f1a283f 100644 Binary files a/clue_hunt.db and b/clue_hunt.db differ diff --git a/lib/update-schema.js b/lib/update-schema.js new file mode 100644 index 0000000..faa1401 --- /dev/null +++ b/lib/update-schema.js @@ -0,0 +1,33 @@ +import { run } from "./db.js"; + +async function updateDatabaseSchema() { + try { + console.log("Checking and updating database schema..."); + + // Check if last_updated column exists in teams table + try { + // Add last_updated column to teams table if it doesn't exist + // SQLite syntax for adding columns if they don't exist + await run(` + ALTER TABLE teams ADD COLUMN last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP; + `); + console.log("Added last_updated column to teams table"); + } catch (error) { + // If error contains "duplicate column name" then the column already exists + if (!error.message.includes("duplicate column name")) { + throw error; // Re-throw if it's a different error + } + console.log("last_updated column already exists in teams table"); + } + + console.log("Database schema update completed successfully"); + } catch (error) { + console.error("Database schema update error:", error); + } +} + +// Run the update +updateDatabaseSchema(); + +// Export for potential use in other modules +export default updateDatabaseSchema;