fixed all issues regarding team's member list page where users couldn't leave properly or where they couldn't join properly. the team selection page will now directly communicate with the database to ensure everything stays up to date. The team member list page will also auto update every few seconds so the user will not have to manually update their page.

This commit is contained in:
bobbert committed 2025-03-12 22:18:19 +00:00
1 parent 10c117e223
commit 673ea41a4c
11 files changed
+373 -36

No files matched your search

+39 -9
View File
@@ -42,15 +42,37 @@ export default function TeamPage({ params }) {
throw new Error("Invalid team ID"); throw new Error("Invalid team ID");
} }
console.log("Fetching team data for team ID:", teamId); // First check if the user is still part of this team by fetching current user data
const userResponse = await fetch("/api/user", {
const response = await fetch(`/api/teams/${teamId}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
credentials: "include", 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", cache: "no-store",
}); });
@@ -99,8 +121,8 @@ export default function TeamPage({ params }) {
console.log("Session authenticated, fetching team data"); console.log("Session authenticated, fetching team data");
fetchTeamData(); fetchTeamData();
// Set up an interval to refresh team data every 10 seconds // Use a shorter interval (3 seconds) for more responsive updates
const refreshInterval = setInterval(fetchTeamData, 10000); const refreshInterval = setInterval(fetchTeamData, 3000);
// Clean up the interval when component unmounts // Clean up the interval when component unmounts
return () => clearInterval(refreshInterval); return () => clearInterval(refreshInterval);
@@ -190,10 +212,18 @@ export default function TeamPage({ params }) {
}); });
if (response.ok) { 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 { } else {
const error = await response.json(); const errorData = await response.json();
throw new Error(error.message || "Failed to leave team"); throw new Error(errorData.error || "Failed to leave team");
} }
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
+4 -1
View File
@@ -88,9 +88,12 @@ export default function HuntNotStarted() {
color: white; color: white;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
padding: 10px 20px; padding: 12px 30px; /* Increased padding */
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
min-width: 150px; /* Ensure minimum width */
display: inline-block; /* Proper box model */
text-align: center; /* Center text */
} }
.back-button:hover { .back-button:hover {
+28 -5
View File
@@ -12,14 +12,37 @@ export default function TeamSelection() {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
// Clear any leftover team data cache from previous teams
localStorage.removeItem("teamData");
sessionStorage.removeItem("teamData");
localStorage.removeItem("userLeftTeam");
if (status === "authenticated") { if (status === "authenticated") {
if (session.user.teamId) { // Always verify team status with a fresh API call to avoid stale session data
window.location.href = `/${session.user.teamId}`; fetch(`/api/user?_t=${Date.now()}`, {
} else { headers: {
setIsLoading(false); "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") { } else if (status === "unauthenticated") {
router.push("/login"); router.push("/login");
} else {
// Still loading session
setIsLoading(true);
} }
}, [status, session, router]); }, [status, session, router]);
+46 -7
View File
@@ -37,14 +37,38 @@ export default function TeamPage({ params }) {
throw new Error("Invalid team ID"); throw new Error("Invalid team ID");
} }
console.log("Fetching team data for team ID:", teamId); // First check if the user is still part of this team by fetching current user data
const userResponse = await fetch("/api/user", {
const response = await fetch(`/api/teams/${teamId}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
credentials: "include", 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) { if (!response.ok) {
@@ -90,6 +114,12 @@ export default function TeamPage({ params }) {
if (status === "authenticated" && teamId) { if (status === "authenticated" && teamId) {
console.log("Session authenticated, fetching team data"); console.log("Session authenticated, fetching team data");
fetchTeamData(); 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") { } else if (status === "unauthenticated") {
console.log("User not authenticated, redirecting to login"); console.log("User not authenticated, redirecting to login");
router.push("/login"); router.push("/login");
@@ -174,10 +204,18 @@ export default function TeamPage({ params }) {
}); });
if (response.ok) { 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 { } else {
const error = await response.json(); const errorData = await response.json();
throw new Error(error.message || "Failed to leave team"); throw new Error(errorData.error || "Failed to leave team");
} }
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@@ -195,7 +233,6 @@ export default function TeamPage({ params }) {
onClick={async () => { onClick={async () => {
try { try {
setLoading(true); setLoading(true);
if (!huntActive) { if (!huntActive) {
// If hunt is not active, redirect to the "not started" page // If hunt is not active, redirect to the "not started" page
router.push(`/hunt-not-started?teamId=${teamId}`); router.push(`/hunt-not-started?teamId=${teamId}`);
@@ -318,6 +355,8 @@ export default function TeamPage({ params }) {
.members-container { .members-container {
padding: 20px; padding: 20px;
flex: 1; flex: 1;
display: flex;
flex-direction: column;
} }
.members-title { .members-title {
+26 -5
View File
@@ -1,6 +1,6 @@
import { queryOne, run } from "@/lib/db"; import { queryOne, run } from "@/lib/db";
import { cookies } from "next/headers"; 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) { export async function POST(request) {
try { try {
@@ -36,13 +36,34 @@ export async function POST(request) {
userData.id, 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, teamId: team.id,
teamName: team.name, teamName: team.name,
success: true, 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) { } catch (error) {
console.error("Join team error:", error); console.error("Join team error:", error);
+89
View File
@@ -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 }
);
}
}
+5 -1
View File
@@ -43,17 +43,21 @@ export async function GET(request, context) {
const timestamp = new Date().toISOString(); // Add timestamp for debugging const timestamp = new Date().toISOString(); // Add timestamp for debugging
// Add additional cache-busting headers
return Response.json( return Response.json(
{ {
success: true, success: true,
team, team,
members, members,
refreshed: timestamp, refreshed: timestamp,
lastUpdated: team.last_updated || timestamp,
}, },
{ {
status: 200, status: 200,
headers: { headers: {
"Cache-Control": "no-store, max-age=0", // Prevent caching "Cache-Control": "no-store, must-revalidate, max-age=0",
Pragma: "no-cache",
Expires: "0",
}, },
} }
); );
@@ -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 }
);
}
}
+31 -8
View File
@@ -1,9 +1,12 @@
// app/api/user/route.js // app/api/user/route.js
import { queryOne } from "@/lib/db";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
export const dynamic = "force-dynamic"; // Disable caching
export async function GET() { export async function GET() {
try { try {
// Get the token from cookies - Fix: await the cookies() call // Get the token from cookies
const cookieStore = await cookies(); const cookieStore = await cookies();
const sessionToken = cookieStore.get("next-auth.session-token")?.value; const sessionToken = cookieStore.get("next-auth.session-token")?.value;
@@ -11,7 +14,7 @@ export async function GET() {
return Response.json({ error: "Unauthorized" }, { status: 401 }); 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"); const { verifyToken } = await import("../auth/[...nextauth]/route");
try { try {
@@ -21,12 +24,32 @@ export async function GET() {
return Response.json({ error: "Invalid session" }, { status: 401 }); return Response.json({ error: "Invalid session" }, { status: 401 });
} }
return Response.json({ // Always fetch the latest user data from the database to ensure it's accurate
id: userData.id, const freshUserData = await queryOne(
username: userData.username, "SELECT id, username, role, team_id FROM users WHERE id = ?",
role: userData.role, [userData.id]
teamId: userData.teamId, );
});
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) { } catch (tokenError) {
console.error("Token verification error:", tokenError); console.error("Token verification error:", tokenError);
return Response.json({ error: "Invalid session token" }, { status: 401 }); return Response.json({ error: "Invalid session token" }, { status: 401 });
BIN
View File
Binary file not shown.
+33
View File
@@ -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;