250715
This commit is contained in:
109
functions/package-lock.json
generated
109
functions/package-lock.json
generated
@@ -6,7 +6,8 @@
|
||||
"": {
|
||||
"name": "functions",
|
||||
"dependencies": {
|
||||
"firebase-admin": "^13.3.0",
|
||||
"@sendgrid/mail": "^8.1.5",
|
||||
"firebase-admin": "^13.4.0",
|
||||
"firebase-functions": "^6.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1995,6 +1996,44 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sendgrid/client": {
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.5.tgz",
|
||||
"integrity": "sha512-Jqt8aAuGIpWGa15ZorTWI46q9gbaIdQFA21HIPQQl60rCjzAko75l3D1z7EyjFrNr4MfQ0StusivWh8Rjh10Cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sendgrid/helpers": "^8.0.0",
|
||||
"axios": "^1.8.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/@sendgrid/helpers": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
|
||||
"integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sendgrid/mail": {
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.5.tgz",
|
||||
"integrity": "sha512-W+YuMnkVs4+HA/bgfto4VHKcPKLc7NiZ50/NH2pzO6UHCCFuq8/GNB98YJlLEr/ESDyzAaDr7lVE7hoBwFTT3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sendgrid/client": "^8.1.5",
|
||||
"@sendgrid/helpers": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@@ -2869,8 +2908,7 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
@@ -2888,6 +2926,33 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -3394,7 +3459,6 @@
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -3604,9 +3668,7 @@
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3652,7 +3714,6 @@
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -3912,7 +3973,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -4654,9 +4714,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/firebase-admin": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.3.0.tgz",
|
||||
"integrity": "sha512-MFxv86Aw2rjM/TpKwU86jN7YUFfN1jy6mREYZTLL1aW1rCpZFi4c70b9U12J9Xa4RbJkiXpWBAwth9IVSqR91A==",
|
||||
"version": "13.4.0",
|
||||
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.4.0.tgz",
|
||||
"integrity": "sha512-Y8DcyKK+4pl4B93ooiy1G8qvdyRMkcNFfBSh+8rbVcw4cW8dgG0VXCCTp5NUwub8sn9vSPsOwpb9tE2OuFmcfQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^3.0.0",
|
||||
@@ -4743,6 +4803,26 @@
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -5249,7 +5329,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -7819,6 +7898,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"firebase-admin": "^13.3.0",
|
||||
"@sendgrid/mail": "^8.1.5",
|
||||
"firebase-admin": "^13.4.0",
|
||||
"firebase-functions": "^6.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,16 +4,19 @@ import { initializeApp, getApps } from 'firebase-admin/app';
|
||||
import * as cookie from 'cookie';
|
||||
import { logger } from 'firebase-functions/v2';
|
||||
import { cookieExpiration, corsMiddlewareHandler } from '../config';
|
||||
|
||||
// ✅ Admin SDK init
|
||||
if (!getApps().length) {
|
||||
initializeApp(); // ✅ auto uses service account in Firebase Functions
|
||||
initializeApp();
|
||||
}
|
||||
|
||||
export const createSession = onRequest(
|
||||
{ region: 'asia-northeast3' },
|
||||
async (req, res) => {
|
||||
return corsMiddlewareHandler(req, res, async () => {
|
||||
logger.info('countBoards: CORS‑enabled function called');
|
||||
// Fix this logger message: it should be about createSession, not countBoards
|
||||
logger.info('createSession: CORS-enabled function called');
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).send('Method not allowed');
|
||||
return;
|
||||
@@ -38,7 +41,7 @@ export const createSession = onRequest(
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
maxAge: expiresIn / 1000,
|
||||
sameSite: 'lax' as const,
|
||||
sameSite: 'none' as const,
|
||||
path: '/',
|
||||
};
|
||||
|
||||
@@ -48,6 +51,10 @@ export const createSession = onRequest(
|
||||
);
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'createSession: Invalid token or session cookie creation failed',
|
||||
error,
|
||||
); // Added more specific logging
|
||||
res.status(401).send('Invalid token');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// In your verifySession.ts (the API endpoint Cloud Function)
|
||||
import { onRequest } from 'firebase-functions/v2/https';
|
||||
import * as cookie from 'cookie';
|
||||
import { getAuth } from 'firebase-admin/auth';
|
||||
import { initializeApp, getApps } from 'firebase-admin/app';
|
||||
import { corsMiddlewareHandler } from '../config';
|
||||
import { logger } from 'firebase-functions/v2';
|
||||
import { logger } from 'firebase-functions/v2'; // Ensure logger is imported
|
||||
|
||||
// ✅ Ensure Admin SDK is initialized
|
||||
if (!getApps().length) {
|
||||
initializeApp();
|
||||
}
|
||||
@@ -14,17 +14,31 @@ export const verifySession = onRequest(
|
||||
{ region: 'asia-northeast3' },
|
||||
async (req, res) => {
|
||||
return corsMiddlewareHandler(req, res, async () => {
|
||||
logger.info('verifySession: Function invoked');
|
||||
logger.info('verifySession: Function invoked'); // This log is already there
|
||||
|
||||
// --- ADD THESE LOGS ---
|
||||
logger.info(
|
||||
'verifySession (endpoint): Raw headers.cookie:',
|
||||
req.headers.cookie,
|
||||
);
|
||||
const cookies = cookie.parse(req.headers.cookie || '');
|
||||
logger.info('verifySession (endpoint): Parsed cookies object:', cookies);
|
||||
const session = cookies.__session;
|
||||
logger.info(
|
||||
'verifySession (endpoint): __session cookie value:',
|
||||
session ? 'Found' : 'Not Found',
|
||||
);
|
||||
// --- END ADDED LOGS ---
|
||||
|
||||
if (req.method !== 'GET') {
|
||||
res.status(405).send('Method Not Allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
const cookies = cookie.parse(req.headers.cookie || '');
|
||||
const session = cookies.__session;
|
||||
|
||||
if (!session) {
|
||||
logger.warn(
|
||||
'verifySession (endpoint): No __session cookie found, returning 401.',
|
||||
); // Specific warn
|
||||
res.status(401).send({ message: 'No session cookie' });
|
||||
return;
|
||||
}
|
||||
@@ -32,14 +46,22 @@ export const verifySession = onRequest(
|
||||
try {
|
||||
const decoded = await getAuth().verifySessionCookie(session, true);
|
||||
const role = decoded.role || 0;
|
||||
logger.info(
|
||||
'verifySession (endpoint): Session verified successfully for UID:',
|
||||
decoded.uid,
|
||||
); // Log success
|
||||
|
||||
res.status(200).json({
|
||||
uid: decoded.uid,
|
||||
email: decoded.email,
|
||||
role,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('verifySession: Session invalid or expired', err);
|
||||
} catch (err: unknown) {
|
||||
// Use any for err to access message
|
||||
logger.warn(
|
||||
'verifySession (endpoint): Session invalid or expired, returning 401:',
|
||||
err instanceof Error ? err.message : String(err),
|
||||
); // Specific warn
|
||||
res.status(401).send({ message: 'Invalid session' });
|
||||
}
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
@@ -82,6 +82,7 @@ const MAX_FILE_SIZE_MB = 20;
|
||||
const MAX_TOTAL_FILES = 10;
|
||||
export const ALLOWED_COLLECTIONS = new Set([
|
||||
'notices',
|
||||
'wadizes' /* … */,
|
||||
'projects' /* … */,
|
||||
] as const);
|
||||
const ALLOWED_IMAGE_TYPES = [
|
||||
@@ -123,6 +124,12 @@ const ALLOWED_FILE_TYPES = [
|
||||
|
||||
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||
|
||||
export const SendEmail = {
|
||||
to: 'bobu1104@naver.com',
|
||||
from: 'movemama@manos.kr',
|
||||
subject: '[보부웹앱] - 새로운 신청',
|
||||
};
|
||||
|
||||
const isImageFile = (file: File): boolean =>
|
||||
ALLOWED_IMAGE_TYPES.includes(file.type);
|
||||
const isVideoFile = (file: File): boolean =>
|
||||
|
||||
@@ -111,6 +111,23 @@ export type BoardItem = {
|
||||
thumbnail?: ImageItem; //Leave it for previous data structure
|
||||
};
|
||||
// Board : Types
|
||||
export interface WadizBoard extends BoardItem {
|
||||
name: string;
|
||||
paymentId: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
emergencyPhone: string;
|
||||
scheduleStart?: string | Date;
|
||||
scheduleEnd?: string | Date;
|
||||
attending: 'yes' | 'no';
|
||||
altName?: string;
|
||||
altPhone?: string;
|
||||
companions?: string;
|
||||
healthNotes?: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
export interface ProjectBoard extends BoardItem {
|
||||
subtitle: string;
|
||||
displayDate?: string;
|
||||
|
||||
@@ -1,33 +1,60 @@
|
||||
// Your verifyHelper.ts (or wherever this function is)
|
||||
|
||||
import { getAuth } from 'firebase-admin/auth';
|
||||
import type { Request } from 'express';
|
||||
import * as cookie from 'cookie';
|
||||
import { logger } from 'firebase-functions/v2'; // <--- Make sure you import logger here
|
||||
|
||||
export type DecodedAuthInfo = {
|
||||
uid: string;
|
||||
role: number;
|
||||
};
|
||||
/**
|
||||
* Verifies the session cookie from the request and returns the decoded auth info.
|
||||
* Throws an error if invalid or missing.
|
||||
* @param {Request} req - The Express request object containing the session cookie
|
||||
* @param {Response} [res] - Optional Express response object for sending error responses
|
||||
*/
|
||||
|
||||
export async function verifySessionFromRequest(
|
||||
req: Request,
|
||||
): Promise<DecodedAuthInfo | null> {
|
||||
// --- ADD THESE LOGS ---
|
||||
logger.info('verifySessionFromRequest: STARTING verification');
|
||||
logger.info(
|
||||
'verifySessionFromRequest: Raw headers.cookie:',
|
||||
req.headers.cookie,
|
||||
); // Log the raw cookie header
|
||||
// --- END ADDED LOGS ---
|
||||
|
||||
const cookies = cookie.parse(req.headers.cookie || '');
|
||||
|
||||
// --- ADD THESE LOGS ---
|
||||
logger.info('verifySessionFromRequest: Parsed cookies object:', cookies); // Log the parsed cookies object
|
||||
// --- END ADDED LOGS ---
|
||||
|
||||
const session = cookies.__session;
|
||||
|
||||
// --- ADD THESE LOGS ---
|
||||
logger.info(
|
||||
'verifySessionFromRequest: __session cookie value:',
|
||||
session ? 'Found (length: ' + session.length + ')' : 'Not Found',
|
||||
);
|
||||
// --- END ADDED LOGS ---
|
||||
|
||||
if (!session) {
|
||||
logger.warn('verifySessionFromRequest: No __session cookie found.'); // Specific log for missing cookie
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = await getAuth().verifySessionCookie(session, true);
|
||||
const role = (decoded.role || 0) as number;
|
||||
logger.info(
|
||||
'verifySessionFromRequest: Session cookie decoded successfully for UID:',
|
||||
decoded.uid,
|
||||
); // Log success
|
||||
return { uid: decoded.uid, role };
|
||||
} catch (err) {
|
||||
console.error('[Auth] Session cookie verification failed:', err);
|
||||
} catch (err: unknown) {
|
||||
// Use 'any' type for err to easily access its properties
|
||||
logger.error(
|
||||
'[Auth] Session cookie verification failed:',
|
||||
err instanceof Error ? err.message : String(err),
|
||||
); // Log the actual error message
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user