Files
bobu/functions/src/board/fetchSingleItem.ts
2025-07-15 11:23:20 +09:00

113 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string>;
if (!collection || !docId) {
response.status(400).send({ message: 'Missing parameters' });
return;
}
// ❶collection allowlist
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' });
}
});
},
);