fix: improve authorization error handling and add missing decline invitation endpoint
BREAKING CHANGE: Added DELETE /api/arcade/rooms/:roomId/invite endpoint for declining invitations Authorization Error Handling: - ModerationPanel: Parse and display API error messages (kick, ban, unban, invite, data loading) - PendingInvitations: Parse and display API error messages (decline, fetch) - All moderation actions now show specific auth errors like "Only the host can kick users" New Endpoint: - DELETE /api/arcade/rooms/:roomId/invite: Allow users to decline their pending invitations * Validates invitation exists and is pending * Only invited user can decline their own invitation * Returns proper error messages for auth failures Bug Fix: - Fixed invitations/pending/route.ts ban check query (removed reference to non-existent unbannedAt field) - Ban records are deleted when unbanned, so any existing ban is active 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { db, schema } from '@/db'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
@@ -34,11 +34,11 @@ export async function GET(req: NextRequest) {
|
||||
.where(eq(schema.roomInvitations.userId, viewerId))
|
||||
.orderBy(schema.roomInvitations.createdAt)
|
||||
|
||||
// Get all active bans for this user
|
||||
// Get all active bans for this user (bans are deleted when unbanned, so any existing ban is active)
|
||||
const activeBans = await db
|
||||
.select({ roomId: schema.roomBans.roomId })
|
||||
.from(schema.roomBans)
|
||||
.where(and(eq(schema.roomBans.userId, viewerId), isNull(schema.roomBans.unbannedAt)))
|
||||
.where(eq(schema.roomBans.userId, viewerId))
|
||||
|
||||
const bannedRoomIds = new Set(activeBans.map((ban) => ban.roomId))
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { createInvitation, getRoomInvitations } from '@/lib/arcade/room-invitations'
|
||||
import {
|
||||
createInvitation,
|
||||
declineInvitation,
|
||||
getInvitation,
|
||||
getRoomInvitations,
|
||||
} from '@/lib/arcade/room-invitations'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
@@ -123,3 +128,33 @@ export async function GET(req: NextRequest, context: RouteContext) {
|
||||
return NextResponse.json({ error: 'Failed to get invitations' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/arcade/rooms/:roomId/invite
|
||||
* Decline an invitation (invited user only)
|
||||
*/
|
||||
export async function DELETE(req: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
const viewerId = await getViewerId()
|
||||
|
||||
// Check if there's an invitation for this user
|
||||
const invitation = await getInvitation(roomId, viewerId)
|
||||
|
||||
if (!invitation) {
|
||||
return NextResponse.json({ error: 'No invitation found for this room' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (invitation.status !== 'pending') {
|
||||
return NextResponse.json({ error: 'Invitation is not pending' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Decline the invitation
|
||||
await declineInvitation(invitation.id)
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to decline invitation:', error)
|
||||
return NextResponse.json({ error: 'Failed to decline invitation' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ export function ModerationPanel({
|
||||
if (reportsRes.ok) {
|
||||
const data = await reportsRes.json()
|
||||
setReports(data.reports || [])
|
||||
} else {
|
||||
const errorData = await reportsRes.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to load reports')
|
||||
}
|
||||
|
||||
// Load bans
|
||||
@@ -128,6 +131,9 @@ export function ModerationPanel({
|
||||
if (bansRes.ok) {
|
||||
const data = await bansRes.json()
|
||||
setBans(data.bans || [])
|
||||
} else {
|
||||
const errorData = await bansRes.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to load bans')
|
||||
}
|
||||
|
||||
// Load historical members
|
||||
@@ -135,10 +141,13 @@ export function ModerationPanel({
|
||||
if (historyRes.ok) {
|
||||
const data = await historyRes.json()
|
||||
setHistoricalMembers(data.historicalMembers || [])
|
||||
} else {
|
||||
const errorData = await historyRes.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to load history')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load moderation data:', err)
|
||||
setError('Failed to load data')
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -159,7 +168,8 @@ export function ModerationPanel({
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to kick player')
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to kick player')
|
||||
}
|
||||
|
||||
// Success - member will be removed via socket update
|
||||
@@ -191,7 +201,8 @@ export function ModerationPanel({
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to ban player')
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to ban player')
|
||||
}
|
||||
|
||||
// Reload bans
|
||||
@@ -221,7 +232,8 @@ export function ModerationPanel({
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to unban player')
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to unban player')
|
||||
}
|
||||
|
||||
// Reload bans and history
|
||||
@@ -256,7 +268,8 @@ export function ModerationPanel({
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to unban player')
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to unban player')
|
||||
}
|
||||
|
||||
// Reload bans and history
|
||||
|
||||
@@ -45,11 +45,12 @@ export function PendingInvitations({ onInvitationChange, currentRoomId }: Pendin
|
||||
const data = await res.json()
|
||||
setInvitations(data.invitations || [])
|
||||
} else {
|
||||
throw new Error('Failed to fetch invitations')
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to fetch invitations')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch invitations:', err)
|
||||
setError('Failed to load invitations')
|
||||
setError(err instanceof Error ? err.message : 'Failed to load invitations')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -87,7 +88,8 @@ export function PendingInvitations({ onInvitationChange, currentRoomId }: Pendin
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to decline invitation')
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || 'Failed to decline invitation')
|
||||
}
|
||||
|
||||
// Refresh invitations
|
||||
|
||||
Reference in New Issue
Block a user