Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions site/lib/airtable-utils.js
Original file line number Diff line number Diff line change
@@ -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,
};
1 change: 1 addition & 0 deletions site/pages/api/GenerateChinaLandingPDF.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
60 changes: 60 additions & 0 deletions site/pages/api/_middleware.js
Original file line number Diff line number Diff line change
@@ -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);
};
}
31 changes: 13 additions & 18 deletions site/pages/api/cancel-juice-stretch.js
Original file line number Diff line number Diff line change
@@ -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' });
Expand All @@ -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' });
}
}
});
32 changes: 12 additions & 20 deletions site/pages/api/cancel-jungle-stretch.js
Original file line number Diff line number Diff line change
@@ -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' });
Expand All @@ -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' });
}
}
});
36 changes: 16 additions & 20 deletions site/pages/api/create-omg-moment.js
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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' });
}
}
});
32 changes: 8 additions & 24 deletions site/pages/api/create-tamagotchi.js
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -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' });
}
}
});
5 changes: 3 additions & 2 deletions site/pages/api/deck/add.js
Original file line number Diff line number Diff line change
@@ -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' });
}
Expand Down Expand Up @@ -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' });
}
}
});
27 changes: 6 additions & 21 deletions site/pages/api/delete-tamagotchi.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,28 @@
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 });
} catch (error) {
console.error('Error deleting Tamagotchi:', error);
res.status(500).json({ message: 'Error deleting Tamagotchi' });
}
}
});
Loading