@@ -36,6 +36,18 @@ function debugLog(...args) {
3636 }
3737}
3838
39+ // Input sanitization function to prevent XSS attacks
40+ function sanitizeInput(input) {
41+ if (typeof input !== 'string') return input;
42+ return input
43+ .replace(/&/g, '&')
44+ .replace(/</g, '<')
45+ .replace(/>/g, '>')
46+ .replace(/"/g, '"')
47+ .replace(/'/g, ''')
48+ .replace(/\//g, '/');
49+ }
50+
3951// Add logging to BASE_PATH extraction
4052const BASE_PATH = (() => {
4153 if (!process.env.BASE_URL) {
@@ -362,7 +374,7 @@ async function getTransactionsInRange(startDate, endDate) {
362374// API Routes - all under BASE_PATH
363375app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
364376 try {
365- const { type, amount, description, category, date, recurring } = req.body;
377+ const { type, amount, description, category, date, recurring, notes } = req.body;
366378
367379 // Basic validation
368380 if (!type || !amount || !description || !date) {
@@ -446,8 +458,9 @@ app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
446458 const newTransaction = {
447459 id: crypto.randomUUID(),
448460 amount: parseFloat(amount),
449- description,
450- date: adjustedDate
461+ description: sanitizeInput(description),
462+ date: adjustedDate,
463+ notes: sanitizeInput(notes || '')
451464 };
452465
453466 // Add recurring information if present
@@ -459,7 +472,7 @@ app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
459472 }
460473
461474 if (type === 'expense') {
462- newTransaction.category = category;
475+ newTransaction.category = sanitizeInput( category) ;
463476 transactions[key].expenses.push(newTransaction);
464477 } else {
465478 transactions[key].income.push(newTransaction);
@@ -764,9 +777,15 @@ app.get(BASE_PATH + '/api/export/:year/:month', authMiddleware, async (req, res)
764777 ].sort((a, b) => new Date(b.date) - new Date(a.date));
765778
766779 // Convert to CSV
767- const csvRows = ['Date,Type,Category,Description,Amount'];
780+ const csvRows = ['Date,Type,Category,Description,Notes, Amount'];
768781 allTransactions.forEach(t => {
769- csvRows.push(`${t.date},${t.type},${t.category || ''},${t.description},${t.amount}`);
782+ // Escape notes and description to handle commas and quotes
783+ const escapedDescription = (t.description || '').replace(/"/g, '""');
784+ const escapedNotes = (t.notes || '').replace(/"/g, '""');
785+ const formattedDescription = escapedDescription.includes(',') ? `"${escapedDescription}"` : escapedDescription;
786+ const formattedNotes = escapedNotes.includes(',') ? `"${escapedNotes}"` : escapedNotes;
787+
788+ csvRows.push(`${t.date},${t.type},${t.category || ''},${formattedDescription},${formattedNotes},${t.amount}`);
770789 });
771790
772791 res.setHeader('Content-Type', 'text/csv');
@@ -788,15 +807,17 @@ app.get(BASE_PATH + '/api/export/range', authMiddleware, async (req, res) => {
788807 const transactions = await getTransactionsInRange(start, end);
789808
790809 // Convert to CSV with specified format
791- const csvRows = ['Category,Date,Description,Value'];
810+ const csvRows = ['Category,Date,Description,Notes, Value'];
792811 transactions.forEach(t => {
793812 const category = t.type === 'income' ? 'Income' : t.category;
794813 const value = t.type === 'income' ? t.amount : -t.amount;
795- // Escape description to handle commas and quotes
796- const escapedDescription = t.description.replace(/"/g, '""');
814+ // Escape description and notes to handle commas and quotes
815+ const escapedDescription = (t.description || '').replace(/"/g, '""');
816+ const escapedNotes = (t.notes || '').replace(/"/g, '""');
797817 const formattedDescription = escapedDescription.includes(',') ? `"${escapedDescription}"` : escapedDescription;
818+ const formattedNotes = escapedNotes.includes(',') ? `"${escapedNotes}"` : escapedNotes;
798819
799- csvRows.push(`${category},${t.date},${formattedDescription},${value}`);
820+ csvRows.push(`${category},${t.date},${formattedDescription},${formattedNotes},${ value}`);
800821 });
801822
802823 res.setHeader('Content-Type', 'text/csv');
@@ -811,7 +832,7 @@ app.get(BASE_PATH + '/api/export/range', authMiddleware, async (req, res) => {
811832app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) => {
812833 try {
813834 const { id } = req.params;
814- const { type, amount, description, category, date, recurring } = req.body;
835+ const { type, amount, description, category, date, recurring, notes } = req.body;
815836
816837 // Basic validation
817838 if (!type || !amount || !description || !date) {
@@ -837,21 +858,23 @@ app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) =>
837858 // If type changed, move to expenses
838859 if (type === 'expense') {
839860 const transaction = monthData.income.splice(incomeIndex, 1)[0];
840- transaction.category = category;
861+ transaction.category = sanitizeInput( category) ;
841862 monthData.expenses.push({
842863 ...transaction,
843864 amount: parseFloat(amount),
844- description,
865+ description: sanitizeInput(description) ,
845866 date,
846- recurring: recurring || null
867+ recurring: recurring || null,
868+ notes: sanitizeInput(notes || '')
847869 });
848870 } else {
849871 monthData.income[incomeIndex] = {
850872 ...monthData.income[incomeIndex],
851873 amount: parseFloat(amount),
852- description,
874+ description: sanitizeInput(description) ,
853875 date,
854- recurring: recurring || null
876+ recurring: recurring || null,
877+ notes: sanitizeInput(notes || '')
855878 };
856879 }
857880 found = true;
@@ -868,18 +891,20 @@ app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) =>
868891 monthData.income.push({
869892 ...transaction,
870893 amount: parseFloat(amount),
871- description,
894+ description: sanitizeInput(description) ,
872895 date,
873- recurring: recurring || null
896+ recurring: recurring || null,
897+ notes: sanitizeInput(notes || '')
874898 });
875899 } else {
876900 monthData.expenses[expenseIndex] = {
877901 ...monthData.expenses[expenseIndex],
878902 amount: parseFloat(amount),
879- description,
880- category,
903+ description: sanitizeInput(description) ,
904+ category: sanitizeInput(category) ,
881905 date,
882- recurring: recurring || null
906+ recurring: recurring || null,
907+ notes: sanitizeInput(notes || '')
883908 };
884909 }
885910 found = true;
@@ -1029,7 +1054,8 @@ app.get(BASE_PATH + '/api/calendar/transactions', apiAuthMiddleware, async (req,
10291054 filteredTransactions.push({
10301055 type: 'income',
10311056 ...transaction,
1032- amount: parseFloat(transaction.amount)
1057+ amount: parseFloat(transaction.amount),
1058+ notes: transaction.notes || ''
10331059 });
10341060 });
10351061
@@ -1038,7 +1064,8 @@ app.get(BASE_PATH + '/api/calendar/transactions', apiAuthMiddleware, async (req,
10381064 filteredTransactions.push({
10391065 type: 'expense',
10401066 ...transaction,
1041- amount: parseFloat(transaction.amount)
1067+ amount: parseFloat(transaction.amount),
1068+ notes: transaction.notes || ''
10421069 });
10431070 });
10441071 }
0 commit comments