Skip to content
Closed
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
24 changes: 23 additions & 1 deletion src/lib/sf/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ const {
export const WISHLIST_RECORD_TYPE_ID = '012Vj000008tfPKIAY';
export const RFP_RECORD_TYPE_ID = '012Vj000008tfPJIAY';

/**
* Get Salesforce credentials securely
* @throws Error if credentials are not configured
*/
const getSalesforceCredentials = () => {
if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
throw new Error('Salesforce credentials not configured');
}
return {
username: SF_PROD_USERNAME,
password: `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`
};
};

/**
* Generate a JWT token for CSAT submission
* @param applicationId - The Salesforce Application ID
Expand Down Expand Up @@ -57,7 +71,8 @@ const createConnection = (): jsforce.Connection => {

const loginToSalesforce = (conn: jsforce.Connection): Promise<void> => {
return new Promise((resolve, reject) => {
conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
const { username, password } = getSalesforceCredentials();
conn.login(username, password, err => {
if (err) {
console.error('Salesforce login error:', err);
return reject(err);
Expand Down Expand Up @@ -105,6 +120,13 @@ const getFieldsForType = (type?: GrantInitiativeType): string => {
*/
export function getGrantInitiativeItems(type?: GrantInitiativeType) {
return new Promise<GrantInitiative[]>(async (resolve, reject) => {
// During build time, credentials may not be available
// Return empty array to allow build to succeed with fallback: 'blocking'
if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
console.warn('Salesforce credentials not configured, returning empty grant initiatives (build-time fallback)');
return resolve([]);
}

const conn = createConnection();

try {
Expand Down
12 changes: 10 additions & 2 deletions src/middlewares/multipartyParse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
export const multipartyParse =
(handler: NextApiHandler, options: formidable.Options) =>
(req: NextApiRequest, res: NextApiResponse) => {
const maxSize = options.maxFileSize || 10 * 1024 * 1024;
const contentLength = parseInt(req.headers['content-length'] || '0', 10);

if (contentLength > maxSize) {
res.status(413).json({ status: 'fail', error: 'File too large' });
return Promise.resolve();
}

const form = formidable(options);

return new Promise<void>(resolve => {
form.parse(req, async (err, fields, files) => {
if (err) {
console.error(err);
res.status(400).json({ status: 'fail' });
console.error('Form parsing error:', err);
res.status(400).json({ status: 'fail', error: 'Invalid form data' });
return resolve();
}

Expand Down
14 changes: 13 additions & 1 deletion src/middlewares/sanitizeFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import { isURL } from '../utils';

const fieldsToSanitize = ['firstName', 'lastName', 'company'];
const numericFields = ['budgetRequest', 'requestedAmount', 'expectedAttendees'];

function removeURLs(text: string) {
return text
Expand All @@ -11,16 +12,27 @@ function removeURLs(text: string) {
.join(' ');
}

function sanitizeNumeric(value: any): number | string {
if (typeof value === 'number') return value;
const parsed = Number(value);
return isNaN(parsed) ? value : parsed;
}

export const sanitizeFields =
(handler: NextApiHandler) => async (req: NextApiRequest, res: NextApiResponse) => {
const fields = req.fields || req.body;

const fieldsSanitized = Object.keys(fields).reduce<Record<string, string>>((prev, key) => {
const fieldsSanitized = Object.keys(fields).reduce<Record<string, any>>((prev, key) => {
let value = fields[key];

if (fieldsToSanitize.includes(key)) {
value = removeURLs(value);
}

if (numericFields.includes(key)) {
value = sanitizeNumeric(value);
}

if (typeof value === 'string') {
value = value.trim();
}
Expand Down
13 changes: 9 additions & 4 deletions src/pages/api/devcon-grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ async function handler(req: DevconGrantsNextApiRequest, res: NextApiResponse): P
const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } =
process.env;

if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
res.status(500).json({ status: 'fail', error: 'Server configuration error' });
return resolve();
}

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
const credentials = `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`;
conn.login(SF_PROD_USERNAME, credentials, err => {
if (err) {
console.error(err);
res.status(500).end();
Expand Down Expand Up @@ -103,8 +108,8 @@ async function handler(req: DevconGrantsNextApiRequest, res: NextApiResponse): P
application
);
} catch (err) {
// as this is something internal we don't want to show this error to the user
console.log(err);
console.error('[CRITICAL] Google Sheets sync failed for Lead ID:', ret.id, err);
// Alert monitoring systems but don't fail the user request
}

console.log(`Devcon Grants Lead with ID: ${ret.id} has been created!`);
Expand Down
13 changes: 10 additions & 3 deletions src/pages/api/ecodev-grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void>
const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } =
process.env;

if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
res.status(500).json({ status: 'fail', error: 'Server configuration error' });
return resolve();
}

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});

Expand Down Expand Up @@ -67,9 +71,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void>
RecordTypeId: process.env.SF_RECORD_TYPE_GENERALIST_ECODEV
};

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
const credentials = `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`;
conn.login(SF_PROD_USERNAME, credentials, err => {
if (err) {
return console.error(err);
console.error('Salesforce authentication error:', err);
res.status(500).json({ status: 'fail', error: 'Authentication failed' });
return resolve();
}

let createdLeadID: string;
Expand Down
13 changes: 9 additions & 4 deletions src/pages/api/epf-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void>
return resolve();
}

if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
res.status(500).json({ status: 'fail', error: 'Server configuration error' });
return resolve();
}

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
const credentials = `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`;
conn.login(SF_PROD_USERNAME, credentials, err => {
if (err) {
console.error(err);
res.status(500).end();
console.error('Salesforce authentication error:', err);
res.status(500).json({ status: 'fail', error: 'Authentication failed' });
return resolve();
}

Expand Down
12 changes: 9 additions & 3 deletions src/pages/api/grantee-finance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,20 @@ async function handler(req: GranteeFinanceNextApiRequest, res: NextApiResponse):
const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } =
process.env;

if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
res.status(500).json({ status: 'fail', error: 'Server configuration error' });
return resolve();
}

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
const credentials = `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`;
conn.login(SF_PROD_USERNAME, credentials, err => {
if (err) {
console.error(err);
console.error('Salesforce login failed:', err);
res.status(500).json({ status: 'fail', error: 'Authentication failed' });
return resolve();
}

Expand Down
13 changes: 9 additions & 4 deletions src/pages/api/pse-grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void>
return resolve();
}

if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
res.status(500).json({ status: 'fail', error: 'Server configuration error' });
return resolve();
}

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
const credentials = `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`;
conn.login(SF_PROD_USERNAME, credentials, err => {
if (err) {
console.error(err);
res.status(500).end();
console.error('Salesforce authentication error:', err);
res.status(500).json({ status: 'fail', error: 'Authentication failed' });
return resolve();
}

Expand Down
13 changes: 9 additions & 4 deletions src/pages/api/pse-sponsorships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,20 @@ async function handler(req: PSESponsorshipsNextApiRequest, res: NextApiResponse)
const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } =
process.env;

if (!SF_PROD_USERNAME || !SF_PROD_PASSWORD || !SF_PROD_SECURITY_TOKEN) {
res.status(500).json({ status: 'fail', error: 'Server configuration error' });
return resolve();
}

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
const credentials = `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`;
conn.login(SF_PROD_USERNAME, credentials, err => {
if (err) {
console.error(err);
res.status(500).end();
console.error('Salesforce authentication error:', err);
res.status(500).json({ status: 'fail', error: 'Authentication failed' });
return resolve();
}

Expand Down