@@ -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