diff --git a/site/lib/airtable-utils.js b/site/lib/airtable-utils.js new file mode 100644 index 00000000..43c271a0 --- /dev/null +++ b/site/lib/airtable-utils.js @@ -0,0 +1,20 @@ +function escapeAirtableString(str) { + return String(str).replace(/\\/g, "\\\\").replace(/'/g, "\\'"); +} + +function normalizeEmail(email) { + const rawEmail = Array.isArray(email) ? email[0] : email; + return String(rawEmail || '').trim().toLowerCase(); +} + +const simpleEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +function isValidEmail(email) { + return simpleEmailRegex.test(email); +} + +module.exports = { + escapeAirtableString, + normalizeEmail, + isValidEmail, +}; diff --git a/site/pages/api/GenerateChinaLandingPDF.js b/site/pages/api/GenerateChinaLandingPDF.js index cbeed851..460e9b70 100644 --- a/site/pages/api/GenerateChinaLandingPDF.js +++ b/site/pages/api/GenerateChinaLandingPDF.js @@ -4,6 +4,7 @@ import { text, multiVariableText, image, table } from '@pdfme/schemas'; import Airtable from 'airtable'; import fs from 'fs'; import path from 'path'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' /* * RawRoommateData Schema from Airtable: diff --git a/site/pages/api/_middleware.js b/site/pages/api/_middleware.js new file mode 100644 index 00000000..47e73a63 --- /dev/null +++ b/site/pages/api/_middleware.js @@ -0,0 +1,60 @@ +import Airtable from "airtable"; + +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID +); + +export async function authenticateRequest(req) { + const authToken = req.headers.authorization?.split(" ")[1] || req.body?.token; + + if (!authToken) { + return { + authenticated: false, + error: "No auth token provided", + status: 401, + }; + } + + try { + const safeAuthToken = authToken.replace(/'/g, "\\'"); + + const records = await base("Signups") + .select({ + filterByFormula: `{token} = '${safeAuthToken}'`, + maxRecords: 1, + }) + .firstPage(); + + if (records.length === 0) { + return { authenticated: false, error: "Invalid token", status: 401 }; + } + + return { + authenticated: true, + user: records[0].fields, + recordId: records[0].id, + }; + } catch (error) { + console.error("Authentication error:", error); + return { + authenticated: false, + error: "Authentication failed", + status: 500, + }; + } +} + +export function withAuth(handler) { + return async (req, res) => { + const auth = await authenticateRequest(req); + + if (!auth.authenticated) { + return res.status(auth.status).json({ message: auth.error }); + } + + req.user = auth.user; + req.userId = auth.recordId; + + return handler(req, res); + }; +} diff --git a/site/pages/api/cancel-juice-stretch.js b/site/pages/api/cancel-juice-stretch.js index 270866c0..791292a4 100644 --- a/site/pages/api/cancel-juice-stretch.js +++ b/site/pages/api/cancel-juice-stretch.js @@ -1,32 +1,27 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, stretchId } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } + const { stretchId } = req.body; + const sanitisedID = escapeAirtableString(stretchId); - const signupRecord = signupRecords[0]; const records = await base('juiceStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedID}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'Juice stretch not found' }); @@ -46,4 +41,4 @@ export default async function handler(req, res) { console.error('Error canceling juice stretch:', error); res.status(500).json({ message: 'Error canceling juice stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/cancel-jungle-stretch.js b/site/pages/api/cancel-jungle-stretch.js index 0d3c3319..ba4b85d9 100644 --- a/site/pages/api/cancel-jungle-stretch.js +++ b/site/pages/api/cancel-jungle-stretch.js @@ -1,32 +1,24 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, stretchId } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; - + const { stretchId } = req.body; + const sanitisedID = escapeAirtableString(stretchId); const records = await base('jungleStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedID}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'jungle stretch not found' }); @@ -46,4 +38,4 @@ export default async function handler(req, res) { console.error('Error canceling jungle stretch:', error); res.status(500).json({ message: 'Error canceling jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/create-omg-moment.js b/site/pages/api/create-omg-moment.js index 438deabe..8475b056 100644 --- a/site/pages/api/create-omg-moment.js +++ b/site/pages/api/create-omg-moment.js @@ -1,41 +1,37 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { description, token, stretchId, stopTime } = req.body; - - // Get user by token - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); + const { description, stretchId, stopTime } = req.body; - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } + const sanitisedDescription = escapeAirtableString(description); - const signupRecord = signupRecords[0]; - - // Create OMG moment record with video URL const omgMoment = await base('omgMoments').create([ { fields: { - description, - email: signupRecord.fields.email + description: sanitisedDescription, + email: req.user.email } } ]); - // Update juice stretch with end time and link to OMG moment + const sanitisedID = escapeAirtableString(stretchId); + + await base('juiceStretches').update([ { - id: stretchId, + id: sanitisedID, fields: { endTime: stopTime, omgMoment: [omgMoment[0].id] @@ -48,4 +44,4 @@ export default async function handler(req, res) { console.error('Error creating OMG moment:', error); res.status(500).json({ message: 'Error creating OMG moment' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/create-tamagotchi.js b/site/pages/api/create-tamagotchi.js index 995e2135..79c5bd44 100644 --- a/site/pages/api/create-tamagotchi.js +++ b/site/pages/api/create-tamagotchi.js @@ -1,46 +1,30 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; - - // Check if user already has a Tamagotchi const existingTamagotchi = await base('Tamagotchi').select({ - filterByFormula: `{user} = '${signupRecord.fields.email}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{user} = '${req.user.email}'`, + maxRecords: 1 + }).firstPage(); if (existingTamagotchi && existingTamagotchi.length > 0) { return res.status(200).json({ message: 'Tamagotchi already exists' }); } - // Use exact UTC timestamp const startDate = new Date().toISOString(); - // Create new Tamagotchi record with a link to the Signups record const record = await base('Tamagotchi').create([ { fields: { - user: [signupRecord.id], - startDate: startDate, // Use full ISO string with time + user: [req.userId], + startDate: startDate, isAlive: true, streakNumber: 0.0 } @@ -52,4 +36,4 @@ export default async function handler(req, res) { console.error('Error creating Tamagotchi:', error); res.status(500).json({ message: 'Error creating Tamagotchi' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/deck/add.js b/site/pages/api/deck/add.js index 26abd1c7..49691ddb 100644 --- a/site/pages/api/deck/add.js +++ b/site/pages/api/deck/add.js @@ -1,8 +1,9 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } @@ -65,4 +66,4 @@ export default async function handler(req, res) { console.error('Error adding card to deck:', error); return res.status(500).json({ error: 'Failed to add card to deck' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/delete-tamagotchi.js b/site/pages/api/delete-tamagotchi.js index 51b316c1..c3988445 100644 --- a/site/pages/api/delete-tamagotchi.js +++ b/site/pages/api/delete-tamagotchi.js @@ -1,38 +1,23 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; - - // Find user's Tamagotchi const tamagotchiRecords = await base('Tamagotchi').select({ - filterByFormula: `{user} = '${signupRecord.fields.email}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{user} = '${req.user.email}'`, + maxRecords: 1 + }).firstPage(); if (!tamagotchiRecords || tamagotchiRecords.length === 0) { return res.status(404).json({ message: 'Tamagotchi not found' }); } - // Delete the Tamagotchi record await base('Tamagotchi').destroy([tamagotchiRecords[0].id]); res.status(200).json({ success: true }); @@ -40,4 +25,4 @@ export default async function handler(req, res) { console.error('Error deleting Tamagotchi:', error); res.status(500).json({ message: 'Error deleting Tamagotchi' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/edit-magazine.js b/site/pages/api/edit-magazine.js index 969d3030..fd894c62 100644 --- a/site/pages/api/edit-magazine.js +++ b/site/pages/api/edit-magazine.js @@ -1,33 +1,30 @@ -// site/pages/api/edit-magazine.js -import { base } from "@/lib/airtable"; +import { base } from '@/lib/airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -export default async function handler(req, res) { + +export default withAuth(async function handler(req, res) { if (req.method !== 'PUT') { return res.status(405).json({ message: 'Method not allowed' }); } - const { id, fields, token } = req.body; - - if (!token) { - return res.status(401).json({ message: 'Unauthorized: Missing token' }); - } + const { id, fields } = req.body; + const sanitisedId = escapeAirtableString(id); try { - // Fetch the record to verify the token - const record = await base('YSWS Project Submission').find(id); - const recordToken = record.fields.token ? record.fields.token[0] : null; // Access the first element - const providedToken = Array.isArray(token) ? token[0] : token; // Ensure token is not an array - console.log('Record Token:', recordToken); // Debugging line - console.log('Provided Token:', providedToken); // Debugging line + const record = await base('YSWS Project Submission').find(sanitisedId); + const recordToken = record.fields.token ? record.fields.token[0] : null; + + const userToken = + req.body.token || req.headers.authorization?.split(' ')[1]; + const providedToken = Array.isArray(userToken) ? userToken[0] : userToken; if (recordToken !== providedToken) { return res.status(401).json({ message: 'Unauthorized: Invalid token' }); } - // Update the record in Airtable const updatedRecord = await base('YSWS Project Submission').update(id, fields); - // Return the updated record res.status(200).json({ id: updatedRecord.id, "Code URL": updatedRecord.fields["Code URL"] || null, @@ -43,4 +40,4 @@ export default async function handler(req, res) { console.error('Error updating magazine submission:', error); res.status(500).json({ message: 'Error updating magazine submission', error: error.message }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/finish-boss.js b/site/pages/api/finish-boss.js index d566ed36..0bef6b03 100644 --- a/site/pages/api/finish-boss.js +++ b/site/pages/api/finish-boss.js @@ -1,35 +1,27 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, githubLink, itchLink } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; + const { githubLink, itchLink } = req.body; const stretchId = uuidv4(); // console.log("singup " + signupRecord.id) const records = await base('jungleBosses').select({}).firstPage(); const jungleBossesFoughtRecords = await base("jungleBossesFought").select({ - filterByFormula: `{user} = '${signupRecord.fields.email}'` - }).firstPage(); + filterByFormula: `{user} = '${req.user.email}'`, + }).firstPage(); const jungleBossesFoughtIds = jungleBossesFoughtRecords.map(jungleBossFought => jungleBossFought.fields.jungleBoss[0]) + // Filter jungle bosses not fought const jungleBossesNotFought = records .filter(record => !jungleBossesFoughtIds.includes(record.id) && record.fields.hours) @@ -38,28 +30,27 @@ export default async function handler(req, res) { // Create new record in jungleStretches with a reference to the Signups record const bossFought = (await base('jungleBossesFought').create([ - { - fields: { - ID: stretchId, - githubLink, - itchLink, - user: [signupRecord.id], // Link to the Signups record - timeFought: new Date().toISOString(), - jungleBoss: [jungleBossesNotFought[0].id] + { + fields: { + ID: stretchId, + githubLink, + itchLink, + user: [req.userId], + timeFought: new Date().toISOString(), + jungleBoss: [jungleBossesNotFought[0].id] + } } - } - ]))[0]; - + ]))[0]; const jungleStretchesRecords = await base('jungleStretches').select({ - filterByFormula: ` + filterByFormula: ` AND( - {email (from Signups)} = '${signupRecord.fields.email}', + {email (from Signups)} = '${req.user.email}', ({endtime}), NOT({isCanceled}) ) ` - }).all(); + }).all(); if (!jungleStretchesRecords || jungleStretchesRecords.length === 0) { return res.status(404).json({ message: 'Jungle stretch not found' }); @@ -84,10 +75,10 @@ export default async function handler(req, res) { ]); maxHours -= jungleStretch[0].fields.timeWorkedHours; } - + res.status(200).json({ stretchId }); } catch (error) { console.error('Error starting jungle stretch:', error); res.status(500).json({ message: 'Error starting jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/get-active-juicers.js b/site/pages/api/get-active-juicers.js index 41725257..7b69dee7 100644 --- a/site/pages/api/get-active-juicers.js +++ b/site/pages/api/get-active-juicers.js @@ -1,8 +1,11 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'GET') { return res.status(405).json({ message: 'Method not allowed' }); } @@ -17,46 +20,46 @@ export default async function handler(req, res) { // 2. Not cancelled // 3. No end time (not completed) const activeRecords = await base('juiceStretches').select({ - filterByFormula: `AND( + filterByFormula: `AND( NOT({endTime} != ''), NOT({isCanceled} = TRUE()), {pauseTimeStart} >= '${fiveMinutesAgoIso}' )` - }).firstPage(); + }).firstPage(); // Count the active stretches const activeJuicersCount = activeRecords ? activeRecords.length : 0; - + // Calculate total time worked across all ACTIVE stretches let totalTimeWorkedSeconds = 0; const stretches = []; const activeJuicers = []; - + console.log('--- Active Stretch Details ---'); - + if (activeRecords && activeRecords.length > 0) { activeRecords.forEach((record, index) => { const fields = record.fields; - + if (fields.startTime) { const startTime = new Date(fields.startTime); let endTime; - + // For active stretches, we either use pauseTimeStart or current time if (fields.pauseTimeStart) { endTime = new Date(fields.pauseTimeStart); } else { endTime = new Date(); } - + // Calculate duration in seconds const durationSeconds = (endTime - startTime) / 1000; - + // Subtract pause time if available const pauseTimeSeconds = fields.totalPauseTimeSeconds || 0; const workTimeSeconds = Math.max(0, durationSeconds - pauseTimeSeconds); const workTimeHours = workTimeSeconds / 3600; - + // Information for this stretch - removing any personally identifiable information const stretchInfo = { // Use a generic identifier instead of the actual ID @@ -65,12 +68,12 @@ export default async function handler(req, res) { timeWorkedHours: workTimeHours, timeWorkedSeconds: workTimeSeconds }; - + stretches.push(stretchInfo); - + // Extract SlackHandle or use "Juicer" as fallback const slackHandle = fields.SlackHandle || "Juicer"; - + // Add to active juicers list with slack handle activeJuicers.push({ slackHandle, @@ -78,7 +81,7 @@ export default async function handler(req, res) { lastActive: fields.pauseTimeStart, timeWorkedHours: workTimeHours.toFixed(2) }); - + // Log each active stretch individually without any identifiable information console.log(`Stretch #${index + 1}`); console.log(` User: ${slackHandle}`); @@ -89,24 +92,24 @@ export default async function handler(req, res) { console.log(` Total pause time: ${fields.totalPauseTimeSeconds} seconds`); } console.log('---'); - + totalTimeWorkedSeconds += workTimeSeconds; } }); } else { console.log('No active stretches found.'); } - + // Convert to hours for logging const totalTimeWorkedHours = totalTimeWorkedSeconds / 3600; - + console.log(`--- Summary ---`); console.log(`Active juicers: ${activeJuicersCount}`); console.log(`Total active time worked: ${totalTimeWorkedHours.toFixed(2)} hours (${totalTimeWorkedSeconds.toFixed(0)} seconds)`); console.log(`Total active stretches: ${stretches.length}`); // Return the aggregate data along with active juicers info - res.status(200).json({ + res.status(200).json({ count: activeJuicersCount, totalActiveTimeWorkedSeconds: totalTimeWorkedSeconds, totalActiveTimeWorkedHours: totalTimeWorkedHours, @@ -116,4 +119,4 @@ export default async function handler(req, res) { console.error('Error getting active juicers count:', error); res.status(500).json({ message: 'Error getting active juicers count' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/get-gallery.js b/site/pages/api/get-gallery.js index 0f297ef7..b40d4b40 100644 --- a/site/pages/api/get-gallery.js +++ b/site/pages/api/get-gallery.js @@ -1,10 +1,11 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'GET') { return res.status(405).json({ message: 'Method not allowed' }); } @@ -51,4 +52,4 @@ export default async function handler(req, res) { console.error('Error fetching gallery records:', error); res.status(500).json({ message: 'Error fetching gallery records' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/get-jungle-stretch-fruit-collected.js b/site/pages/api/get-jungle-stretch-fruit-collected.js index 49b4781f..e3c69ba0 100644 --- a/site/pages/api/get-jungle-stretch-fruit-collected.js +++ b/site/pages/api/get-jungle-stretch-fruit-collected.js @@ -1,33 +1,30 @@ import Airtable from 'airtable'; import { ReplaceStencilOp } from 'three'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, stretchId } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } + const { stretchId } = req.body; - const signupRecord = signupRecords[0]; + const sanitisedStretchId = escapeAirtableString(stretchId); - const records = await base('jungleStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + const records = await base('jungleStretches') + .select({ + filterByFormula: `{ID} = '${sanitisedStretchId}'`, + maxRecords: 1, + }) + .firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'jungle stretch not found' }); @@ -45,4 +42,4 @@ export default async function handler(req, res) { console.error('Error resuming jungle stretch:', error); res.status(500).json({ message: 'Error resuming jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/get-magazine.js b/site/pages/api/get-magazine.js index 28b0fa98..2b72dd36 100644 --- a/site/pages/api/get-magazine.js +++ b/site/pages/api/get-magazine.js @@ -1,23 +1,16 @@ -import { base } from "@/lib/airtable"; +import { base } from '@/lib/airtable'; +import { withAuth } from './_middleware'; -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'GET') { return res.status(405).json({ message: 'Method not allowed' }); } - // Get the user's token from the query const { token } = req.query; - - console.log("Received token from query:", token); - // Check if the password is provided and correct -// const { password } = req.query; -// if (!password || password !== process.env.MAGAZINE_API_PASSWORD) { -// return res.status(401).json({xr message: 'Unauthorized: Invalid or missing password' }); -// } + console.log('Received token from query:', token); try { - // Fetch records from the YSWS Project Submission table const records = await base('YSWS Project Submission') .select({ filterByFormula: "NOT({DoNotIncludeOnWebsite})" @@ -43,26 +36,25 @@ export default async function handler(req, res) { .all(); juiceStretchData = stretchRecords.map(stretch => ({ - id: stretch.id, - startTime: stretch.fields.startTime, - endTime: stretch.fields.endTime, - timeWorkedSeconds: stretch.fields.timeWorkedSeconds, - timeWorkedHours: stretch.fields.timeWorkedHours, - totalPauseTimeSeconds: stretch.fields.totalPauseTimeSeconds, - isCancelled: stretch.fields.isCancelled, + id: stretch.id, + startTime: stretch.fields.startTime, + endTime: stretch.fields.endTime, + timeWorkedSeconds: stretch.fields.timeWorkedSeconds, + timeWorkedHours: stretch.fields.timeWorkedHours, + totalPauseTimeSeconds: stretch.fields.totalPauseTimeSeconds, + isCancelled: stretch.fields.isCancelled, video: stretch?.fields["video (from omgMoments)"] ? (stretch?.fields["video (from omgMoments)"][0]) : "", description: stretch?.fields["description (from omgMoments)"] ? (stretch?.fields["description (from omgMoments)"][0]) : "", - createdTime: stretch.fields.created, - })); - } - + createdTime: stretch.fields.created, + })); + } + return { id: record.id, "Code URL": fields["Code URL"] || null, "Playable URL": fields["Playable URL"] || null, "videoURL": fields["videoURL"] || null, "First Name": fields["First Name"] || null, - "Last Name": fields["Last Name"] || null, "GitHub Username": fields["GitHub Username"] || null, "Description": fields["Description"] || null, "Screenshot": fields["Screenshot"] || null, @@ -78,4 +70,4 @@ export default async function handler(req, res) { console.error('Error fetching magazine submissions:', error); res.status(500).json({ message: 'Error fetching magazine submissions', error: error.message }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/get-next-boss.js b/site/pages/api/get-next-boss.js index e3a04ff7..92d2ca49 100644 --- a/site/pages/api/get-next-boss.js +++ b/site/pages/api/get-next-boss.js @@ -1,37 +1,26 @@ import Airtable from 'airtable'; import { ReplaceStencilOp } from 'three'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; - const records = await base('jungleBosses').select({}).firstPage(); // const jungleBossesFought = signupRecord.fields.jungleBossesFought || []; // console.log(jungleBossesFought) // Fetch jungle bosses fought by Airtable record ID const jungleBossesFoughtRecords = await base("jungleBossesFought").select({ - filterByFormula: `{user} = '${signupRecord.fields.email}'` - }).firstPage(); + filterByFormula: `{user} = '${req.user.email}'` + }).firstPage(); const jungleBossesFoughtIds = jungleBossesFoughtRecords.map(jungleBossFought => jungleBossFought.fields.jungleBoss[0]) console.log(jungleBossesFoughtIds) @@ -54,4 +43,4 @@ export default async function handler(req, res) { console.error('Error resuming jungle stretch:', error); res.status(500).json({ message: 'Error resuming jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/get-omg-moments.js b/site/pages/api/get-omg-moments.js index 4cfc287a..7bf13f02 100644 --- a/site/pages/api/get-omg-moments.js +++ b/site/pages/api/get-omg-moments.js @@ -1,10 +1,11 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'GET') { return res.status(405).json({ message: 'Method not allowed' }); } @@ -34,4 +35,4 @@ export default async function handler(req, res) { console.error('Error fetching OMG moments:', error); res.status(500).json({ message: 'Error fetching OMG moments' }); } -} +}); diff --git a/site/pages/api/get-roommate-data.js b/site/pages/api/get-roommate-data.js index bc0b44ff..2d22ccdb 100644 --- a/site/pages/api/get-roommate-data.js +++ b/site/pages/api/get-roommate-data.js @@ -1,10 +1,11 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'GET') { return res.status(405).json({ message: 'Method not allowed' }); } @@ -14,21 +15,61 @@ export default async function handler(req, res) { if (!email) { return res.status(400).json({ message: 'Email parameter is required' }); } + const rawEmail = Array.isArray(email) ? email[0] : email; + const normalizedEmail = String(rawEmail || '').trim().toLowerCase(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(normalizedEmail)) { + return res.status(400).json({ message: 'Invalid email parameter' }); + } + const escapeAirtableString = str => + String(str).replace(/\\/g, "\\\\").replace(/'/g, "\\'"); + + const escaped = escapeAirtableString(normalizedEmail); try { const records = await base('RawRoommateData') .select({ - filterByFormula: `{Email Address} = '${email}'`, + filterByFormula: `LOWER({Email Address}) = '${escaped}'`, }) .all(); - const roommateData = records.map(record => ({ - ...record.fields - })); + const allowedKeys = [ + 'Submission ID', + 'Last updated', + 'Submission started', + 'Status', + 'Current step', + 'Full Name', + 'Email Address', + 'Age', + 'Gender', + "Country you're from", + 'Short catch phrase that embodies your vibe (for others to see)', + 'Your Phone Number', + "WeChat Contact (put - if you do not have one yet or INDIA if you're in India)", + 'Slack Handle', + 'Favorite Game', + 'Favorite Food', + 'Favorite Flavor of Juice', + 'Mandarin speaking ability', + "Name of the game you're making", + 'Link to your game' + ]; + + const roommateData = records.map(record => { + const fields = record.fields || {}; + const out = {}; + for (const k of allowedKeys) { + if (Object.prototype.hasOwnProperty.call(fields, k)) { + out[k] = fields[k]; + } + } + return out; + }); res.status(200).json(roommateData); } catch (error) { console.error('Error fetching roommate data:', error); res.status(500).json({ message: 'Error fetching roommate data' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/get-user-omg-moments.js b/site/pages/api/get-user-omg-moments.js index 62dfeab4..762b3243 100644 --- a/site/pages/api/get-user-omg-moments.js +++ b/site/pages/api/get-user-omg-moments.js @@ -1,13 +1,18 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'GET') { return res.status(405).json({ message: 'Method not allowed' }); } const { email } = req.query; + const sanitisedEmail = escapeAirtableString(email); if (!email) { return res.status(400).json({ message: 'Email is required' }); @@ -16,7 +21,7 @@ export default async function handler(req, res) { try { // Get all OMG moments for this user const records = await base('omgMoments').select({ - filterByFormula: `{email} = '${email}'`, + filterByFormula: `{email} = '${sanitisedEmail}'`, sort: [{ field: 'created_at', direction: 'desc' }] }).all(); @@ -35,4 +40,4 @@ export default async function handler(req, res) { console.error('Error fetching moments:', error); return res.status(500).json({ message: 'Error fetching moments' }); } -} +}); diff --git a/site/pages/api/give-kudos.js b/site/pages/api/give-kudos.js index 29c6018f..62a9fde7 100644 --- a/site/pages/api/give-kudos.js +++ b/site/pages/api/give-kudos.js @@ -1,63 +1,70 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); + +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); const KUDOS_LIMIT = 100; -export default async function handler(req, res) { - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }); - } +export default withAuth(async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } - const { momentId } = req.body; - if (!momentId) { - return res.status(400).json({ error: 'Missing momentId' }); - } + const { momentId } = req.body; + const sanitisedMomentId = escapeAirtableString(momentId); - try { - // Get the current moment data - const records = await base('omgMoments').select({ - filterByFormula: `RECORD_ID() = '${momentId}'`, - maxRecords: 1 - }).firstPage(); + if (!sanitisedMomentId) { + return res.status(400).json({ error: 'Missing momentId' }); + } - if (!records || records.length === 0) { - throw new Error('Moment not found'); - } + try { + // Get the current moment data + const records = await base('omgMoments').select({ + filterByFormula: `RECORD_ID() = '${sanitisedMomentId}'`, + maxRecords: 1 + }).firstPage(); - const record = records[0]; - const currentKudos = record.fields.kudos || 0; + if (!records || records.length === 0) { + throw new Error('Moment not found'); + } - // Check if we've hit the kudos limit - if (currentKudos >= KUDOS_LIMIT) { - return res.status(200).json({ - kudos: currentKudos, - message: 'Kudos limit reached', - limitReached: true - }); - } + const record = records[0]; + const currentKudos = record.fields.kudos || 0; + + // Check if we've hit the kudos limit + if (currentKudos >= KUDOS_LIMIT) { + return res.status(200).json({ + kudos: currentKudos, + message: 'Kudos limit reached', + limitReached: true + }); + } - // Update the kudos count - const updatedRecords = await base('omgMoments').update([ - { - id: momentId, - fields: { - kudos: Math.min(KUDOS_LIMIT, currentKudos + 1) - } - } - ]); - - if (!updatedRecords || updatedRecords.length === 0) { - throw new Error('Failed to update kudos'); + // Update the kudos count + const updatedRecords = await base('omgMoments').update([ + { + id: momentId, + fields: { + kudos: Math.min(KUDOS_LIMIT, currentKudos + 1) } + } + ]); - const newKudos = updatedRecords[0].fields.kudos; - return res.status(200).json({ - kudos: newKudos, - limitReached: newKudos >= KUDOS_LIMIT - }); - } catch (error) { - console.error('Error updating kudos:', error); - return res.status(500).json({ error: 'Failed to update kudos' }); + if (!updatedRecords || updatedRecords.length === 0) { + throw new Error('Failed to update kudos'); } -} \ No newline at end of file + + const newKudos = updatedRecords[0].fields.kudos; + return res.status(200).json({ + kudos: newKudos, + limitReached: newKudos >= KUDOS_LIMIT + }); + } catch (error) { + console.error('Error updating kudos:', error); + return res.status(500).json({ error: 'Failed to update kudos' }); + } +}); \ No newline at end of file diff --git a/site/pages/api/invite.js b/site/pages/api/invite.js index b90b2a8a..497c191e 100644 --- a/site/pages/api/invite.js +++ b/site/pages/api/invite.js @@ -1,17 +1,15 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; -const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } - const authToken = req.headers.authorization?.split(' ')[1]; - if (!authToken) { - return res.status(401).json({ message: 'No auth token provided' }); - } - try { const { invitedParticipantEmail, flavor } = req.body; @@ -19,21 +17,10 @@ export default async function handler(req, res) { return res.status(400).json({ message: 'Email and flavor are required' }); } - // First, get the sender's record from their auth token - const userRecords = await base('signups').select({ - filterByFormula: `{token} = '${authToken}'`, - maxRecords: 1 - }).firstPage(); - - if (userRecords.length === 0) { - return res.status(404).json({ message: `Sender not found, ${authToken}` }); - } - - const userRecord = userRecords[0]; - const senderEmail = userRecord.fields.email || ""; - const invitesAvailable = userRecord.fields.invitesAvailable || []; + const senderEmail = req.user.email || ""; + const invitesAvailable = req.user.invitesAvailable || []; - // Check if user has the specified flavor invite available + // Check if user has the specified flavor invite available if (!invitesAvailable.includes(flavor)) { return res.status(400).json({ message: 'Invite flavor not available' }); } @@ -44,14 +31,14 @@ export default async function handler(req, res) { // Update the user's record with remaining invites await base('signups').update([ { - id: userRecord.id, + id: req.userId, fields: { invitesAvailable: updatedInvites } } ]); - // Create the invite record + // Create the invite record const record = await base('Invites').create([ { fields: { @@ -62,16 +49,16 @@ export default async function handler(req, res) { } ]); - return res.status(200).json({ - success: true, + return res.status(200).json({ + success: true, record, - remainingInvites: updatedInvites + remainingInvites: updatedInvites }); } catch (error) { console.error('Invite creation error:', error); - return res.status(500).json({ + return res.status(500).json({ message: error.message || 'Error creating invite', error: error.error || 'UNKNOWN_ERROR' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/load-fruit-data.js b/site/pages/api/load-fruit-data.js index 86f089d7..fffe8dda 100644 --- a/site/pages/api/load-fruit-data.js +++ b/site/pages/api/load-fruit-data.js @@ -1,38 +1,26 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; - - const jungleStretches = (await base('jungleStretches').select({ - filterByFormula: ` + filterByFormula: ` AND( - {email (from Signups)} = '${signupRecord.fields.email}', + {email (from Signups)} = '${req.user.email}', ({endtime}), NOT({isCanceled}) ) `, - }).firstPage()).map((record) => record.fields); + }).firstPage()).map((record) => record.fields); if(jungleStretches.length === 0) { res.status(200).json({}); @@ -59,4 +47,4 @@ export default async function handler(req, res) { console.error('Error loading jungle stretch:', error); res.status(500).json({ message: 'Error loading jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/load-juice-data.js b/site/pages/api/load-juice-data.js index ed1d828b..34df248d 100644 --- a/site/pages/api/load-juice-data.js +++ b/site/pages/api/load-juice-data.js @@ -1,33 +1,21 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; - - const stretchRecordsData = (await base("juiceStretches").select({ - filterByFormula: ` + filterByFormula: ` AND( - {email (from Signups)} = '${signupRecord.fields.email}', + {email (from Signups)} = '${req.user.email}', NOT({endtime}), NOT({isCanceled}), {pauseTimeStart} @@ -41,7 +29,7 @@ export default async function handler(req, res) { } const lastRecord = stretchRecordsData[0] - + console.log(lastRecord) const previousPauseTime = lastRecord.totalPauseTimeSeconds == undefined ? 0 : lastRecord.totalPauseTimeSeconds const newPauseTime = Math.round(previousPauseTime + Math.abs(new Date() - new Date(lastRecord.pauseTimeStart))/1000) @@ -51,4 +39,4 @@ export default async function handler(req, res) { console.error('Error loading juice stretch:', error); res.status(500).json({ message: 'Error loading juice stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/load-jungle-data.js b/site/pages/api/load-jungle-data.js index de43a014..d46e3ee8 100644 --- a/site/pages/api/load-jungle-data.js +++ b/site/pages/api/load-jungle-data.js @@ -1,33 +1,21 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; - - const stretchRecordsData = (await base("jungleStretches").select({ - filterByFormula: ` + filterByFormula: ` AND( - {email (from Signups)} = '${signupRecord.fields.email}', + {email (from Signups)} = '${req.user.email}', NOT({endtime}), NOT({isCanceled}), {pauseTimeStart} @@ -41,16 +29,16 @@ export default async function handler(req, res) { } const lastRecord = stretchRecordsData[0] - + console.log(lastRecord) const previousPauseTime = lastRecord.totalPauseTimeSeconds == undefined ? 0 : lastRecord.totalPauseTimeSeconds const newPauseTime = Math.round(previousPauseTime + Math.abs(new Date() - new Date(lastRecord.pauseTimeStart))/1000) res.status(200).json({ id: lastRecord.ID, startTime: lastRecord.startTime, totalPauseTimeSeconds: newPauseTime, kiwisCollected: lastRecord.kiwisCollected, lemonsCollected: lastRecord.lemonsCollected, orangesCollected: lastRecord.orangesCollected, applesCollected : lastRecord.applesCollected, blueberriesCollected: lastRecord.blueberriesCollected }); - + } catch (error) { console.error('Error loading jungle stretch:', error); res.status(500).json({ message: 'Error loading jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/pause-juice-stretch.js b/site/pages/api/pause-juice-stretch.js index a38caa7c..973d6897 100644 --- a/site/pages/api/pause-juice-stretch.js +++ b/site/pages/api/pause-juice-stretch.js @@ -1,32 +1,27 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, stretchId } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } + const { stretchId } = req.body; - const signupRecord = signupRecords[0]; + const sanitisedStretchId = escapeAirtableString(stretchId); const records = await base('juiceStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedStretchId}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'Juice stretch not found' }); @@ -46,4 +41,4 @@ export default async function handler(req, res) { console.error('Error pausing juice stretch:', error); res.status(500).json({ message: 'Error pausing juice stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/pause-jungle-stretch.js b/site/pages/api/pause-jungle-stretch.js index 98672de1..028c241e 100644 --- a/site/pages/api/pause-jungle-stretch.js +++ b/site/pages/api/pause-jungle-stretch.js @@ -1,32 +1,26 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, stretchId } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; + const { stretchId } = req.body; + const sanitisedStretchId = escapeAirtableString(stretchId); const records = await base('jungleStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedStretchId}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'jungle stretch not found' }); @@ -131,7 +125,6 @@ export default async function handler(req, res) { acc.push(sum); return acc; }, []); - const random = Math.random(); // Select a fruit based on the random number and cumulative probabilities @@ -200,7 +193,7 @@ export default async function handler(req, res) { } } } - + await base('jungleStretches').update([ { @@ -216,4 +209,4 @@ export default async function handler(req, res) { console.error('Error pausing jungle stretch:', error); res.status(500).json({ message: 'Error pausing jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/register.js b/site/pages/api/register.js index 9840eb7d..207b9943 100644 --- a/site/pages/api/register.js +++ b/site/pages/api/register.js @@ -17,7 +17,7 @@ export default async function handler(req, res) { return res.status(400).json({ message: 'Email is required' }); } - // Normalize email to lowercase for consistent comparison + // Normalize email to lowercase for consistent comparison const normalizedEmail = email.toLowerCase(); // Check if email exists in Signups table @@ -44,34 +44,26 @@ export default async function handler(req, res) { ]); } - return res.status(200).json({ - success: true, + return res.status(200).json({ + success: true, message: 'Token resend requested', - isResend: true + isResend: true }); } - // Create new signup record if email doesn't exist - const record = await base("Signups").create([ - { - fields: { - email: normalizedEmail - } - } - ]); - - return res.status(200).json({ - success: true, - record, - isResend: false + return res.status(403).json({ + success: false, + message: + 'New signups are disabled. If you already have an account, please check your email spelling.', + error: 'SIGNUPS_DISABLED', }); } catch (error) { console.error('Registration error:', error); // Send more specific error message back to client - return res.status(500).json({ + return res.status(500).json({ success: false, message: error.message || 'Error processing registration', error: error.error || 'UNKNOWN_ERROR' }); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/site/pages/api/resume-juice-stretch.js b/site/pages/api/resume-juice-stretch.js index 5e728fff..a02a3a53 100644 --- a/site/pages/api/resume-juice-stretch.js +++ b/site/pages/api/resume-juice-stretch.js @@ -1,32 +1,26 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, stretchId } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; + const { stretchId } = req.body; + const sanitisedStretchId = escapeAirtableString(stretchId); const records = await base('juiceStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedStretchId}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'Juice stretch not found' }); @@ -48,4 +42,4 @@ export default async function handler(req, res) { console.error('Error resuming juice stretch:', error); res.status(500).json({ message: 'Error resuming juice stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/resume-jungle-stretch.js b/site/pages/api/resume-jungle-stretch.js index 069d6886..2cc413e1 100644 --- a/site/pages/api/resume-jungle-stretch.js +++ b/site/pages/api/resume-jungle-stretch.js @@ -1,32 +1,26 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { - const { token, stretchId } = req.body; - - // Get user's email from Signups table - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ message: 'User not found' }); - } - - const signupRecord = signupRecords[0]; + const { stretchId } = req.body; + const sanitisedStretchId = escapeAirtableString(stretchId); const records = await base('jungleStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedStretchId}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'jungle stretch not found' }); @@ -48,4 +42,4 @@ export default async function handler(req, res) { console.error('Error resuming jungle stretch:', error); res.status(500).json({ message: 'Error resuming jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/roommate-pairing.js b/site/pages/api/roommate-pairing.js index 57e4ea53..98ad5158 100644 --- a/site/pages/api/roommate-pairing.js +++ b/site/pages/api/roommate-pairing.js @@ -1,11 +1,11 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; -// Initialize Airtable const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } @@ -14,8 +14,8 @@ export default async function handler(req, res) { // Get all records from RawRoommateData including bed sharing preference const allRoommatesQuery = base('RawRoommateData').select({ fields: [ - 'Email Address', - 'Full Name', + 'Email Address', + 'Full Name', 'Name of person you are sharing room (they must indicate you on the form for it to be a match) (FULL NAME)', 'Gender', 'Age', @@ -29,7 +29,7 @@ export default async function handler(req, res) { fields: ['Roommate A', 'Roommate B'] }); const existingPairs = await existingPairsQuery.all(); - + // Track people who already have rooms const peopleWithRooms = new Set(); existingPairs.forEach(pair => { @@ -64,10 +64,10 @@ export default async function handler(req, res) { if (!name1 || !name2) return false; const n1 = normalizeName(name1); const n2 = normalizeName(name2); - + // Exact match if (n1 === n2) return true; - + // Handle common variations const variations1 = [ n1, @@ -75,14 +75,14 @@ export default async function handler(req, res) { n1.split(' ').filter(n => n.length > 1).join(' '), // Remove single letters n1.split(' ').map(n => n[0]).join('') // Initials ]; - + const variations2 = [ n2, n2.replace(/\s+/g, ' '), n2.split(' ').filter(n => n.length > 1).join(' '), n2.split(' ').map(n => n[0]).join('') ]; - + return variations1.some(v1 => variations2.some(v2 => v1 === v2)); } @@ -99,7 +99,7 @@ export default async function handler(req, res) { // Handle multiple preferences const preferences = preferredName.split(/[,/]/).map(p => p.trim()).filter(p => p); - + for (const pref of preferences) { // Skip invalid preferences if (pref.length > 100 || /[<>{}]/.test(pref)) continue; @@ -117,12 +117,12 @@ export default async function handler(req, res) { if (!bPreferredName) continue; const bPreferences = bPreferredName.split(/[,/]/).map(p => p.trim()).filter(p => p); - + // Check if B has A in their preferences const isMutualMatch = bPreferences.some(bPref => namesMatch(bPref, roommateA.fields['Full Name']) ); - + if (isMutualMatch) { pairs.push({ roommateA: roommateA, @@ -167,7 +167,7 @@ export default async function handler(req, res) { if (queenA !== queenB) { return queenB - queenA; // Put comfortable people first } - + // Then sort by age const ageA = parseInt(a.fields['Age']) || 0; const ageB = parseInt(b.fields['Age']) || 0; @@ -180,7 +180,7 @@ export default async function handler(req, res) { const age1 = parseInt(roommates[i].fields['Age']) || 0; const queen1 = isComfortableWithQueenBed(roommates[i]); - + for (let j = i + 1; j < roommates.length; j++) { if (processedIds.has(roommates[j].id)) continue; @@ -276,4 +276,4 @@ export default async function handler(req, res) { error: error.message }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/rsvp.js b/site/pages/api/rsvp.js index 0c6743d4..90fbf532 100644 --- a/site/pages/api/rsvp.js +++ b/site/pages/api/rsvp.js @@ -1,35 +1,20 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; -const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(process.env.AIRTABLE_BASE_ID); +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { - const authToken = req.headers.authorization?.split(' ')[1]; - if (!authToken) { - return res.status(401).json({ error: 'No auth token provided' }); - } - - // First find the email from Signups table using the token - const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${authToken}'`, - maxRecords: 1 - }).firstPage(); - - if (!signupRecords || signupRecords.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - const userEmail = signupRecords[0].fields.email; - - // Create RSVP record in Airtable with the found email const record = await base('RSVP').create([ { fields: { - Email: userEmail, + Email: req.user.email, Meeting: 'Kickoff' } } @@ -40,4 +25,4 @@ export default async function handler(req, res) { console.error('RSVP Error:', error); return res.status(500).json({ error: 'Failed to create RSVP' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/start-juice-stretch.js b/site/pages/api/start-juice-stretch.js index 2af3b52c..24119eeb 100644 --- a/site/pages/api/start-juice-stretch.js +++ b/site/pages/api/start-juice-stretch.js @@ -1,19 +1,25 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { const { token } = req.body; + const sanitisedToken = escapeAirtableString(token); // Get user's email from Signups table const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, + filterByFormula: `{token} = '${sanitisedToken}'`, maxRecords: 1 }).firstPage(); @@ -40,4 +46,4 @@ export default async function handler(req, res) { console.error('Error starting juice stretch:', error); res.status(500).json({ message: 'Error starting juice stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/start-jungle-stretch.js b/site/pages/api/start-jungle-stretch.js index a448004c..35a5f011 100644 --- a/site/pages/api/start-jungle-stretch.js +++ b/site/pages/api/start-jungle-stretch.js @@ -1,19 +1,25 @@ import Airtable from 'airtable'; import { v4 as uuidv4 } from 'uuid'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { const { token } = req.body; + const sanitisedToken = escapeAirtableString(token); // Get user's email from Signups table const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, + filterByFormula: `{token} = '${sanitisedToken}'`, maxRecords: 1 }).firstPage(); @@ -40,4 +46,4 @@ export default async function handler(req, res) { console.error('Error starting jungle stretch:', error); res.status(500).json({ message: 'Error starting jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/stop-juice-stretch.js b/site/pages/api/stop-juice-stretch.js index 4fcfb0ae..06205a82 100644 --- a/site/pages/api/stop-juice-stretch.js +++ b/site/pages/api/stop-juice-stretch.js @@ -1,20 +1,26 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { const { stretchId } = req.body; - + const sanitisedStretchId = escapeAirtableString(stretchId); + // Find the record with matching ID const records = await base('juiceStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedStretchId}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'Juice stretch not found' }); @@ -35,4 +41,4 @@ export default async function handler(req, res) { console.error('Error stopping juice stretch:', error); res.status(500).json({ message: 'Error stopping juice stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/stop-jungle-stretch.js b/site/pages/api/stop-jungle-stretch.js index 3d8b9b14..90d0a6ff 100644 --- a/site/pages/api/stop-jungle-stretch.js +++ b/site/pages/api/stop-jungle-stretch.js @@ -1,20 +1,26 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { const { stretchId } = req.body; - + const sanitisedStretchId = escapeAirtableString(stretchId); + // Find the record with matching ID const records = await base('jungleStretches').select({ - filterByFormula: `{ID} = '${stretchId}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{ID} = '${sanitisedStretchId}'`, + maxRecords: 1 + }).firstPage(); if (!records || records.length === 0) { return res.status(404).json({ message: 'jungle stretch not found' }); @@ -35,4 +41,4 @@ export default async function handler(req, res) { console.error('Error stopping jungle stretch:', error); res.status(500).json({ message: 'Error stopping jungle stretch' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/submit-address.js b/site/pages/api/submit-address.js index 8d0f5648..be852c3f 100644 --- a/site/pages/api/submit-address.js +++ b/site/pages/api/submit-address.js @@ -1,17 +1,21 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' + -// Initialize Airtable const base = new Airtable({ - apiKey: process.env.AIRTABLE_API_KEY + apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { const { token, mailingAddress } = req.body; + const sanitisedToken = escapeAirtableString(token); + const sanitisedMailingAddress = escapeAirtableString(mailingAddress); if (!token || !mailingAddress) { return res.status(400).json({ message: 'Token and mailing address are required' }); @@ -19,7 +23,7 @@ export default async function handler(req, res) { // Find user by token const records = await base("Signups").select({ - filterByFormula: `{token} = '${token}'`, + filterByFormula: `{token} = '${sanitisedToken}'`, maxRecords: 1 }).firstPage(); @@ -34,7 +38,7 @@ export default async function handler(req, res) { { id: record.id, fields: { - Address: mailingAddress + Address: sanitisedMailingAddress } } ]); @@ -52,4 +56,4 @@ export default async function handler(req, res) { error: error.error || 'UNKNOWN_ERROR' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/submit-final-ship.js b/site/pages/api/submit-final-ship.js index c453b2b0..ef733cb2 100644 --- a/site/pages/api/submit-final-ship.js +++ b/site/pages/api/submit-final-ship.js @@ -1,8 +1,9 @@ import Airtable from 'airtable'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' + -// Initialize Airtable const base = new Airtable({ - apiKey: process.env.AIRTABLE_API_KEY + apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); export default async function handler(req, res) { @@ -11,7 +12,7 @@ export default async function handler(req, res) { } try { - const { + const { codeUrl, playableUrl, videoURL, @@ -64,7 +65,6 @@ export default async function handler(req, res) { })); if (missingFields.length > 0) { - // Group missing fields by page const groupedByPage = missingFields.reduce((acc, field) => { if (!acc[field.page]) { acc[field.page] = []; @@ -73,18 +73,17 @@ export default async function handler(req, res) { return acc; }, {}); - return res.status(400).json({ + return res.status(400).json({ success: false, message: 'Required fields are missing', missingFields: groupedByPage }); } - // Check for existing record with the same email const existingRecords = await base('YSWS Project Submission').select({ - filterByFormula: `{Email} = '${email}'`, - maxRecords: 1 - }).firstPage(); + filterByFormula: `{Email} = '${email}'`, + maxRecords: 1 + }).firstPage(); const submissionData = { fields: { @@ -116,7 +115,6 @@ export default async function handler(req, res) { let record; if (existingRecords && existingRecords.length > 0) { - // Update existing record record = await base('YSWS Project Submission').update([ { id: existingRecords[0].id, @@ -124,24 +122,24 @@ export default async function handler(req, res) { } ]); } else { - // Create new record record = await base('YSWS Project Submission').create([submissionData]); } - return res.status(200).json({ + return res.status(200).json({ success: true, - message: existingRecords && existingRecords.length > 0 ? - "Project updated successfully!" : - "Project submitted successfully!", - record: record[0] + message: + existingRecords && existingRecords.length > 0 + ? "Project updated successfully!" + : "Project submitted successfully!", + record: record[0], }); } catch (error) { console.error('YSWS submission error:', error); - return res.status(500).json({ + return res.status(500).json({ success: false, message: error.message || 'Error processing submission', error: error.error || 'UNKNOWN_ERROR' }); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/site/pages/api/submit-pr.js b/site/pages/api/submit-pr.js index 3310a056..85351b09 100644 --- a/site/pages/api/submit-pr.js +++ b/site/pages/api/submit-pr.js @@ -1,17 +1,21 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' + -// Initialize Airtable const base = new Airtable({ - apiKey: process.env.AIRTABLE_API_KEY + apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { const { token, prLink } = req.body; + const sanitisedToken = escapeAirtableString(token); + const sanitisedPrLink = escapeAirtableString(prLink); if (!token || !prLink) { return res.status(400).json({ message: 'Token and PR link are required' }); @@ -19,7 +23,7 @@ export default async function handler(req, res) { // Find user by token const records = await base("Signups").select({ - filterByFormula: `{token} = '${token}'`, + filterByFormula: `{token} = '${sanitisedToken}'`, maxRecords: 1 }).firstPage(); @@ -34,7 +38,7 @@ export default async function handler(req, res) { { id: record.id, fields: { - game_pr: prLink, + game_pr: sanitisedPrLink, achievements: [...(record?.fields?.achievements || []), 'pr_submitted'] } } @@ -53,4 +57,4 @@ export default async function handler(req, res) { error: error.error || 'UNKNOWN_ERROR' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/submit-second-challenge.js b/site/pages/api/submit-second-challenge.js index 92b80caf..d2d56bc9 100644 --- a/site/pages/api/submit-second-challenge.js +++ b/site/pages/api/submit-second-challenge.js @@ -1,17 +1,22 @@ -import Airtable from 'airtable'; +import Airtable from "airtable"; +import { withAuth } from "./_middleware"; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' + -// Initialize Airtable const base = new Airtable({ - apiKey: process.env.AIRTABLE_API_KEY + apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { - if (req.method !== 'POST') { - return res.status(405).json({ message: 'Method not allowed' }); +export default withAuth(async function handler(req, res) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); } try { const { token, itchLink, platforms } = req.body; + const sanitisedToken = escapeAirtableString(token); + const sanitisedItchLink = escapeAirtableString(itchLink); + const sanitisedPlatforms = platforms.map(platform => escapeAirtableString(platform)); if (!token || !itchLink || !platforms?.length) { return res.status(400).json({ message: 'Token, itch.io link, and platforms are required' }); @@ -19,7 +24,7 @@ export default async function handler(req, res) { // First find user by token const userRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, + filterByFormula: `{token} = '${sanitisedToken}'`, maxRecords: 1 }).firstPage(); @@ -45,8 +50,8 @@ export default async function handler(req, res) { const shipRecord = await base('Ships').create([ { fields: { - Link: itchLink, - Platforms: platforms, + Link: sanitisedItchLink, + Platforms: sanitisedPlatforms, user: [userRecord.id], Type: 'base-mechanic', omgMomentsThatWentIntoThis: omgMoments.map(record => record.id) @@ -78,4 +83,4 @@ export default async function handler(req, res) { error: error.error || 'UNKNOWN_ERROR' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/submit-v1-challenge.js b/site/pages/api/submit-v1-challenge.js index 41b78cbc..056189df 100644 --- a/site/pages/api/submit-v1-challenge.js +++ b/site/pages/api/submit-v1-challenge.js @@ -1,11 +1,13 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' + -// Initialize Airtable const base = new Airtable({ - apiKey: process.env.AIRTABLE_API_KEY + apiKey: process.env.AIRTABLE_API_KEY, }).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } @@ -21,8 +23,12 @@ export default async function handler(req, res) { } // First find user by token + const sanitisedToken = escapeAirtableString(token); + const sanitisedGameWebsiteUrl = escapeAirtableString(gameWebsiteUrl); + const sanitisedGithubLink = escapeAirtableString(githubLink); + const userRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, + filterByFormula: `{token} = '${sanitisedToken}'`, maxRecords: 1 }).firstPage(); @@ -46,7 +52,7 @@ export default async function handler(req, res) { const shipRecord = await base('Ships').create([ { fields: { - Link: gameWebsiteUrl, + Link: sanitisedGameWebsiteUrl, user: [userRecord.id], Type: 'v1', omgMomentsThatWentIntoThis: omgMoments.map(record => record.id) @@ -60,7 +66,7 @@ export default async function handler(req, res) { id: userRecord.id, fields: { achievements: [...(userRecord.fields.achievements || []), 'v1_submitted'], - GitHubLink: githubLink + GitHubLink: sanitisedGithubLink } } ]); @@ -79,4 +85,4 @@ export default async function handler(req, res) { error: error.error || 'UNKNOWN_ERROR' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/sync-tamagotchi-days.js b/site/pages/api/sync-tamagotchi-days.js index 1d4386e9..dd3c548c 100644 --- a/site/pages/api/sync-tamagotchi-days.js +++ b/site/pages/api/sync-tamagotchi-days.js @@ -1,18 +1,25 @@ import Airtable from 'airtable'; +import { withAuth } from './_middleware'; +import {escapeAirtableString, normalizeEmail, isValidEmail} from '../../lib/airtable-utils' -const base = new Airtable({apiKey: process.env.AIRTABLE_API_KEY}).base(process.env.AIRTABLE_BASE_ID); -export default async function handler(req, res) { +const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.AIRTABLE_BASE_ID, +); + +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { const { token } = req.body; + + const sanitisedToken = escapeAirtableString(token); // Get user's record from Signups table const signupRecords = await base('Signups').select({ - filterByFormula: `{token} = '${token}'`, + filterByFormula: `{token} = '${sanitisedToken}'`, maxRecords: 1 }).firstPage(); @@ -36,10 +43,10 @@ export default async function handler(req, res) { const tamagotchi = tamagotchiRecords[0]; const startDate = new Date(tamagotchi.fields.startDate); const now = new Date(); - + // Calculate current day number (1-indexed) const dayNumber = Math.floor((now - startDate) / (24 * 60 * 60 * 1000)) + 1; - + // Get all stretches (both juice and jungle) const juiceStretches = await base('juiceStretches').select({ filterByFormula: `AND({email (from Signups)} = '${userEmail}', NOT({isCanceled}))` @@ -53,73 +60,73 @@ export default async function handler(req, res) { const omgMoments = await base('omgMoments').select({ filterByFormula: `{email} = '${userEmail}'` }).all(); - + // Calculate total hours for today const todayStart = new Date(now); todayStart.setHours(0, 0, 0, 0); - + let todayJuiceHours = 0; let todayJungleHours = 0; - + // Process juice stretches for today juiceStretches.forEach(stretch => { if (!stretch.fields.endTime) return; // Skip ongoing stretches - + const stretchEnd = new Date(stretch.fields.endTime); if (stretchEnd >= todayStart && stretchEnd <= now) { const stretchStart = new Date(stretch.fields.startTime); const pauseTimeSeconds = stretch.fields.totalPauseTimeSeconds || 0; - + // Calculate hours worked (accounting for pause time) const totalTimeMs = stretchEnd - stretchStart; const activeTimeMs = totalTimeMs - (pauseTimeSeconds * 1000); const hoursWorked = activeTimeMs / (1000 * 60 * 60); - + todayJuiceHours += hoursWorked; } }); - + // Process jungle stretches for today jungleStretches.forEach(stretch => { if (!stretch.fields.endTime) return; // Skip ongoing stretches - + const stretchEnd = new Date(stretch.fields.endTime); if (stretchEnd >= todayStart && stretchEnd <= now) { const stretchStart = new Date(stretch.fields.startTime); const pauseTimeSeconds = stretch.fields.totalPauseTimeSeconds || 0; - + // Calculate hours worked (accounting for pause time) const totalTimeMs = stretchEnd - stretchStart; const activeTimeMs = totalTimeMs - (pauseTimeSeconds * 1000); const hoursWorked = activeTimeMs / (1000 * 60 * 60); - + todayJungleHours += hoursWorked; } }); - + const todayTotalHours = todayJuiceHours + todayJungleHours; - + // Organize OMG moments by day const dayOmgMoments = {}; const daysWithOmgMoments = new Set(); - + // Initialize day data structure for up to 10 days const maxDays = Math.min(10, dayNumber); for (let i = 1; i <= maxDays; i++) { const dayStart = new Date(startDate); dayStart.setDate(dayStart.getDate() + (i - 1)); dayStart.setHours(0, 0, 0, 0); - + const dayEnd = new Date(startDate); dayEnd.setDate(dayEnd.getDate() + i); dayEnd.setHours(0, 0, 0, 0); - + dayOmgMoments[`Day${i}`] = []; - + // Find OMG moments for this day omgMoments.forEach(moment => { if (!moment.fields.created_at) return; - + const momentDate = new Date(moment.fields.created_at); if (momentDate >= dayStart && momentDate < dayEnd) { dayOmgMoments[`Day${i}`].push(moment.id); @@ -127,14 +134,14 @@ export default async function handler(req, res) { } }); } - + // Calculate streak based on consecutive days with OMG moments let currentStreak = 0; let maxStreak = 0; - + // Check if today has an OMG moment const todayHasOmgMoment = daysWithOmgMoments.has(dayNumber); - + // Calculate the streak for (let i = dayNumber; i >= 1; i--) { if (daysWithOmgMoments.has(i)) { @@ -143,7 +150,7 @@ export default async function handler(req, res) { break; // Break on first day without OMG moment } } - + // If today doesn't have an OMG moment, check yesterday's streak if (!todayHasOmgMoment && dayNumber > 1) { let yesterdayStreak = 0; @@ -158,12 +165,12 @@ export default async function handler(req, res) { } else { maxStreak = currentStreak; } - + // Prepare update fields const updateFields = { streakNumber: maxStreak // Use the streak of consecutive days with OMG moments }; - + // Add OMG moments to day fields for (let i = 1; i <= maxDays; i++) { const dayKey = `Day${i}`; @@ -171,7 +178,7 @@ export default async function handler(req, res) { updateFields[dayKey] = dayOmgMoments[dayKey]; } } - + // Update Tamagotchi record with day data and OMG moments await base('Tamagotchi').update([ { @@ -179,10 +186,10 @@ export default async function handler(req, res) { fields: updateFields } ]); - + // Store the day data in localStorage instead of Airtable - res.status(200).json({ - success: true, + res.status(200).json({ + success: true, currentDay: dayNumber, streakDays: maxStreak, todayHours: { @@ -193,9 +200,9 @@ export default async function handler(req, res) { dayOmgMoments, daysWithOmgMoments: Array.from(daysWithOmgMoments) }); - + } catch (error) { console.error('Error syncing Tamagotchi days:', error); res.status(500).json({ message: 'Error syncing Tamagotchi days' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/upload-image.js b/site/pages/api/upload-image.js index 417404c1..d1ed19b7 100644 --- a/site/pages/api/upload-image.js +++ b/site/pages/api/upload-image.js @@ -1,36 +1,36 @@ import formidable from 'formidable'; import fs from 'fs'; +import { withAuth } from './_middleware'; export const config = { - api: { - bodyParser: false, - }, + api: { + bodyParser: false, + }, }; -export default async function handler(req, res) { - if (req.method !== 'POST') { - return res.status(405).json({ message: 'Method not allowed' }); - } - - try { - const form = formidable({}); - const [fields, files] = await form.parse(req); +export default withAuth(async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ message: 'Method not allowed' }); + } - if (!files.file || !files.file[0]) { - return res.status(400).json({ error: 'No file uploaded' }); - } + try { + const form = formidable({}); + const [fields, files] = await form.parse(req); - const file = files.file[0]; - const fileData = fs.readFileSync(file.filepath); - const base64Data = fileData.toString('base64'); + if (!files.file || !files.file[0]) { + return res.status(400).json({ error: 'No file uploaded' }); + } - // Return the base64 data that can be used in Airtable - return res.status(200).json({ - url: `data:${file.mimetype};base64,${base64Data}` - }); + const file = files.file[0]; + const fileData = fs.readFileSync(file.filepath); + const base64Data = fileData.toString('base64'); - } catch (error) { - console.error('Upload error:', error); - return res.status(500).json({ error: 'Error uploading file' }); - } -} \ No newline at end of file + // Return the base64 data that can be used in Airtable + return res.status(200).json({ + url: `data:${file.mimetype};base64,${base64Data}` + }); + } catch (error) { + console.error('Upload error:', error); + return res.status(500).json({ error: 'Error uploading file' }); + } +}); \ No newline at end of file diff --git a/site/pages/api/upload-s3.js b/site/pages/api/upload-s3.js index 39f8eb67..3f502965 100644 --- a/site/pages/api/upload-s3.js +++ b/site/pages/api/upload-s3.js @@ -1,6 +1,7 @@ import AWS from 'aws-sdk'; import formidable from 'formidable'; import fs from 'fs'; +import { withAuth } from './_middleware'; export const config = { api: { @@ -12,10 +13,10 @@ export const config = { const s3 = new AWS.S3({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - region: process.env.AWS_REGION + region: process.env.AWS_REGION, }); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } @@ -44,9 +45,9 @@ export default async function handler(req, res) { fs.unlinkSync(file.filepath); return res.status(200).json({ url: uploadResult.Location }); - + } catch (error) { console.error('Error uploading to S3:', error); return res.status(500).json({ error: 'Failed to upload image' }); } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/site/pages/api/upload/s3.js b/site/pages/api/upload/s3.js index eeeeae2e..c46ccd19 100644 --- a/site/pages/api/upload/s3.js +++ b/site/pages/api/upload/s3.js @@ -3,6 +3,7 @@ import formidable from 'formidable'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; +import { withAuth } from '../_middleware'; // Configure formidable to parse form data export const config = { @@ -24,48 +25,48 @@ console.log('S3 upload handler initialized with:', { bucket: process.env.AWS_S3_BUCKET_NAME || 'kodan-cdn' }); -export default async function handler(req, res) { +export default withAuth(async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { console.log('Received upload request'); - + // Parse the incoming form data using the updated formidable API const form = formidable({ keepExtensions: true, }); - + const [fields, files] = await new Promise((resolve, reject) => { form.parse(req, (err, fields, files) => { if (err) reject(err); resolve([fields, files]); }); }); - + // In newer versions of formidable, files is an object with arrays const file = files.file?.[0]; - + if (!file) { return res.status(400).json({ error: 'No file uploaded' }); } - + // Generate a unique file name const fileExtension = path.extname(file.originalFilename || file.originalName || '.jpg'); const randomString = crypto.randomBytes(16).toString('hex'); const fileName = `card-images/${randomString}-${Date.now()}${fileExtension}`; - + // Read the file - filepath might be named differently in newer versions const filePath = file.filepath || file.path; const fileContent = fs.readFileSync(filePath); - + console.log('File received:', file ? { name: file.originalFilename || file.originalName, size: fileContent.length, type: file.mimetype } : 'No file'); - + // Upload to S3 const params = { Bucket: process.env.AWS_S3_BUCKET_NAME || 'kodan-cdn', @@ -73,26 +74,26 @@ export default async function handler(req, res) { Body: fileContent, ContentType: file.mimetype || 'image/jpeg', }; - + console.log('Uploading to S3 with params:', { Bucket: params.Bucket, Key: params.Key, ContentType: params.ContentType, Size: fileContent.length }); - + await s3.upload(params).promise(); - + // Generate the URL where the file is accessible const imageUrl = `https://${params.Bucket}.s3.${process.env.AWS_REGION}.amazonaws.com/${fileName}`; - + console.log('Upload successful, returning URL:', imageUrl); - + // Return the URL to the client return res.status(200).json({ imageUrl }); - + } catch (error) { console.error('Error uploading to S3:', error); return res.status(500).json({ error: 'Failed to upload image', details: error.message }); } -} \ No newline at end of file +}); \ No newline at end of file