Skip to content
Merged
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
17 changes: 16 additions & 1 deletion public/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,14 @@ header {
font-weight: 500;
}

.transaction-item .notes {
font-size: 0.75rem;
color: var(--text);
opacity: 0.8;
margin-top: 0.25rem;
line-height: 1.3;
}

.transaction-item .metadata {
display: flex;
align-items: center;
Expand Down Expand Up @@ -834,7 +842,8 @@ footer {
}

#transactionForm input,
#transactionForm select {
#transactionForm select,
#transactionForm textarea {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
Expand All @@ -844,6 +853,12 @@ footer {
width: 100%;
}

#transactionForm textarea {
resize: vertical;
min-height: 60px;
font-family: inherit;
}

#transactionForm button {
background: var(--primary);
color: white;
Expand Down
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ <h3>Transactions</h3>
<input type="number" id="amount" placeholder="0.00" required step="0.01">
</div>
<input type="text" id="description" placeholder="Description" required>
<textarea id="notes" placeholder="Notes (optional)" rows="3"></textarea>
<input type="date" id="transactionDate" required>
<button type="submit">Add</button>
</form>
Expand Down
36 changes: 26 additions & 10 deletions public/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ function debugLog(...args) {
}
}

// HTML escaping function to prevent XSS attacks
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

// Helper function to join paths with base path
function joinPath(path) {
const basePath = window.appConfig?.basePath || '';
Expand Down Expand Up @@ -358,9 +369,10 @@ async function loadTransactions() {
<div class="transaction-item ${isRecurring ? 'recurring-instance' : ''}" data-id="${transaction.id}" data-type="${transaction.type}">
<div class="transaction-content">
<div class="details">
<div class="description">${transaction.description}</div>
<div class="description">${escapeHtml(transaction.description)}</div>
${transaction.notes ? `<div class="notes">${escapeHtml(transaction.notes)}</div>` : ''}
<div class="metadata">
${transaction.category ? `<span class="category">${transaction.category}</span>` : ''}
${transaction.category ? `<span class="category">${escapeHtml(transaction.category)}</span>` : ''}
<span class="date">${formattedDate}</span>
${isRecurring ? `<span class="recurring-info">(Recurring)</span>` : ''}
</div>
Expand Down Expand Up @@ -466,6 +478,7 @@ function editTransaction(id, transaction, isRecurringInstance) {
document.getElementById('amount').value = transaction.amount;
document.getElementById('description').value = transaction.description;
document.getElementById('transactionDate').value = transaction.date;
document.getElementById('notes').value = transaction.notes || '';

// Update the currentTransactionType to match the transaction being edited
currentTransactionType = transaction.type;
Expand Down Expand Up @@ -733,7 +746,8 @@ function initModalHandling() {
description: document.getElementById('description').value,
category: currentTransactionType === 'expense' ? document.getElementById('category').value : null,
date: document.getElementById('transactionDate').value,
recurring: buildRecurringPattern()
recurring: buildRecurringPattern(),
notes: document.getElementById('notes').value
};

try {
Expand Down Expand Up @@ -1047,24 +1061,26 @@ async function initMainPage() {
const tableData = transactions.map(t => [
t.date,
t.description,
t.notes || '-',
t.category || '-',
formatCurrency(t.type === 'expense' ? -t.amount : t.amount),
t.type
]);

doc.autoTable({
startY: 85,
head: [['Date', 'Description', 'Category', 'Amount', 'Type']],
head: [['Date', 'Description', 'Notes', 'Category', 'Amount', 'Type']],
body: tableData,
theme: 'grid',
headStyles: { fillColor: [66, 66, 66] },
styles: { fontSize: 10 },
styles: { fontSize: 9 },
columnStyles: {
0: { cellWidth: 30 }, // Date
1: { cellWidth: 60 }, // Description
2: { cellWidth: 30 }, // Category
3: { cellWidth: 30 }, // Amount
4: { cellWidth: 20 } // Type
0: { cellWidth: 25 }, // Date
1: { cellWidth: 40 }, // Description
2: { cellWidth: 35 }, // Notes
3: { cellWidth: 25 }, // Category
4: { cellWidth: 25 }, // Amount
5: { cellWidth: 15 } // Type
}
});

Expand Down
73 changes: 50 additions & 23 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ function debugLog(...args) {
}
}

// Input sanitization function to prevent XSS attacks
function sanitizeInput(input) {
if (typeof input !== 'string') return input;
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}

// Add logging to BASE_PATH extraction
const BASE_PATH = (() => {
if (!process.env.BASE_URL) {
Expand Down Expand Up @@ -362,7 +374,7 @@ async function getTransactionsInRange(startDate, endDate) {
// API Routes - all under BASE_PATH
app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
try {
const { type, amount, description, category, date, recurring } = req.body;
const { type, amount, description, category, date, recurring, notes } = req.body;

// Basic validation
if (!type || !amount || !description || !date) {
Expand Down Expand Up @@ -446,8 +458,9 @@ app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
const newTransaction = {
id: crypto.randomUUID(),
amount: parseFloat(amount),
description,
date: adjustedDate
description: sanitizeInput(description),
date: adjustedDate,
notes: sanitizeInput(notes || '')
};

// Add recurring information if present
Expand All @@ -459,7 +472,7 @@ app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
}

if (type === 'expense') {
newTransaction.category = category;
newTransaction.category = sanitizeInput(category);
transactions[key].expenses.push(newTransaction);
} else {
transactions[key].income.push(newTransaction);
Expand Down Expand Up @@ -764,9 +777,15 @@ app.get(BASE_PATH + '/api/export/:year/:month', authMiddleware, async (req, res)
].sort((a, b) => new Date(b.date) - new Date(a.date));

// Convert to CSV
const csvRows = ['Date,Type,Category,Description,Amount'];
const csvRows = ['Date,Type,Category,Description,Notes,Amount'];
allTransactions.forEach(t => {
csvRows.push(`${t.date},${t.type},${t.category || ''},${t.description},${t.amount}`);
// Escape notes and description to handle commas and quotes
const escapedDescription = (t.description || '').replace(/"/g, '""');
const escapedNotes = (t.notes || '').replace(/"/g, '""');
const formattedDescription = escapedDescription.includes(',') ? `"${escapedDescription}"` : escapedDescription;
const formattedNotes = escapedNotes.includes(',') ? `"${escapedNotes}"` : escapedNotes;

csvRows.push(`${t.date},${t.type},${t.category || ''},${formattedDescription},${formattedNotes},${t.amount}`);
});

res.setHeader('Content-Type', 'text/csv');
Expand All @@ -788,15 +807,17 @@ app.get(BASE_PATH + '/api/export/range', authMiddleware, async (req, res) => {
const transactions = await getTransactionsInRange(start, end);

// Convert to CSV with specified format
const csvRows = ['Category,Date,Description,Value'];
const csvRows = ['Category,Date,Description,Notes,Value'];
transactions.forEach(t => {
const category = t.type === 'income' ? 'Income' : t.category;
const value = t.type === 'income' ? t.amount : -t.amount;
// Escape description to handle commas and quotes
const escapedDescription = t.description.replace(/"/g, '""');
// Escape description and notes to handle commas and quotes
const escapedDescription = (t.description || '').replace(/"/g, '""');
const escapedNotes = (t.notes || '').replace(/"/g, '""');
const formattedDescription = escapedDescription.includes(',') ? `"${escapedDescription}"` : escapedDescription;
const formattedNotes = escapedNotes.includes(',') ? `"${escapedNotes}"` : escapedNotes;

csvRows.push(`${category},${t.date},${formattedDescription},${value}`);
csvRows.push(`${category},${t.date},${formattedDescription},${formattedNotes},${value}`);
});

res.setHeader('Content-Type', 'text/csv');
Expand All @@ -811,7 +832,7 @@ app.get(BASE_PATH + '/api/export/range', authMiddleware, async (req, res) => {
app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) => {
try {
const { id } = req.params;
const { type, amount, description, category, date, recurring } = req.body;
const { type, amount, description, category, date, recurring, notes } = req.body;

// Basic validation
if (!type || !amount || !description || !date) {
Expand All @@ -837,21 +858,23 @@ app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) =>
// If type changed, move to expenses
if (type === 'expense') {
const transaction = monthData.income.splice(incomeIndex, 1)[0];
transaction.category = category;
transaction.category = sanitizeInput(category);
monthData.expenses.push({
...transaction,
amount: parseFloat(amount),
description,
description: sanitizeInput(description),
date,
recurring: recurring || null
recurring: recurring || null,
notes: sanitizeInput(notes || '')
});
} else {
monthData.income[incomeIndex] = {
...monthData.income[incomeIndex],
amount: parseFloat(amount),
description,
description: sanitizeInput(description),
date,
recurring: recurring || null
recurring: recurring || null,
notes: sanitizeInput(notes || '')
};
}
found = true;
Expand All @@ -868,18 +891,20 @@ app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) =>
monthData.income.push({
...transaction,
amount: parseFloat(amount),
description,
description: sanitizeInput(description),
date,
recurring: recurring || null
recurring: recurring || null,
notes: sanitizeInput(notes || '')
});
} else {
monthData.expenses[expenseIndex] = {
...monthData.expenses[expenseIndex],
amount: parseFloat(amount),
description,
category,
description: sanitizeInput(description),
category: sanitizeInput(category),
date,
recurring: recurring || null
recurring: recurring || null,
notes: sanitizeInput(notes || '')
};
}
found = true;
Expand Down Expand Up @@ -1029,7 +1054,8 @@ app.get(BASE_PATH + '/api/calendar/transactions', apiAuthMiddleware, async (req,
filteredTransactions.push({
type: 'income',
...transaction,
amount: parseFloat(transaction.amount)
amount: parseFloat(transaction.amount),
notes: transaction.notes || ''
});
});

Expand All @@ -1038,7 +1064,8 @@ app.get(BASE_PATH + '/api/calendar/transactions', apiAuthMiddleware, async (req,
filteredTransactions.push({
type: 'expense',
...transaction,
amount: parseFloat(transaction.amount)
amount: parseFloat(transaction.amount),
notes: transaction.notes || ''
});
});
}
Expand Down