250715
This commit is contained in:
73
functions/src/board/boardMail.ts
Normal file
73
functions/src/board/boardMail.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { onRequest } from 'firebase-functions/v2/https';
|
||||
import { logger } from 'firebase-functions/v2';
|
||||
import { defineSecret } from 'firebase-functions/params';
|
||||
import { initializeApp, getApps } from 'firebase-admin/app';
|
||||
import sgMail from '@sendgrid/mail';
|
||||
|
||||
import type { BoardAccessMode } from '../types/boardItem';
|
||||
|
||||
import { corsMiddlewareHandler, SendEmail } from '../config';
|
||||
|
||||
/* ─── Boiler-plate init ───────────────────────────────────────── */
|
||||
if (!getApps().length) initializeApp();
|
||||
const SENDGRID_API_KEY = defineSecret('SENDGRID_API_KEY');
|
||||
let isSendGridInitialized = false;
|
||||
|
||||
/* ─── Cloud Function entry ────────────────────────────────────── */
|
||||
export const boardMail = onRequest(
|
||||
{
|
||||
region: 'asia-northeast3',
|
||||
secrets: [SENDGRID_API_KEY], // ❷ declare dependency
|
||||
},
|
||||
async (req, res) => {
|
||||
return corsMiddlewareHandler(req, res, async () => {
|
||||
/* one-time SendGrid init per cold start */
|
||||
if (!isSendGridInitialized) {
|
||||
const apiKey = process.env.SENDGRID_API_KEY;
|
||||
if (!apiKey) {
|
||||
logger.error('SendGrid API key is undefined. Check secret setup.');
|
||||
throw new Error('Missing SendGrid API key');
|
||||
}
|
||||
sgMail.setApiKey(apiKey);
|
||||
isSendGridInitialized = true;
|
||||
}
|
||||
/* 1. Method guard --------------------------------------------------- */
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).send({ message: 'Method Not Allowed' });
|
||||
return;
|
||||
}
|
||||
/* 2. Extract params ------------------------------------------------- */
|
||||
const { access = 'public', subject, html, action = '' } = req.body ?? {};
|
||||
|
||||
if (!['public', 'private', 'admin'].includes(access)) {
|
||||
logger.warn(`Invalid access mode: ${access}`);
|
||||
res.status(400).send({ message: 'Bad access mode' });
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveAccess = access as BoardAccessMode; // ❹ always public/param
|
||||
logger.info('boardMail: effectiveAccess', effectiveAccess);
|
||||
if (!subject || !html) {
|
||||
res.status(400).send({ message: 'Missing subject or html' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sgMail.send({
|
||||
to: SendEmail.to,
|
||||
from: SendEmail.from,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`[boardMail] Email sent successfully via SendGrid [${action}]`,
|
||||
);
|
||||
res.status(200).send({ message: 'Email sent' });
|
||||
} catch (err) {
|
||||
logger.error('[boardMail] Error sending email', err as Error);
|
||||
res.status(500).send({ message: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -26,32 +26,22 @@ export const fetchBoards = onRequest(
|
||||
return corsMiddlewareHandler(request, response, async () => {
|
||||
logger.info('fetchBoards: CORS‑enabled function called');
|
||||
|
||||
/* 1.Method guard ------------------------------------------------------------------ */
|
||||
/* 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.Read & validate query params --------------------------------------------------- */
|
||||
/* 2.Read & validate query params EARLY */
|
||||
const {
|
||||
collection = '',
|
||||
sortOrder = 'desc',
|
||||
itemsPerPage = '10',
|
||||
access = 'public',
|
||||
access = 'public', // Default to 'public' if not provided
|
||||
pageToken,
|
||||
} = request.query as Record<string, string>;
|
||||
|
||||
// ❶collection allow‑list
|
||||
// ❶ collection allow-list
|
||||
if (
|
||||
!ALLOWED_COLLECTIONS.has(
|
||||
collection as unknown as typeof ALLOWED_COLLECTIONS extends Set<
|
||||
@@ -65,25 +55,45 @@ export const fetchBoards = onRequest(
|
||||
return;
|
||||
}
|
||||
|
||||
// ❷access must be one of three strings
|
||||
// ❷ access must be one of three strings. Validate BEFORE auth for basic correctness.
|
||||
if (!['public', 'private', 'admin'].includes(access)) {
|
||||
response.status(400).send({ message: 'Bad access mode' });
|
||||
return;
|
||||
}
|
||||
|
||||
/* 3. Authentication and Authorization Check */
|
||||
let userRole = 0;
|
||||
let userId: string | null = null;
|
||||
let authUser = null; // Initialize authUser to null
|
||||
|
||||
try {
|
||||
authUser = await verifySessionFromRequest(request);
|
||||
} catch (err) {
|
||||
logger.warn('fetchBoards: Session verification failed', err);
|
||||
}
|
||||
if (authUser) {
|
||||
userId = authUser.uid;
|
||||
userRole = authUser.role;
|
||||
}
|
||||
// If user is NOT logged in (userId is null) AND they are requesting 'private' or 'admin' access
|
||||
if (!userId && (access === 'private' || access === 'admin')) {
|
||||
response
|
||||
.status(401)
|
||||
.send({ message: 'Unauthorized: Login required for this resource.' });
|
||||
return; // Function stops here.
|
||||
}
|
||||
|
||||
// ❸numeric arguments
|
||||
const limit = Math.max(1, Math.min(Number(itemsPerPage) || 10, 100));
|
||||
const order = sortOrder === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
/* 4.Build query with access control ----------------------------------------------- */
|
||||
const accessParam = (access ?? 'public') as BoardAccessMode;
|
||||
const effectiveAccess = userId ? accessParam : 'public'; // force public if anonymous
|
||||
|
||||
let queryRef: Query<DocumentData>;
|
||||
try {
|
||||
queryRef = buildQueryWithAccessControl(
|
||||
db.collection(collection),
|
||||
effectiveAccess,
|
||||
access as BoardAccessMode, // Use the original 'access'
|
||||
userId,
|
||||
userRole,
|
||||
);
|
||||
@@ -141,7 +151,7 @@ export const fetchBoards = onRequest(
|
||||
)
|
||||
: null;
|
||||
|
||||
if (effectiveAccess === 'public')
|
||||
if (access === 'public')
|
||||
response.set('Cache-Control', 'public,max-age=60');
|
||||
|
||||
response.status(200).send({ items, nextPageToken });
|
||||
|
||||
112
functions/src/board/fetchSingleItem.ts
Normal file
112
functions/src/board/fetchSingleItem.ts
Normal 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 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' });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './countBoard';
|
||||
export * from './fetchBoard';
|
||||
export * from './fetchSingleItem';
|
||||
export * from './boardMail';
|
||||
|
||||
Reference in New Issue
Block a user