import { onRequest } from 'firebase-functions/v2/https'; import { logger } from 'firebase-functions/v2'; import { getFirestore } from 'firebase-admin/firestore'; import { initializeApp, getApps } from 'firebase-admin/app'; import type { BoardItem, BoardAccessMode } from '../types/boardItem'; import { corsMiddlewareHandler, ALLOWED_COLLECTIONS } from '../config'; import { isValidBoardItem, verifySessionFromRequest, buildQueryWithAccessControl, } from '../utils'; if (!getApps().length) { initializeApp(); } const db = getFirestore(); export const fetchSingleItem = onRequest( { region: 'asia-northeast3' }, async (request, response) => { return corsMiddlewareHandler(request, response, async () => { logger.info('fetchSingleItem: CORS-enabled function called'); /* 1.Method guard ------------------------------------------------------------------ */ if (request.method !== 'GET') { response.status(405).send({ message: 'Method Not Allowed' }); return; } // 2. Auth (optional) let userRole = 0; let userId: string | null = null; const authUser = await verifySessionFromRequest(request); if (authUser) { userId = authUser.uid; userRole = authUser.role; } // 3. Query params const { collection = '', docId = '', access = 'public', } = request.query as Record; if (!collection || !docId) { response.status(400).send({ message: 'Missing parameters' }); return; } // ❶collection allow‑list if ( !ALLOWED_COLLECTIONS.has( collection as unknown as typeof ALLOWED_COLLECTIONS extends Set< infer U > ? U : never, ) ) { response.status(400).send({ message: 'Invalid collection' }); return; } if (!['public', 'private', 'admin'].includes(access)) { response.status(400).send({ message: 'Bad access mode' }); return; } const accessParam = (access ?? 'public') as BoardAccessMode; const effectiveAccess = userId ? accessParam : 'public'; // force public if anonymous try { // Build query with access control const query = buildQueryWithAccessControl( db.collection(collection), effectiveAccess, userId, userRole, ).where('__name__', '==', docId); const snap = await query.limit(1).get(); if (snap.empty) { response .status(404) .send({ message: 'Item not found or access denied' }); return; } const docSnap = snap.docs[0]; const data = docSnap.data() as BoardItem; if (!isValidBoardItem(data)) { response.status(500).send({ message: 'Invalid item data' }); return; } if (access === 'public') { response.set('Cache-Control', 'public,max-age=60'); } response.status(200).send({ ...data, docId: docSnap.id }); return; } catch (err) { logger.error('fetchSingleItem: Firestore error', err); response.status(500).send({ message: 'Internal Server Error' }); } }); }, );