- Add PlayerState model to database schema for storing game states - Create player identifier system (UUID-based) for cross-domain sync - Implement API endpoints for loading/saving player states - Refactor gameState hook to use backend storage with localStorage fallback - Support synchronization between hoerdle.de and hördle.de - Migration automatically runs on Docker container start
173 lines
5.1 KiB
TypeScript
173 lines
5.1 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import { getLocalizedValue } from '@/lib/i18n';
|
|
import type { GameState, Statistics } from '@/lib/gameState';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
/**
|
|
* Validate UUID format (basic check)
|
|
*/
|
|
function isValidUUID(uuid: string): boolean {
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
return uuidRegex.test(uuid);
|
|
}
|
|
|
|
/**
|
|
* GET /api/player-state
|
|
*
|
|
* Loads player state for a given identifier and genre/special.
|
|
*
|
|
* Query parameters:
|
|
* - genre: Genre name (e.g., "Rock")
|
|
* - special: Special name (e.g., "00725")
|
|
*
|
|
* Headers:
|
|
* - X-Player-Id: Player identifier (UUID)
|
|
*/
|
|
export async function GET(request: Request) {
|
|
try {
|
|
const { searchParams } = new URL(request.url);
|
|
const genreName = searchParams.get('genre');
|
|
const specialName = searchParams.get('special');
|
|
|
|
// Get player identifier from header
|
|
const playerId = request.headers.get('X-Player-Id');
|
|
if (!playerId || !isValidUUID(playerId)) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid or missing player identifier' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Determine genre key
|
|
let genreKey: string;
|
|
if (specialName) {
|
|
genreKey = `special:${specialName}`;
|
|
} else if (genreName) {
|
|
genreKey = genreName;
|
|
} else {
|
|
genreKey = 'global';
|
|
}
|
|
|
|
// Load player state from database
|
|
const playerState = await prisma.playerState.findUnique({
|
|
where: {
|
|
identifier_genreKey: {
|
|
identifier: playerId,
|
|
genreKey: genreKey,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!playerState) {
|
|
return NextResponse.json(null, { status: 404 });
|
|
}
|
|
|
|
// Parse JSON strings
|
|
let gameState: GameState;
|
|
let statistics: Statistics;
|
|
|
|
try {
|
|
gameState = JSON.parse(playerState.gameState) as GameState;
|
|
statistics = JSON.parse(playerState.statistics) as Statistics;
|
|
} catch (parseError) {
|
|
console.error('[player-state] Failed to parse stored state:', parseError);
|
|
return NextResponse.json(
|
|
{ error: 'Invalid stored state format' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({
|
|
gameState,
|
|
statistics,
|
|
});
|
|
} catch (error) {
|
|
console.error('[player-state] Error loading player state:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Internal Server Error' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/player-state
|
|
*
|
|
* Saves player state for a given identifier and genre/special.
|
|
*
|
|
* Request body:
|
|
* - genreKey: Genre key (e.g., "global", "Rock", "special:00725")
|
|
* - gameState: GameState object
|
|
* - statistics: Statistics object
|
|
*
|
|
* Headers:
|
|
* - X-Player-Id: Player identifier (UUID)
|
|
*/
|
|
export async function POST(request: Request) {
|
|
try {
|
|
// Get player identifier from header
|
|
const playerId = request.headers.get('X-Player-Id');
|
|
if (!playerId || !isValidUUID(playerId)) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid or missing player identifier' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Parse request body
|
|
const body = await request.json();
|
|
const { genreKey, gameState, statistics } = body;
|
|
|
|
if (!genreKey || !gameState || !statistics) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing required fields: genreKey, gameState, statistics' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate genre key format
|
|
if (typeof genreKey !== 'string' || genreKey.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid genreKey format' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Serialize to JSON strings
|
|
const gameStateJson = JSON.stringify(gameState);
|
|
const statisticsJson = JSON.stringify(statistics);
|
|
|
|
// Upsert player state (update if exists, create if not)
|
|
await prisma.playerState.upsert({
|
|
where: {
|
|
identifier_genreKey: {
|
|
identifier: playerId,
|
|
genreKey: genreKey,
|
|
},
|
|
},
|
|
update: {
|
|
gameState: gameStateJson,
|
|
statistics: statisticsJson,
|
|
lastPlayed: new Date(),
|
|
},
|
|
create: {
|
|
identifier: playerId,
|
|
genreKey: genreKey,
|
|
gameState: gameStateJson,
|
|
statistics: statisticsJson,
|
|
},
|
|
});
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
console.error('[player-state] Error saving player state:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Internal Server Error' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|