feat: add API routes for moderation and invitations
Add REST API endpoints for: - POST/DELETE /ban: Ban/unban users (with auto-invite on unban) - POST /kick: Remove users from room temporarily - POST /report: Submit player misconduct reports - GET /reports: Retrieve pending reports for room hosts - GET /history: Get all historical room members with statuses - POST /invite: Send room invitations - GET /invitations/pending: Get user's pending invitations All endpoints include proper authentication, validation, and real-time socket notifications for affected users. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
44
apps/web/src/app/api/arcade/invitations/pending/route.ts
Normal file
44
apps/web/src/app/api/arcade/invitations/pending/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { db, schema } from '@/db'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
|
||||
/**
|
||||
* GET /api/arcade/invitations/pending
|
||||
* Get all pending invitations for the current user with room details
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const viewerId = await getViewerId()
|
||||
|
||||
// Get pending invitations with room details
|
||||
const invitations = await db
|
||||
.select({
|
||||
id: schema.roomInvitations.id,
|
||||
roomId: schema.roomInvitations.roomId,
|
||||
roomName: schema.arcadeRooms.name,
|
||||
roomGameName: schema.arcadeRooms.gameName,
|
||||
userId: schema.roomInvitations.userId,
|
||||
userName: schema.roomInvitations.userName,
|
||||
invitedBy: schema.roomInvitations.invitedBy,
|
||||
invitedByName: schema.roomInvitations.invitedByName,
|
||||
status: schema.roomInvitations.status,
|
||||
invitationType: schema.roomInvitations.invitationType,
|
||||
message: schema.roomInvitations.message,
|
||||
createdAt: schema.roomInvitations.createdAt,
|
||||
expiresAt: schema.roomInvitations.expiresAt,
|
||||
})
|
||||
.from(schema.roomInvitations)
|
||||
.innerJoin(schema.arcadeRooms, eq(schema.roomInvitations.roomId, schema.arcadeRooms.id))
|
||||
.where(eq(schema.roomInvitations.userId, viewerId))
|
||||
.orderBy(schema.roomInvitations.createdAt)
|
||||
|
||||
// Filter to only pending invitations
|
||||
const pendingInvitations = invitations.filter((inv) => inv.status === 'pending')
|
||||
|
||||
return NextResponse.json({ invitations: pendingInvitations }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get pending invitations:', error)
|
||||
return NextResponse.json({ error: 'Failed to get pending invitations' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
223
apps/web/src/app/api/arcade/rooms/[roomId]/ban/route.ts
Normal file
223
apps/web/src/app/api/arcade/rooms/[roomId]/ban/route.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { banUserFromRoom, getRoomBans, unbanUserFromRoom } from '@/lib/arcade/room-moderation'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getRoomActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { getUserRoomHistory } from '@/lib/arcade/room-member-history'
|
||||
import { createInvitation } from '@/lib/arcade/room-invitations'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/arcade/rooms/:roomId/ban
|
||||
* Ban a user from the room (host only)
|
||||
* Body:
|
||||
* - userId: string
|
||||
* - reason: string (enum)
|
||||
* - notes?: string (optional)
|
||||
*/
|
||||
export async function POST(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.userId || !body.reason) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: userId, reason' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate reason
|
||||
const validReasons = ['harassment', 'cheating', 'inappropriate-name', 'spam', 'afk', 'other']
|
||||
if (!validReasons.includes(body.reason)) {
|
||||
return NextResponse.json({ error: 'Invalid reason' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can ban users' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Can't ban yourself
|
||||
if (body.userId === viewerId) {
|
||||
return NextResponse.json({ error: 'Cannot ban yourself' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get the user to ban (they might not be in the room anymore)
|
||||
const targetUser = members.find((m) => m.userId === body.userId)
|
||||
const userName = targetUser?.displayName || body.userId.slice(-4)
|
||||
|
||||
// Ban the user
|
||||
await banUserFromRoom({
|
||||
roomId,
|
||||
userId: body.userId,
|
||||
userName,
|
||||
bannedBy: viewerId,
|
||||
bannedByName: currentMember.displayName,
|
||||
reason: body.reason,
|
||||
notes: body.notes,
|
||||
})
|
||||
|
||||
// Broadcast updates via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
// Get updated member list
|
||||
const updatedMembers = await getRoomMembers(roomId)
|
||||
const memberPlayers = await getRoomActivePlayers(roomId)
|
||||
|
||||
// Convert memberPlayers Map to object for JSON serialization
|
||||
const memberPlayersObj: Record<string, any[]> = {}
|
||||
for (const [uid, players] of memberPlayers.entries()) {
|
||||
memberPlayersObj[uid] = players
|
||||
}
|
||||
|
||||
// Tell the banned user they've been removed
|
||||
io.to(`user:${body.userId}`).emit('banned-from-room', {
|
||||
roomId,
|
||||
bannedBy: currentMember.displayName,
|
||||
reason: body.reason,
|
||||
})
|
||||
|
||||
// Notify everyone else in the room
|
||||
io.to(`room:${roomId}`).emit('member-left', {
|
||||
roomId,
|
||||
userId: body.userId,
|
||||
members: updatedMembers,
|
||||
memberPlayers: memberPlayersObj,
|
||||
reason: 'banned',
|
||||
})
|
||||
|
||||
console.log(`[Ban API] User ${body.userId} banned from room ${roomId}`)
|
||||
} catch (socketError) {
|
||||
console.error('[Ban API] Failed to broadcast ban:', socketError)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to ban user:', error)
|
||||
return NextResponse.json({ error: 'Failed to ban user' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/arcade/rooms/:roomId/ban
|
||||
* Unban a user from the room (host only)
|
||||
* Body:
|
||||
* - userId: string
|
||||
*/
|
||||
export async function DELETE(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.userId) {
|
||||
return NextResponse.json({ error: 'Missing required field: userId' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can unban users' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Unban the user
|
||||
await unbanUserFromRoom(roomId, body.userId)
|
||||
|
||||
// Auto-invite the unbanned user back to the room
|
||||
const history = await getUserRoomHistory(roomId, body.userId)
|
||||
if (history) {
|
||||
const invitation = await createInvitation({
|
||||
roomId,
|
||||
userId: body.userId,
|
||||
userName: history.displayName,
|
||||
invitedBy: viewerId,
|
||||
invitedByName: currentMember.displayName,
|
||||
invitationType: 'auto-unban',
|
||||
message: 'You have been unbanned and are welcome to rejoin.',
|
||||
})
|
||||
|
||||
// Broadcast invitation via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
io.to(`user:${body.userId}`).emit('room-invitation-received', {
|
||||
invitation: {
|
||||
id: invitation.id,
|
||||
roomId: invitation.roomId,
|
||||
invitedBy: invitation.invitedBy,
|
||||
invitedByName: invitation.invitedByName,
|
||||
message: invitation.message,
|
||||
createdAt: invitation.createdAt,
|
||||
invitationType: 'auto-unban',
|
||||
},
|
||||
})
|
||||
|
||||
console.log(
|
||||
`[Unban API] Auto-invited user ${body.userId} after unban from room ${roomId}`
|
||||
)
|
||||
} catch (socketError) {
|
||||
console.error('[Unban API] Failed to broadcast invitation:', socketError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to unban user:', error)
|
||||
return NextResponse.json({ error: 'Failed to unban user' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/arcade/rooms/:roomId/ban
|
||||
* Get all bans for a room (host only)
|
||||
*/
|
||||
export async function GET(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can view bans' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Get all bans
|
||||
const bans = await getRoomBans(roomId)
|
||||
|
||||
return NextResponse.json({ bans }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get bans:', error)
|
||||
return NextResponse.json({ error: 'Failed to get bans' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
40
apps/web/src/app/api/arcade/rooms/[roomId]/history/route.ts
Normal file
40
apps/web/src/app/api/arcade/rooms/[roomId]/history/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getRoomHistoricalMembersWithStatus } from '@/lib/arcade/room-member-history'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/arcade/rooms/:roomId/history
|
||||
* Get all historical members with their current status (host only)
|
||||
* Returns: array of historical members with status info
|
||||
*/
|
||||
export async function GET(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can view room history' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Get all historical members with status
|
||||
const historicalMembers = await getRoomHistoricalMembersWithStatus(roomId)
|
||||
|
||||
return NextResponse.json({ historicalMembers }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get room history:', error)
|
||||
return NextResponse.json({ error: 'Failed to get room history' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
125
apps/web/src/app/api/arcade/rooms/[roomId]/invite/route.ts
Normal file
125
apps/web/src/app/api/arcade/rooms/[roomId]/invite/route.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { createInvitation, getRoomInvitations } from '@/lib/arcade/room-invitations'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/arcade/rooms/:roomId/invite
|
||||
* Send an invitation to a user (host only)
|
||||
* Body:
|
||||
* - userId: string
|
||||
* - userName: string
|
||||
* - message?: string (optional)
|
||||
*/
|
||||
export async function POST(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.userId || !body.userName) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: userId, userName' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can send invitations' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Can't invite yourself
|
||||
if (body.userId === viewerId) {
|
||||
return NextResponse.json({ error: 'Cannot invite yourself' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Can't invite someone who's already in the room
|
||||
const targetUser = members.find((m) => m.userId === body.userId)
|
||||
if (targetUser) {
|
||||
return NextResponse.json({ error: 'User is already in this room' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create invitation
|
||||
const invitation = await createInvitation({
|
||||
roomId,
|
||||
userId: body.userId,
|
||||
userName: body.userName,
|
||||
invitedBy: viewerId,
|
||||
invitedByName: currentMember.displayName,
|
||||
invitationType: 'manual',
|
||||
message: body.message,
|
||||
})
|
||||
|
||||
// Broadcast invitation via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
// Send to the invited user's channel
|
||||
io.to(`user:${body.userId}`).emit('room-invitation-received', {
|
||||
invitation: {
|
||||
id: invitation.id,
|
||||
roomId: invitation.roomId,
|
||||
invitedBy: invitation.invitedBy,
|
||||
invitedByName: invitation.invitedByName,
|
||||
message: invitation.message,
|
||||
createdAt: invitation.createdAt,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`[Invite API] Sent invitation to user ${body.userId} for room ${roomId}`)
|
||||
} catch (socketError) {
|
||||
console.error('[Invite API] Failed to broadcast invitation:', socketError)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ invitation }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to send invitation:', error)
|
||||
return NextResponse.json({ error: 'Failed to send invitation' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/arcade/rooms/:roomId/invite
|
||||
* Get all invitations for a room (host only)
|
||||
*/
|
||||
export async function GET(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can view invitations' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Get all invitations
|
||||
const invitations = await getRoomInvitations(roomId)
|
||||
|
||||
return NextResponse.json({ invitations }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get invitations:', error)
|
||||
return NextResponse.json({ error: 'Failed to get invitations' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
95
apps/web/src/app/api/arcade/rooms/[roomId]/kick/route.ts
Normal file
95
apps/web/src/app/api/arcade/rooms/[roomId]/kick/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { kickUserFromRoom } from '@/lib/arcade/room-moderation'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getRoomActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/arcade/rooms/:roomId/kick
|
||||
* Kick a user from the room (host only)
|
||||
* Body:
|
||||
* - userId: string
|
||||
*/
|
||||
export async function POST(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.userId) {
|
||||
return NextResponse.json({ error: 'Missing required field: userId' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can kick users' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Can't kick yourself
|
||||
if (body.userId === viewerId) {
|
||||
return NextResponse.json({ error: 'Cannot kick yourself' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify the user to kick is in the room
|
||||
const targetUser = members.find((m) => m.userId === body.userId)
|
||||
if (!targetUser) {
|
||||
return NextResponse.json({ error: 'User is not in this room' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Kick the user
|
||||
await kickUserFromRoom(roomId, body.userId)
|
||||
|
||||
// Broadcast updates via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
// Get updated member list
|
||||
const updatedMembers = await getRoomMembers(roomId)
|
||||
const memberPlayers = await getRoomActivePlayers(roomId)
|
||||
|
||||
// Convert memberPlayers Map to object for JSON serialization
|
||||
const memberPlayersObj: Record<string, any[]> = {}
|
||||
for (const [uid, players] of memberPlayers.entries()) {
|
||||
memberPlayersObj[uid] = players
|
||||
}
|
||||
|
||||
// Tell the kicked user they've been removed
|
||||
io.to(`user:${body.userId}`).emit('kicked-from-room', {
|
||||
roomId,
|
||||
kickedBy: currentMember.displayName,
|
||||
})
|
||||
|
||||
// Notify everyone else in the room
|
||||
io.to(`room:${roomId}`).emit('member-left', {
|
||||
roomId,
|
||||
userId: body.userId,
|
||||
members: updatedMembers,
|
||||
memberPlayers: memberPlayersObj,
|
||||
reason: 'kicked',
|
||||
})
|
||||
|
||||
console.log(`[Kick API] User ${body.userId} kicked from room ${roomId}`)
|
||||
} catch (socketError) {
|
||||
console.error('[Kick API] Failed to broadcast kick:', socketError)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to kick user:', error)
|
||||
return NextResponse.json({ error: 'Failed to kick user' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
97
apps/web/src/app/api/arcade/rooms/[roomId]/report/route.ts
Normal file
97
apps/web/src/app/api/arcade/rooms/[roomId]/report/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createReport } from '@/lib/arcade/room-moderation'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/arcade/rooms/:roomId/report
|
||||
* Submit a report about another player
|
||||
* Body:
|
||||
* - reportedUserId: string
|
||||
* - reason: string (enum)
|
||||
* - details?: string (optional)
|
||||
*/
|
||||
export async function POST(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.reportedUserId || !body.reason) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: reportedUserId, reason' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate reason
|
||||
const validReasons = ['harassment', 'cheating', 'inappropriate-name', 'spam', 'afk', 'other']
|
||||
if (!validReasons.includes(body.reason)) {
|
||||
return NextResponse.json({ error: 'Invalid reason' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Can't report yourself
|
||||
if (body.reportedUserId === viewerId) {
|
||||
return NextResponse.json({ error: 'Cannot report yourself' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get room members to verify both users are in the room and get names
|
||||
const members = await getRoomMembers(roomId)
|
||||
const reporter = members.find((m) => m.userId === viewerId)
|
||||
const reported = members.find((m) => m.userId === body.reportedUserId)
|
||||
|
||||
if (!reporter) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!reported) {
|
||||
return NextResponse.json({ error: 'Reported user is not in this room' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Create report
|
||||
const report = await createReport({
|
||||
roomId,
|
||||
reporterId: viewerId,
|
||||
reporterName: reporter.displayName,
|
||||
reportedUserId: body.reportedUserId,
|
||||
reportedUserName: reported.displayName,
|
||||
reason: body.reason,
|
||||
details: body.details,
|
||||
})
|
||||
|
||||
// Notify host via socket (find the host)
|
||||
const host = members.find((m) => m.isCreator)
|
||||
if (host) {
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
// Send notification only to the host
|
||||
io.to(`user:${host.userId}`).emit('report-submitted', {
|
||||
roomId,
|
||||
report: {
|
||||
id: report.id,
|
||||
reporterName: report.reporterName,
|
||||
reportedUserName: report.reportedUserName,
|
||||
reportedUserId: report.reportedUserId,
|
||||
reason: report.reason,
|
||||
createdAt: report.createdAt,
|
||||
},
|
||||
})
|
||||
} catch (socketError) {
|
||||
console.error('[Report API] Failed to notify host:', socketError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, report }, { status: 201 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to submit report:', error)
|
||||
return NextResponse.json({ error: 'Failed to submit report' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
39
apps/web/src/app/api/arcade/rooms/[roomId]/reports/route.ts
Normal file
39
apps/web/src/app/api/arcade/rooms/[roomId]/reports/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getAllReports } from '@/lib/arcade/room-moderation'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/arcade/rooms/:roomId/reports
|
||||
* Get all reports for a room (host only)
|
||||
*/
|
||||
export async function GET(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can view reports' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Get all reports
|
||||
const reports = await getAllReports(roomId)
|
||||
|
||||
return NextResponse.json({ reports }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get reports:', error)
|
||||
return NextResponse.json({ error: 'Failed to get reports' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user