mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
cannot fix issue with elapsed timer on admin dashboard. fixing issues with QR code editing.
Grey = cant click Red = current Green = complete blue = skipped?
This commit is contained in:
1 parent
7d6b73ffb4
commit
0afa6817a8
12 files changed
+1529
-211
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { queryOne, run } from "@/lib/db";
|
||||
import { queryOne, query, run } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic"; // Disable caching
|
||||
|
||||
@@ -32,39 +32,120 @@ export async function DELETE(request, context) {
|
||||
return Response.json({ error }, { status });
|
||||
}
|
||||
|
||||
const { clueId } = context.params;
|
||||
// Get clueId parameter - properly awaited
|
||||
const { clueId } = await context.params;
|
||||
|
||||
// Validate clueId is a number
|
||||
if (isNaN(parseInt(clueId))) {
|
||||
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if clue exists
|
||||
const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
|
||||
// Always set shouldRenumber to true - no longer read from request body
|
||||
const shouldRenumber = true;
|
||||
|
||||
// Check if clue exists and get its QR code value
|
||||
const clue = await queryOne("SELECT id, qr_code FROM clues WHERE id = ?", [
|
||||
clueId,
|
||||
]);
|
||||
if (!clue) {
|
||||
return Response.json({ error: "Clue not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete clue record
|
||||
await run("DELETE FROM clues WHERE id = ?", [clueId]);
|
||||
console.log(
|
||||
`Starting deletion of clue ${clueId} with QR code ${clue.qr_code}`
|
||||
);
|
||||
|
||||
// Also delete any references in team_clues table
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS team_clues (
|
||||
team_id INTEGER NOT NULL,
|
||||
clue_id INTEGER NOT NULL,
|
||||
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (team_id, clue_id),
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id),
|
||||
FOREIGN KEY (clue_id) REFERENCES clues(id)
|
||||
)
|
||||
`);
|
||||
await run("DELETE FROM team_clues WHERE clue_id = ?", [clueId]);
|
||||
// Start a transaction for atomicity
|
||||
try {
|
||||
await run("BEGIN TRANSACTION");
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "Clue deleted successfully",
|
||||
});
|
||||
console.log("Transaction started");
|
||||
|
||||
// First, make sure the team_clues table exists and clear any references
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS team_clues (
|
||||
team_id INTEGER NOT NULL,
|
||||
clue_id INTEGER NOT NULL,
|
||||
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (team_id, clue_id),
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (clue_id) REFERENCES clues(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Delete any references in team_clues table
|
||||
const deleteTeamClues = await run(
|
||||
"DELETE FROM team_clues WHERE clue_id = ?",
|
||||
[clueId]
|
||||
);
|
||||
console.log(
|
||||
`Deleted team clues result: ${deleteTeamClues.changes} references cleared`
|
||||
);
|
||||
|
||||
// Delete clue record
|
||||
const deleteResult = await run("DELETE FROM clues WHERE id = ?", [
|
||||
clueId,
|
||||
]);
|
||||
console.log(`Deleted clue result: ${deleteResult.changes} rows affected`);
|
||||
|
||||
if (deleteResult.changes === 0) {
|
||||
throw new Error(`Clue ${clueId} was not deleted`);
|
||||
}
|
||||
|
||||
// If renumbering is enabled (which is always true now)
|
||||
if (shouldRenumber) {
|
||||
// Always use the sequential renumbering approach
|
||||
// This ensures QR codes are sequential regardless of DB IDs
|
||||
console.log("Renumbering all QR codes sequentially");
|
||||
|
||||
// Get all remaining clues ordered by ID
|
||||
const allClues = await query("SELECT id FROM clues ORDER BY id ASC");
|
||||
console.log(`Found ${allClues.length} clues to renumber`);
|
||||
|
||||
// Update each clue to have sequential QR[index+1] format
|
||||
for (let i = 0; i < allClues.length; i++) {
|
||||
const clue = allClues[i];
|
||||
const sequentialQrCode = `QR${i + 1}`;
|
||||
|
||||
const updateResult = await run(
|
||||
"UPDATE clues SET qr_code = ? WHERE id = ?",
|
||||
[sequentialQrCode, clue.id]
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Updated clue ${clue.id} to ${sequentialQrCode}: ${updateResult.changes} rows affected`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
await run("COMMIT");
|
||||
console.log("Transaction committed successfully");
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "Clue deleted successfully",
|
||||
renumbered: shouldRenumber,
|
||||
});
|
||||
} catch (transactionError) {
|
||||
// If any error occurs, roll back the transaction
|
||||
console.error("Error during deletion transaction:", transactionError);
|
||||
try {
|
||||
await run("ROLLBACK");
|
||||
console.log("Transaction rolled back");
|
||||
} catch (rollbackError) {
|
||||
console.error("Error while rolling back transaction:", rollbackError);
|
||||
}
|
||||
|
||||
// Provide more specific error message based on the error type
|
||||
let errorMessage = "Failed to delete clue: " + transactionError.message;
|
||||
if (transactionError.message.includes("SQLITE_CONSTRAINT")) {
|
||||
errorMessage =
|
||||
"Database constraint violation. The clue may be referenced by other records.";
|
||||
}
|
||||
|
||||
return Response.json({ error: errorMessage }, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Admin clue deletion error:", error);
|
||||
return Response.json(
|
||||
@@ -83,16 +164,17 @@ export async function GET(request, context) {
|
||||
return Response.json({ error }, { status });
|
||||
}
|
||||
|
||||
const { clueId } = context.params;
|
||||
// Get clueId parameter - properly awaited
|
||||
const { clueId } = await context.params;
|
||||
|
||||
// Validate clueId is a number
|
||||
if (isNaN(parseInt(clueId))) {
|
||||
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the specific clue - removed coordinates
|
||||
// Get the specific clue - now including qr_code
|
||||
const clue = await queryOne(
|
||||
"SELECT id, title, description, location FROM clues WHERE id = ?",
|
||||
"SELECT id, title, description, location, qr_code FROM clues WHERE id = ?",
|
||||
[clueId]
|
||||
);
|
||||
|
||||
@@ -119,15 +201,16 @@ export async function PUT(request, context) {
|
||||
return Response.json({ error }, { status });
|
||||
}
|
||||
|
||||
const { clueId } = context.params;
|
||||
// Get clueId parameter - properly awaited
|
||||
const { clueId } = await context.params;
|
||||
|
||||
// Validate clueId is a number
|
||||
if (isNaN(parseInt(clueId))) {
|
||||
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Parse the request body - removed coordinates
|
||||
const { title, description, location } = await request.json();
|
||||
// Parse the request body
|
||||
const { title, description, location, qr_code } = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!title || !description) {
|
||||
@@ -137,20 +220,33 @@ export async function PUT(request, context) {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate QR code format if provided
|
||||
if (qr_code && !qr_code.match(/^QR\d+$/i)) {
|
||||
return Response.json(
|
||||
{ error: "QR code must follow the format QR[number], e.g. QR1, QR2" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if clue exists
|
||||
const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
|
||||
const clue = await queryOne("SELECT id, qr_code FROM clues WHERE id = ?", [
|
||||
clueId,
|
||||
]);
|
||||
if (!clue) {
|
||||
return Response.json({ error: "Clue not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Update the clue - removed coordinates
|
||||
// Allow custom QR code - no longer enforcing QR{id} format
|
||||
const finalQrCode = qr_code || `QR${clueId}`;
|
||||
|
||||
// Update the clue with the provided or default QR code
|
||||
await run(
|
||||
`
|
||||
UPDATE clues
|
||||
SET title = ?, description = ?, location = ?
|
||||
SET title = ?, description = ?, location = ?, qr_code = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, description, location || "", clueId]
|
||||
[title, description, location || "", finalQrCode, clueId]
|
||||
);
|
||||
|
||||
return Response.json({
|
||||
|
||||
+122
-19
@@ -39,13 +39,14 @@ export async function GET(request) {
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
location TEXT,
|
||||
qr_code TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Fetch all clues with sequential ordering
|
||||
// Fetch all clues with sequential ordering - IMPORTANT: explicitly include qr_code field
|
||||
const clues = await query(`
|
||||
SELECT id, title, description, location, created_at
|
||||
SELECT id, title, description, location, qr_code, created_at
|
||||
FROM clues
|
||||
ORDER BY id ASC
|
||||
`);
|
||||
@@ -77,34 +78,136 @@ export async function POST(request) {
|
||||
return Response.json({ error }, { status });
|
||||
}
|
||||
|
||||
// Parse the request body - simplified without coordinates
|
||||
const { title, description, location = "" } = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!title || !description) {
|
||||
// Parse the request body - now accepting qr_code
|
||||
let bodyData;
|
||||
try {
|
||||
bodyData = await request.json();
|
||||
} catch (parseError) {
|
||||
console.error("Error parsing JSON request body:", parseError);
|
||||
return Response.json(
|
||||
{ error: "Title and description are required" },
|
||||
{ error: "Invalid JSON in request body" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Insert the new clue - simplified without coordinates
|
||||
await run(
|
||||
`
|
||||
INSERT INTO clues (title, description, location)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
[title, description, location]
|
||||
);
|
||||
const { title, description, location = "", qr_code = "" } = bodyData;
|
||||
|
||||
// Get the inserted clue ID
|
||||
const newClue = await queryOne("SELECT last_insert_rowid() as id");
|
||||
// Add more detailed validation
|
||||
if (!title) {
|
||||
return Response.json({ error: "Title is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
return Response.json(
|
||||
{ error: "Description is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate QR code format if provided
|
||||
if (qr_code && !qr_code.match(/^QR\d+$/i)) {
|
||||
return Response.json(
|
||||
{ error: "QR code must follow the format QR[number], e.g. QR1, QR2" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Make sure the clues table exists before inserting
|
||||
try {
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS clues (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
location TEXT,
|
||||
qr_code TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Check if we need to add the qr_code column (for existing tables that don't have it)
|
||||
try {
|
||||
// First check if the column exists
|
||||
const tableInfo = await query(`PRAGMA table_info(clues)`);
|
||||
const qrCodeColumnExists = tableInfo.some(
|
||||
(col) => col.name === "qr_code"
|
||||
);
|
||||
|
||||
if (!qrCodeColumnExists) {
|
||||
// If the column doesn't exist, add it with a default value
|
||||
await run(`ALTER TABLE clues ADD COLUMN qr_code TEXT DEFAULT ''`);
|
||||
console.log("Added qr_code column to clues table");
|
||||
}
|
||||
} catch (alterTableError) {
|
||||
console.error(
|
||||
"Error checking/modifying table structure:",
|
||||
alterTableError
|
||||
);
|
||||
// Continue anyway - the insertion will either work or fail
|
||||
}
|
||||
} catch (tableError) {
|
||||
console.error("Error ensuring table exists:", tableError);
|
||||
return Response.json(
|
||||
{ error: "Database error: Could not ensure clues table exists" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Insert the new clue with QR code if provided
|
||||
let result;
|
||||
try {
|
||||
if (qr_code) {
|
||||
// If QR code is provided, use it
|
||||
result = await run(
|
||||
`INSERT INTO clues (title, description, location, qr_code) VALUES (?, ?, ?, ?)`,
|
||||
[title, description, location, qr_code]
|
||||
);
|
||||
} else {
|
||||
// Otherwise, just insert without QR code (will be set later)
|
||||
result = await run(
|
||||
`INSERT INTO clues (title, description, location) VALUES (?, ?, ?)`,
|
||||
[title, description, location]
|
||||
);
|
||||
}
|
||||
} catch (insertError) {
|
||||
console.error("Database insert error:", insertError);
|
||||
return Response.json(
|
||||
{ error: `Database error: ${insertError.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get the inserted clue ID and update QR codes
|
||||
let newClueId;
|
||||
try {
|
||||
const newClue = await queryOne("SELECT last_insert_rowid() as id");
|
||||
newClueId = newClue?.id;
|
||||
|
||||
// If no QR code was provided, update all QR codes to ensure sequential order
|
||||
if (!qr_code && newClueId) {
|
||||
// Update all clues to have sequential QR codes
|
||||
const allClues = await query("SELECT id FROM clues ORDER BY id ASC");
|
||||
|
||||
// Update each clue to have QR[index+1] format (sequential without gaps)
|
||||
for (let i = 0; i < allClues.length; i++) {
|
||||
const clue = allClues[i];
|
||||
const sequentialQrCode = `QR${i + 1}`;
|
||||
await run("UPDATE clues SET qr_code = ? WHERE id = ?", [
|
||||
sequentialQrCode,
|
||||
clue.id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (updateError) {
|
||||
console.error("Error updating QR codes:", updateError);
|
||||
// Continue anyway, the clue was inserted successfully
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Clue added successfully",
|
||||
clueId: newClue.id,
|
||||
clueId: newClueId || null,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { query, queryOne, run } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic"; // Disable caching
|
||||
|
||||
// Ensure the user is an admin
|
||||
async function verifyAdmin(request) {
|
||||
const token = await getToken({
|
||||
req: request,
|
||||
secret:
|
||||
process.env.NEXTAUTH_SECRET ||
|
||||
"your-fallback-secret-should-be-at-least-32-chars",
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return { authorized: false, error: "Unauthorized", status: 401 };
|
||||
}
|
||||
|
||||
if (token.role !== "admin") {
|
||||
return { authorized: false, error: "Admin access required", status: 403 };
|
||||
}
|
||||
|
||||
return { authorized: true, token };
|
||||
}
|
||||
|
||||
// POST endpoint to fix database schema issues
|
||||
export async function POST(request) {
|
||||
try {
|
||||
// Verify admin access
|
||||
const { authorized, error, status } = await verifyAdmin(request);
|
||||
if (!authorized) {
|
||||
return Response.json({ error }, { status });
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
operations: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
// Get current table structure
|
||||
const tableInfo = await query("PRAGMA table_info(clues)");
|
||||
console.log("Current clues table schema:", tableInfo);
|
||||
|
||||
// Check if qr_code column exists and its constraints
|
||||
const qrCodeColumn = tableInfo.find((col) => col.name === "qr_code");
|
||||
|
||||
if (!qrCodeColumn) {
|
||||
// QR code column doesn't exist, add it with default empty string
|
||||
try {
|
||||
await run("ALTER TABLE clues ADD COLUMN qr_code TEXT DEFAULT ''");
|
||||
result.operations.push(
|
||||
"Added qr_code column with TEXT type and empty default"
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error adding qr_code column:", err);
|
||||
result.errors.push(`Failed to add qr_code column: ${err.message}`);
|
||||
}
|
||||
} else if (qrCodeColumn.notnull === 1 && !qrCodeColumn.dflt_value) {
|
||||
// Column exists but has NOT NULL constraint with no default
|
||||
// We need to recreate the table to modify constraints
|
||||
try {
|
||||
// Start transaction
|
||||
await run("BEGIN TRANSACTION");
|
||||
|
||||
// Create new table with correct schema
|
||||
await run(`
|
||||
CREATE TABLE clues_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
location TEXT,
|
||||
qr_code TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Copy data
|
||||
await run(`
|
||||
INSERT INTO clues_new (id, title, description, location, qr_code, created_at)
|
||||
SELECT id, title, description, location, COALESCE(qr_code, 'QR' || id) as qr_code, created_at
|
||||
FROM clues
|
||||
`);
|
||||
|
||||
// Drop old table
|
||||
await run("DROP TABLE clues");
|
||||
|
||||
// Rename new table to original name
|
||||
await run("ALTER TABLE clues_new RENAME TO clues");
|
||||
|
||||
// Commit transaction
|
||||
await run("COMMIT");
|
||||
|
||||
result.operations.push("Recreated clues table with proper schema");
|
||||
} catch (err) {
|
||||
// Rollback on error
|
||||
try {
|
||||
await run("ROLLBACK");
|
||||
} catch (rollbackErr) {
|
||||
console.error("Error during rollback:", rollbackErr);
|
||||
}
|
||||
|
||||
console.error("Error fixing table schema:", err);
|
||||
result.errors.push(`Failed to fix table schema: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all clues have proper QR codes
|
||||
try {
|
||||
// Update any NULL QR codes to match their ID
|
||||
await run(
|
||||
"UPDATE clues SET qr_code = 'QR' || id WHERE qr_code IS NULL OR qr_code = ''"
|
||||
);
|
||||
result.operations.push("Updated any missing QR codes to match their IDs");
|
||||
} catch (err) {
|
||||
console.error("Error updating QR codes:", err);
|
||||
result.errors.push(`Failed to update QR codes: ${err.message}`);
|
||||
}
|
||||
|
||||
// Ensure all clues have proper sequential QR codes
|
||||
try {
|
||||
// Get all clues ordered by ID
|
||||
const clues = await query("SELECT id FROM clues ORDER BY id ASC");
|
||||
|
||||
// Update each clue to have QR[index+1] format (sequential without gaps)
|
||||
for (let i = 0; i < clues.length; i++) {
|
||||
const clue = clues[i];
|
||||
const sequentialQrCode = `QR${i + 1}`;
|
||||
await run("UPDATE clues SET qr_code = ? WHERE id = ?", [
|
||||
sequentialQrCode,
|
||||
clue.id,
|
||||
]);
|
||||
}
|
||||
|
||||
result.operations.push(
|
||||
"Updated all QR codes to be sequential (QR1, QR2, etc.)"
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error updating sequential QR codes:", err);
|
||||
result.errors.push(
|
||||
`Failed to update sequential QR codes: ${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Add foreign key support for team_clues table
|
||||
try {
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS team_clues_new (
|
||||
team_id INTEGER NOT NULL,
|
||||
clue_id INTEGER NOT NULL,
|
||||
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (team_id, clue_id),
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (clue_id) REFERENCES clues(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Copy existing data if the table already exists
|
||||
try {
|
||||
await run(`
|
||||
INSERT OR IGNORE INTO team_clues_new (team_id, clue_id, found_at)
|
||||
SELECT team_id, clue_id, found_at FROM team_clues
|
||||
`);
|
||||
} catch (copyErr) {
|
||||
// If the old table doesn't exist, this is fine
|
||||
console.log("Note: No existing team_clues data to copy");
|
||||
}
|
||||
|
||||
// Drop the old table if it exists
|
||||
try {
|
||||
await run("DROP TABLE IF EXISTS team_clues");
|
||||
} catch (dropErr) {
|
||||
console.error("Error dropping old team_clues table:", dropErr);
|
||||
}
|
||||
|
||||
// Rename new table
|
||||
await run("ALTER TABLE team_clues_new RENAME TO team_clues");
|
||||
|
||||
result.operations.push("Updated team_clues table with ON DELETE CASCADE");
|
||||
} catch (err) {
|
||||
console.error("Error fixing team_clues table:", err);
|
||||
result.errors.push(`Failed to fix team_clues table: ${err.message}`);
|
||||
}
|
||||
|
||||
// Return result summary
|
||||
return Response.json({
|
||||
...result,
|
||||
message:
|
||||
result.errors.length === 0
|
||||
? "Database schema fixed successfully"
|
||||
: "Database fix completed with some errors",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Database fix error:", error);
|
||||
return Response.json(
|
||||
{ error: "Failed to fix database: " + error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { query, run } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic"; // Disable caching
|
||||
|
||||
// Ensure the user is an admin
|
||||
async function verifyAdmin(request) {
|
||||
const token = await getToken({
|
||||
req: request,
|
||||
secret:
|
||||
process.env.NEXTAUTH_SECRET ||
|
||||
"your-fallback-secret-should-be-at-least-32-chars",
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return { authorized: false, error: "Unauthorized", status: 401 };
|
||||
}
|
||||
|
||||
if (token.role !== "admin") {
|
||||
return { authorized: false, error: "Admin access required", status: 403 };
|
||||
}
|
||||
|
||||
return { authorized: true, token };
|
||||
}
|
||||
|
||||
// POST - Fix all QR codes in the database to follow the QR[id] format
|
||||
export async function POST(request) {
|
||||
try {
|
||||
// Verify admin access
|
||||
const { authorized, error, status } = await verifyAdmin(request);
|
||||
if (!authorized) {
|
||||
return Response.json({ error }, { status });
|
||||
}
|
||||
|
||||
// Get all clues ordered by ID to maintain sequence
|
||||
const clues = await query("SELECT id FROM clues ORDER BY id ASC");
|
||||
|
||||
// Count of fixed clues
|
||||
let fixedCount = 0;
|
||||
|
||||
// Start a transaction
|
||||
await run("BEGIN TRANSACTION");
|
||||
|
||||
try {
|
||||
// Assign sequential QR codes (QR1, QR2, QR3, etc.) based on array index
|
||||
// This ensures no gaps in the QR sequence regardless of DB IDs
|
||||
for (let i = 0; i < clues.length; i++) {
|
||||
const sequentialQrCode = `QR${i + 1}`;
|
||||
|
||||
// Update the clue with the sequential QR code
|
||||
await run("UPDATE clues SET qr_code = ? WHERE id = ?", [
|
||||
sequentialQrCode,
|
||||
clues[i].id,
|
||||
]);
|
||||
|
||||
fixedCount++;
|
||||
}
|
||||
|
||||
// Commit changes
|
||||
await run("COMMIT");
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: `QR codes fixed successfully. Set ${fixedCount} clues to sequential QR codes without gaps.`,
|
||||
totalClues: clues.length,
|
||||
fixedClues: fixedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
// Roll back on error
|
||||
await run("ROLLBACK");
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fix QR codes error:", error);
|
||||
return Response.json(
|
||||
{ error: "Failed to fix QR codes: " + error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,14 +41,23 @@ export async function GET(request, context) {
|
||||
return Response.json({ error: "Clue not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Get timestamp parameter to determine if this is a refresh call
|
||||
const timestamp = request.nextUrl?.searchParams?.get("t");
|
||||
const isRefresh = timestamp && !isNaN(parseInt(timestamp));
|
||||
|
||||
// Check if this team has found this clue before
|
||||
const found = await queryOne(
|
||||
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||
[token.teamId, clueId]
|
||||
);
|
||||
|
||||
// If not found yet, mark it as found now
|
||||
if (!found) {
|
||||
// Only mark as "foundNow" if:
|
||||
// 1. The clue was not found before
|
||||
// 2. This is not a refresh request (or we can't determine)
|
||||
// 3. The user is not an admin (admins don't need "foundNow" animations)
|
||||
let foundNow = false;
|
||||
|
||||
if (!found && (!isRefresh || token.role !== "admin")) {
|
||||
try {
|
||||
// First ensure the team_clues table exists
|
||||
await queryOne(`
|
||||
@@ -67,25 +76,41 @@ export async function GET(request, context) {
|
||||
"INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)",
|
||||
[token.teamId, clueId]
|
||||
);
|
||||
|
||||
// Set foundNow flag for non-admin users only
|
||||
foundNow = token.role !== "admin";
|
||||
|
||||
console.log(
|
||||
`Clue ${clueId} marked as found for team ${token.teamId}, foundNow=${foundNow}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error marking clue as found:", err);
|
||||
// Continue even if there's an error recording the find
|
||||
}
|
||||
}
|
||||
|
||||
// Add different cache headers for admins vs regular users
|
||||
const cacheHeaders =
|
||||
token.role === "admin"
|
||||
? {
|
||||
"Cache-Control": "private, max-age=10", // Allow 10 seconds of caching for admins
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
}
|
||||
: {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
};
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
...clue,
|
||||
found: !!found,
|
||||
foundNow: !found,
|
||||
found: true, // Always return as found since the user is viewing it
|
||||
foundNow, // Only true during initial discovery for non-admins
|
||||
isAdmin: token.role === "admin", // Add flag so client knows user is admin
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
},
|
||||
}
|
||||
{ headers: cacheHeaders }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching clue:", error);
|
||||
|
||||
@@ -218,19 +218,41 @@ export async function GET(request) {
|
||||
? huntActiveSetting.value === "true"
|
||||
: false;
|
||||
|
||||
// Use the actual timer status as the source of truth if it exists
|
||||
const isTimerActive = timerStatus ? !!timerStatus.active : false;
|
||||
|
||||
// Determine the actual active state by checking both sources
|
||||
// This ensures we don't miss an active timer due to inconsistent settings
|
||||
const isActuallyActive = huntActive || isTimerActive;
|
||||
|
||||
if (!timerStatus) {
|
||||
return Response.json({
|
||||
active: huntActive, // Use settings value if timer not found
|
||||
active: isActuallyActive,
|
||||
timerActive: false,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
serverTime: new Date().toISOString(),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
active: huntActive, // Return settings value for consistency
|
||||
timerActive: !!timerStatus.active, // Also return timer status for admin panel
|
||||
// Add cache-busting timestamp to ensure fresh data
|
||||
const responseData = {
|
||||
// Return consistent activity status from both sources
|
||||
active: isActuallyActive,
|
||||
timerActive: isTimerActive,
|
||||
startTime: timerStatus.start_time,
|
||||
endTime: timerStatus.end_time,
|
||||
serverTime: new Date().toISOString(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
return Response.json(responseData, {
|
||||
headers: {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error getting timer status:", error);
|
||||
|
||||
Reference in new issue
Block a user