Implementiere Kurator-Kommentar-System

- Benutzer können nach Rätsel-Abschluss optional Nachricht an Kuratoren senden
- Kommentare werden in Datenbank gespeichert und in /curator angezeigt
- Neue Datenbank-Modelle: CuratorComment und CuratorCommentRecipient
- API-Routen für Kommentar-Versand, Abfrage und Markierung als gelesen
- Rate-Limiting: 1 Kommentar pro Spieler pro Rätsel (persistent in DB)
- Sicherheitsschutz: PlayerIdentifier-Validierung, Puzzle-Validierung
- Automatische Zuordnung zu Kuratoren (Genre-basiert + globale Kuratoren)
- Frontend: Kommentar-Formular in Game-Komponente
- Frontend: Kommentare-Anzeige in Kuratoren-Seite mit Markierung als gelesen
- Übersetzungen für DE und EN hinzugefügt
This commit is contained in:
Hördle Bot
2025-12-03 22:46:02 +01:00
parent 863539a5e9
commit cd564b5d8c
9 changed files with 740 additions and 2 deletions

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { requireStaffAuth } from '@/lib/auth';
const prisma = new PrismaClient();
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
// Require curator authentication
const { error, context } = await requireStaffAuth(request);
if (error || !context) {
return error!;
}
// Only curators can mark comments as read
if (context.role !== 'curator') {
return NextResponse.json(
{ error: 'Only curators can mark comments as read' },
{ status: 403 }
);
}
try {
const { id } = await params;
const commentId = Number(id);
const curatorId = context.curator.id;
// Verify that this comment belongs to this curator
const recipient = await prisma.curatorCommentRecipient.findUnique({
where: {
commentId_curatorId: {
commentId: commentId,
curatorId: curatorId
}
}
});
if (!recipient) {
return NextResponse.json(
{ error: 'Comment not found or access denied' },
{ status: 404 }
);
}
// Update readAt timestamp
await prisma.curatorCommentRecipient.update({
where: {
id: recipient.id
},
data: {
readAt: new Date()
}
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error marking comment as read:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { requireStaffAuth } from '@/lib/auth';
const prisma = new PrismaClient();
export async function GET(request: NextRequest) {
// Require curator authentication
const { error, context } = await requireStaffAuth(request);
if (error || !context) {
return error!;
}
// Only curators can view comments (not admins directly)
if (context.role !== 'curator') {
return NextResponse.json(
{ error: 'Only curators can view comments' },
{ status: 403 }
);
}
try {
const curatorId = context.curator.id;
// Get all comments for this curator, ordered by creation date (newest first)
const comments = await prisma.curatorCommentRecipient.findMany({
where: {
curatorId: curatorId
},
include: {
comment: {
include: {
puzzle: {
include: {
song: {
select: {
title: true,
artist: true
}
},
genre: {
select: {
id: true,
name: true
}
}
}
}
}
}
},
orderBy: {
comment: {
createdAt: 'desc'
}
}
});
// Format the response
const formattedComments = comments.map(recipient => ({
id: recipient.comment.id,
message: recipient.comment.message,
createdAt: recipient.comment.createdAt,
readAt: recipient.readAt,
puzzle: {
id: recipient.comment.puzzle.id,
date: recipient.comment.puzzle.date,
song: {
title: recipient.comment.puzzle.song.title,
artist: recipient.comment.puzzle.song.artist
},
genre: recipient.comment.puzzle.genre ? {
id: recipient.comment.puzzle.genre.id,
name: recipient.comment.puzzle.genre.name
} : null
}
}));
return NextResponse.json(formattedComments);
} catch (error) {
console.error('Error fetching curator comments:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}