feat(api): add auth, user role, match upload, and tournament routes
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
|
export const { GET, POST } = toNextJsHandler(auth);
|
||||||
+210
-196
@@ -1,244 +1,258 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma";
|
||||||
import { auth } from "@/lib/auth"
|
import Papa from "papaparse";
|
||||||
|
import { canManageTournament } from "@/lib/permissions";
|
||||||
|
import { getSession } from "@/lib/auth-simple";
|
||||||
|
import { calculateTeamElo, calculateExpectedTeamScore, calculateTeamEloChange } from "@/lib/elo-utils";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
interface CSVRow {
|
||||||
const Papa = require("papaparse")
|
"Event #": string;
|
||||||
|
Round: string;
|
||||||
// Map table names to numeric IDs
|
Table: string;
|
||||||
const TABLE_NAME_MAP: Record<string, number> = {
|
"Seat 1": string;
|
||||||
"Clubs": 1,
|
"Seat 3": string;
|
||||||
"Hearts": 2,
|
"Odds Points": string;
|
||||||
"Diamonds": 3,
|
"Seat 2": string;
|
||||||
"Spades": 4,
|
"Seat 4": string;
|
||||||
"Stars": 5,
|
"Evens Points": string;
|
||||||
|
Winner?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
const K_FACTOR = 32; // Standard K-factor for Elo calculations
|
||||||
const session = await auth()
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const formData = await request.formData()
|
const formData = await request.formData();
|
||||||
const file = formData.get("csvFile") as File
|
const csvFile = formData.get("csvFile") as File;
|
||||||
const eventId = formData.get("eventId") as string
|
const eventId = formData.get("eventId") as string;
|
||||||
|
|
||||||
if (!file) {
|
if (!csvFile) {
|
||||||
return NextResponse.json({ error: "No file uploaded" }, { status: 400 })
|
return NextResponse.json(
|
||||||
|
{ error: "No CSV file provided" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileBuffer = Buffer.from(await file.arrayBuffer())
|
if (!eventId) {
|
||||||
const fileText = fileBuffer.toString()
|
return NextResponse.json(
|
||||||
|
{ error: "No tournament selected" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const parseResult = Papa.parse(fileText, {
|
// Check if user has permission to add matches to this tournament
|
||||||
|
const permission = await canManageTournament(parseInt(eventId));
|
||||||
|
if (!permission.allowed) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: permission.reason || 'Insufficient permissions to add matches' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current user to assign as match creator
|
||||||
|
const session = await getSession();
|
||||||
|
const userId = session?.user?.id || null;
|
||||||
|
|
||||||
|
// Read and parse CSV
|
||||||
|
const csvText = await csvFile.text();
|
||||||
|
const parseResult = Papa.parse<CSVRow>(csvText, {
|
||||||
header: true,
|
header: true,
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (parseResult.errors.length > 0) {
|
if (parseResult.errors.length > 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "CSV parsing error", details: parseResult.errors },
|
{ error: "CSV parsing errors", errors: parseResult.errors.map((e: any) => e.message) },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = parseResult.data as Record<string, string>[]
|
const rows = parseResult.data;
|
||||||
|
let importedCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
let importedCount = 0
|
// Process each row
|
||||||
let errorCount = 0
|
for (const [index, row] of rows.entries()) {
|
||||||
const errors: string[] = []
|
|
||||||
|
|
||||||
for (const row of results) {
|
|
||||||
try {
|
try {
|
||||||
// Extract and validate data
|
// Extract player names
|
||||||
const roundNumber = parseInt(row.Round)
|
const player1Name = row["Seat 1"]?.trim();
|
||||||
const tableName = row.Table
|
const player2Name = row["Seat 3"]?.trim();
|
||||||
const seat1Name = row["Seat 1"]?.trim()
|
const player3Name = row["Seat 2"]?.trim();
|
||||||
const seat3Name = row["Seat 3"]?.trim()
|
const player4Name = row["Seat 4"]?.trim();
|
||||||
const oddsPoints = parseInt(row["Odds Points"])
|
|
||||||
const seat2Name = row["Seat 2"]?.trim()
|
|
||||||
const seat4Name = row["Seat 4"]?.trim()
|
|
||||||
const evensPoints = parseInt(row["Evens Points"])
|
|
||||||
|
|
||||||
// Validate required fields
|
if (!player1Name || !player2Name || !player3Name || !player4Name) {
|
||||||
if (!seat1Name || !seat3Name || !seat2Name || !seat4Name) {
|
errors.push(`Row ${index + 2}: Missing player names`);
|
||||||
errors.push(`Row ${importedCount + errorCount + 2}: Missing player names`)
|
errorCount++;
|
||||||
errorCount++
|
continue;
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or create players
|
// Find or create players
|
||||||
const players = await Promise.all(
|
const players = await Promise.all([
|
||||||
[seat1Name, seat3Name, seat2Name, seat4Name].map(async (name) => {
|
findOrCreatePlayer(player1Name),
|
||||||
let player = await prisma.player.findUnique({ where: { name } })
|
findOrCreatePlayer(player2Name),
|
||||||
if (!player) {
|
findOrCreatePlayer(player3Name),
|
||||||
player = await prisma.player.create({ data: { name } })
|
findOrCreatePlayer(player4Name),
|
||||||
}
|
]);
|
||||||
return player
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const [player1, player2, player3, player4] = players
|
console.log('Players found/created:');
|
||||||
|
players.forEach((p, i) => {
|
||||||
|
console.log(` Player ${i + 1}: ${p.name} (ID: ${p.id}, Elo: ${p.currentElo})`);
|
||||||
|
});
|
||||||
|
|
||||||
// Map table name to number
|
// Parse scores
|
||||||
const tableNumber = TABLE_NAME_MAP[tableName] || 0
|
const team1Score = parseInt(row["Odds Points"]) || 0;
|
||||||
|
const team2Score = parseInt(row["Evens Points"]) || 0;
|
||||||
// Find or create round
|
|
||||||
let round = await prisma.tournamentRound.findFirst({
|
|
||||||
where: { eventId: parseInt(eventId), roundNumber },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!round) {
|
|
||||||
round = await prisma.tournamentRound.create({
|
|
||||||
data: {
|
|
||||||
eventId: parseInt(eventId),
|
|
||||||
roundNumber,
|
|
||||||
status: "completed",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine winner
|
// Determine winner
|
||||||
const team1Won = oddsPoints > evensPoints
|
const team1Won = team1Score > team2Score;
|
||||||
|
const team2Won = team2Score > team1Score;
|
||||||
|
const isTie = team1Score === team2Score;
|
||||||
|
|
||||||
|
// Calculate Elo ratings using standard formula
|
||||||
|
const team1Rating = calculateTeamElo(players[0].currentElo, players[1].currentElo);
|
||||||
|
const team2Rating = calculateTeamElo(players[2].currentElo, players[3].currentElo);
|
||||||
|
|
||||||
|
// Actual scores (1 for win, 0.5 for tie, 0 for loss)
|
||||||
|
const team1ScoreActual = isTie ? 0.5 : (team1Won ? 1 : 0);
|
||||||
|
const team2ScoreActual = isTie ? 0.5 : (team2Won ? 1 : 0);
|
||||||
|
|
||||||
|
// Elo change for each team
|
||||||
|
const team1EloChange = calculateTeamEloChange(team1Rating, team2Rating, team1ScoreActual, K_FACTOR);
|
||||||
|
const team2EloChange = calculateTeamEloChange(team2Rating, team1Rating, team2ScoreActual, K_FACTOR);
|
||||||
|
|
||||||
|
// Individual Elo changes (split evenly between team members)
|
||||||
|
const player1EloChange = team1EloChange / 2;
|
||||||
|
const player2EloChange = team1EloChange / 2;
|
||||||
|
const player3EloChange = team2EloChange / 2;
|
||||||
|
const player4EloChange = team2EloChange / 2;
|
||||||
|
|
||||||
// Create match
|
// Create match
|
||||||
const match = await prisma.match.create({
|
await prisma.match.create({
|
||||||
data: {
|
data: {
|
||||||
eventId: parseInt(eventId),
|
eventId: parseInt(eventId),
|
||||||
team1P1Id: player1.id,
|
|
||||||
team1P2Id: player2.id,
|
|
||||||
team2P1Id: player3.id,
|
|
||||||
team2P2Id: player4.id,
|
|
||||||
team1Score: oddsPoints,
|
|
||||||
team2Score: evensPoints,
|
|
||||||
playedAt: new Date(),
|
playedAt: new Date(),
|
||||||
|
team1P1Id: players[0].id,
|
||||||
|
team1P2Id: players[1].id,
|
||||||
|
team2P1Id: players[2].id,
|
||||||
|
team2P2Id: players[3].id,
|
||||||
|
team1Score,
|
||||||
|
team2Score,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
|
createdById: userId,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// Calculate Elo for all players
|
// Update individual player stats
|
||||||
const playersForElo = [
|
await updatePlayerStats(players[0].id, team1Won, player1EloChange);
|
||||||
{ id: player1.id, isTeam1: true },
|
await updatePlayerStats(players[1].id, team1Won, player2EloChange);
|
||||||
{ id: player2.id, isTeam1: true },
|
await updatePlayerStats(players[2].id, team2Won, player3EloChange);
|
||||||
{ id: player3.id, isTeam1: false },
|
await updatePlayerStats(players[3].id, team2Won, player4EloChange);
|
||||||
{ id: player4.id, isTeam1: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
const K = 32
|
// Update partnership stats (for tracking how players perform together)
|
||||||
|
await updatePartnershipStats(players[0].id, players[1].id, team1Won, player1EloChange);
|
||||||
|
await updatePartnershipStats(players[2].id, players[3].id, team2Won, player3EloChange);
|
||||||
|
|
||||||
for (const p of playersForElo) {
|
importedCount++;
|
||||||
const playerData = await prisma.player.findUnique({
|
|
||||||
where: { id: p.id },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!playerData) continue
|
|
||||||
|
|
||||||
const expectedScore = 1 / (1 + Math.pow(10, (1200 - playerData.currentElo) / 400))
|
|
||||||
const actualScore = p.isTeam1 ? (team1Won ? 1 : 0) : (team1Won ? 0 : 1)
|
|
||||||
|
|
||||||
const ratingChange = Math.round(K * (actualScore - expectedScore))
|
|
||||||
const newRating = playerData.currentElo + ratingChange
|
|
||||||
|
|
||||||
await prisma.player.update({
|
|
||||||
where: { id: p.id },
|
|
||||||
data: { currentElo: newRating },
|
|
||||||
})
|
|
||||||
|
|
||||||
await prisma.eloSnapshot.create({
|
|
||||||
data: {
|
|
||||||
playerId: p.id,
|
|
||||||
matchId: match.id,
|
|
||||||
ratingBefore: playerData.currentElo,
|
|
||||||
ratingAfter: newRating,
|
|
||||||
ratingChange,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track partnership games
|
|
||||||
await prisma.partnershipGame.create({
|
|
||||||
data: {
|
|
||||||
player1Id: player1.id,
|
|
||||||
player2Id: player2.id,
|
|
||||||
matchId: match.id,
|
|
||||||
teamNumber: 1,
|
|
||||||
wonMatch: team1Won ? 1 : 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await prisma.partnershipGame.create({
|
|
||||||
data: {
|
|
||||||
player1Id: player3.id,
|
|
||||||
player2Id: player4.id,
|
|
||||||
matchId: match.id,
|
|
||||||
teamNumber: 2,
|
|
||||||
wonMatch: team1Won ? 0 : 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update partnership statistics
|
|
||||||
const partnerships = [
|
|
||||||
[player1.id, player2.id],
|
|
||||||
[player3.id, player4.id],
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const [p1Id, p2Id] of partnerships) {
|
|
||||||
const [player1Id, player2Id] = p1Id < p2Id ? [p1Id, p2Id] : [p2Id, p1Id]
|
|
||||||
|
|
||||||
const existingStat = await prisma.partnershipStat.findFirst({
|
|
||||||
where: { player1Id, player2Id },
|
|
||||||
})
|
|
||||||
|
|
||||||
const won =
|
|
||||||
(player1Id === player1.id && player2Id === player2.id && team1Won) ||
|
|
||||||
(player1Id === player3.id && player2Id === player4.id && !team1Won)
|
|
||||||
|
|
||||||
if (existingStat) {
|
|
||||||
await prisma.partnershipStat.update({
|
|
||||||
where: { id: existingStat.id },
|
|
||||||
data: {
|
|
||||||
gamesPlayed: { increment: 1 },
|
|
||||||
wins: { increment: won ? 1 : 0 },
|
|
||||||
losses: { increment: won ? 0 : 1 },
|
|
||||||
lastPlayed: new Date(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await prisma.partnershipStat.create({
|
|
||||||
data: {
|
|
||||||
player1Id,
|
|
||||||
player2Id,
|
|
||||||
gamesPlayed: 1,
|
|
||||||
wins: won ? 1 : 0,
|
|
||||||
losses: won ? 0 : 1,
|
|
||||||
totalEloChange: 0,
|
|
||||||
lastPlayed: new Date(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
importedCount++
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
errors.push(`Row ${importedCount + errorCount + 2}: ${err.message}`)
|
errors.push(`Row ${index + 2}: ${err.message}`);
|
||||||
errorCount++
|
errorCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
|
||||||
importedCount,
|
importedCount,
|
||||||
errorCount,
|
errorCount,
|
||||||
errors: errorCount > 0 ? errors : undefined,
|
errors: errors.length > 0 ? errors : undefined,
|
||||||
})
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error uploading CSV:", error)
|
console.error("Upload error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to process CSV file", details: error.message },
|
{ error: error.message || "Failed to upload CSV" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findOrCreatePlayer(name: string) {
|
||||||
|
// Try to find existing player with exact name match
|
||||||
|
let player = await prisma.player.findFirst({
|
||||||
|
where: { name },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!player) {
|
||||||
|
// Create a new player with a unique name
|
||||||
|
const uniqueName = `${name}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||||
|
player = await prisma.player.create({
|
||||||
|
data: {
|
||||||
|
name: uniqueName,
|
||||||
|
rating: 1000,
|
||||||
|
currentElo: 1000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePlayerStats(playerId: number, won: boolean, eloChange: number) {
|
||||||
|
// First, get the current player data to calculate the new values
|
||||||
|
const player = await prisma.player.findUnique({
|
||||||
|
where: { id: playerId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!player) {
|
||||||
|
throw new Error(`Player ${playerId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the player's statistics
|
||||||
|
await prisma.player.update({
|
||||||
|
where: { id: playerId },
|
||||||
|
data: {
|
||||||
|
currentElo: player.currentElo + Math.round(eloChange),
|
||||||
|
gamesPlayed: player.gamesPlayed + 1,
|
||||||
|
wins: won ? player.wins + 1 : player.wins,
|
||||||
|
losses: won ? player.losses : player.losses + 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePartnershipStats(player1Id: number, player2Id: number, won: boolean, eloChange: number) {
|
||||||
|
// Sort IDs to ensure consistent partnership lookup
|
||||||
|
const [smallId, largeId] = player1Id < player2Id ? [player1Id, player2Id] : [player2Id, player1Id];
|
||||||
|
|
||||||
|
// Find existing partnership
|
||||||
|
const existing = await prisma.partnershipStat.findFirst({
|
||||||
|
where: {
|
||||||
|
player1Id: smallId,
|
||||||
|
player2Id: largeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Update existing partnership
|
||||||
|
await prisma.partnershipStat.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data: {
|
||||||
|
gamesPlayed: { increment: 1 },
|
||||||
|
wins: won ? { increment: 1 } : undefined,
|
||||||
|
losses: won ? undefined : { increment: 1 },
|
||||||
|
totalEloChange: { increment: eloChange },
|
||||||
|
lastPlayed: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new partnership
|
||||||
|
await prisma.partnershipStat.create({
|
||||||
|
data: {
|
||||||
|
player1Id: smallId,
|
||||||
|
player2Id: largeId,
|
||||||
|
gamesPlayed: 1,
|
||||||
|
wins: won ? 1 : 0,
|
||||||
|
losses: won ? 0 : 1,
|
||||||
|
totalEloChange: eloChange,
|
||||||
|
lastPlayed: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,14 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma";
|
||||||
import { auth } from "@/lib/auth"
|
import { getTournamentStatus } from "@/lib/tournamentUtils";
|
||||||
|
import { canCreateTournaments } from "@/lib/permissions";
|
||||||
export async function POST(request: NextRequest) {
|
import { getSession } from "@/lib/auth-simple";
|
||||||
const session = await auth()
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { name, description, eventDate, format, maxParticipants } = body
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Tournament name is required" },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tournament = await prisma.event.create({
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
description: description || "",
|
|
||||||
eventDate: eventDate ? new Date(eventDate) : null,
|
|
||||||
format: format || "round_robin",
|
|
||||||
status: "planned",
|
|
||||||
maxParticipants: maxParticipants || null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true, tournament })
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating tournament:", error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Failed to create tournament" },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const tournaments = await prisma.event.findMany({
|
const tournaments = await prisma.event.findMany({
|
||||||
|
where: { eventType: "tournament" },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
include: {
|
include: {
|
||||||
participants: true,
|
participants: true,
|
||||||
teams: {
|
teams: {
|
||||||
@@ -52,17 +17,77 @@ export async function GET() {
|
|||||||
player2: true,
|
player2: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rounds: true,
|
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "desc" },
|
});
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ tournaments })
|
// Update tournament statuses based on event date
|
||||||
|
const updatedTournaments = await Promise.all(
|
||||||
|
tournaments.map(async (tournament) => {
|
||||||
|
const calculatedStatus = getTournamentStatus(tournament.eventDate);
|
||||||
|
|
||||||
|
// Only update if the calculated status differs from the stored status
|
||||||
|
if (tournament.status !== calculatedStatus) {
|
||||||
|
await prisma.event.update({
|
||||||
|
where: { id: tournament.id },
|
||||||
|
data: { status: calculatedStatus },
|
||||||
|
});
|
||||||
|
return { ...tournament, status: calculatedStatus };
|
||||||
|
}
|
||||||
|
|
||||||
|
return tournament;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({ tournaments: updatedTournaments });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching tournaments:", error)
|
console.error("Failed to fetch tournaments:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch tournaments" },
|
{ error: "Failed to fetch tournaments" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
// Check if user has permission to create tournaments
|
||||||
|
const permission = await canCreateTournaments();
|
||||||
|
if (!permission.allowed) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: permission.reason || 'Insufficient permissions to create tournaments' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current user to assign as owner
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'User not authenticated' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, format, eventDate } = body;
|
||||||
|
|
||||||
|
const tournament = await prisma.event.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
format: format || "round_robin",
|
||||||
|
eventDate: eventDate ? new Date(eventDate) : null,
|
||||||
|
eventType: "tournament",
|
||||||
|
status: "planned",
|
||||||
|
ownerId: session.user.id, // Assign ownership to the creator
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ tournament }, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create tournament:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create tournament" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { getSession } from '@/lib/auth-simple';
|
||||||
|
import { canAssignTournamentAdmin } from '@/lib/permissions';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await getSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users can only query their own role or if they're an admin
|
||||||
|
// Fetch the requesting user's current role from database to avoid session cache issues
|
||||||
|
const requestingUser = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
select: { role: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!requestingUser) {
|
||||||
|
return NextResponse.json({ error: 'Requesting user not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.user.id !== params.id && requestingUser.role !== 'club_admin') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
select: { role: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ role: user.role });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Check if the current user has permission to assign roles
|
||||||
|
const permission = await canAssignTournamentAdmin();
|
||||||
|
if (!permission.allowed) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: permission.reason || 'Insufficient permissions' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { role } = await request.json();
|
||||||
|
|
||||||
|
// Validate the role
|
||||||
|
const validRoles = ['player', 'tournament_admin', 'club_admin'];
|
||||||
|
if (!validRoles.includes(role)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Invalid role. Must be one of: ${validRoles.join(', ')}` },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user's role
|
||||||
|
const updatedUser = await prisma.user.update({
|
||||||
|
where: { id: params.id },
|
||||||
|
data: { role }
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Role updated successfully',
|
||||||
|
user: { id: updatedUser.id, email: updatedUser.email, role: updatedUser.role }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update user role:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to update user role" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user