This commit is contained in:
2025-07-15 11:23:20 +09:00
parent 368c52029a
commit 8fd4066d42
27 changed files with 5091 additions and 1847 deletions

View File

@@ -0,0 +1,112 @@
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' });
}
});
},
);