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:
Thomas Hallock
2025-10-13 14:56:20 -05:00
parent 233bd342a8
commit 97669ad084
4 changed files with 62 additions and 12 deletions

View File

@@ -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))

View File

@@ -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 })
}
}

View File

@@ -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

View File

@@ -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