import { NextRequest, NextResponse } from 'next/server'; import type { QueryDocumentSnapshot } from 'firebase-admin/firestore'; import { getAdminAuth, getAdminFirestore } from '../../../../../lib/firebase-admin'; export const dynamic = 'force-dynamic'; interface RouteContext { params: Promise<{ rideId: string; }>; } export interface GroupRideJoinRequestDto { userId: string; riderDisplayName: string | null; riderHandle: string | null; riderAvatarPreset: string | null; riderAvatarUrl: string | null; createdAt: string; } export async function GET(request: NextRequest, context: RouteContext) { try { // 1. Require and extract Bearer token const authHeader = request.headers.get('authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return NextResponse.json( { error: 'unauthorized', detail: 'Missing or malformed Authorization header.' }, { status: 401 } ); } const idToken = authHeader.slice(7).trim(); if (!idToken) { return NextResponse.json( { error: 'unauthorized', detail: 'Missing authentication token.' }, { status: 401 } ); } // 2. Verify Firebase ID token server-side and derive callerUid strictly from token let callerUid: string; try { const decoded = await getAdminAuth().verifyIdToken(idToken); callerUid = decoded.uid; } catch { return NextResponse.json( { error: 'unauthorized', detail: 'Invalid or expired authentication token.' }, { status: 401 } ); } // 3. Validate rideId const { rideId } = await context.params; if ( !rideId || typeof rideId !== 'string' || rideId.trim().length === 0 || rideId.length > 64 || /\s/.test(rideId) ) { return NextResponse.json( { error: 'invalid_request', detail: 'Invalid rideId parameter.' }, { status: 400 } ); } const normalizedRideId = rideId.trim(); const db = getAdminFirestore(); // 4. Fetch ride document const rideSnap = await db.collection('group_rides').doc(normalizedRideId).get(); if (!rideSnap.exists) { return NextResponse.json( { error: 'not_found', detail: 'Group ride not found.' }, { status: 404 } ); } const rideData = rideSnap.data() || {}; const organizerUid = rideData.organizerUid; const isOrganizer = organizerUid === callerUid; let isClubAdmin = false; if (!isOrganizer && typeof rideData.clubId === 'string' && rideData.clubId.trim().length > 0) { const clubId = rideData.clubId.trim(); const memberSnap = await db .collection('clubs') .doc(clubId) .collection('members') .doc(callerUid) .get(); if (memberSnap.exists) { const memberData = memberSnap.data(); if ( memberData?.status === 'active' && (memberData?.role === 'admin' || memberData?.role === 'owner') ) { isClubAdmin = true; } } } // 5. Require caller to be either organizer or active club administrator if (!isOrganizer && !isClubAdmin) { return NextResponse.json( { error: 'forbidden', detail: 'Only the organizer or a club administrator can view join requests.' }, { status: 403 } ); } // 6. Query pending join requests bounded by limit 50, ordered by createdAt asc const requestsSnap = await db .collection('group_rides') .doc(normalizedRideId) .collection('participants') .where('status', '==', 'requested') .orderBy('createdAt', 'asc') .limit(50) .get(); // 7. Minimal DTO mapping (never expose location, live presence, or telemetry) const requests: GroupRideJoinRequestDto[] = requestsSnap.docs.map((doc: QueryDocumentSnapshot) => { const d = doc.data(); const createdAt = typeof d.createdAt === 'string' ? d.createdAt : (d.createdAt?.toDate?.() ? d.createdAt.toDate().toISOString() : ''); return { userId: d.userId || doc.id, riderDisplayName: d.riderDisplayName || null, riderHandle: d.riderHandle || null, riderAvatarPreset: d.riderAvatarPreset || null, riderAvatarUrl: d.riderAvatarUrl || null, createdAt, }; }); return NextResponse.json( { rideId: normalizedRideId, requests, }, { status: 200, headers: { 'Cache-Control': 'no-store, max-age=0', }, } ); } catch (err: any) { console.error('[GROUP_RIDE_JOIN_REQUESTS] Internal error:', err?.message || err); return NextResponse.json( { error: 'server_error', detail: 'Join requests are temporarily unavailable.' }, { status: 500 } ); } }