113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
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 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' });
|
||
}
|
||
});
|
||
},
|
||
);
|