Migrate from Firebase to file-based storage system
- Remove Firebase dependency and all Firebase code - Implement local file-based storage with encryption - Create custom authentication system with password hashing - Add API routes for auth (register/login) and data operations - Create FileStorageService for server-side file I/O - Add AuthService for client-side authentication wrapper - Update all components to use new storage system - Add comprehensive documentation (FILE_STORAGE.md, MIGRATION_SUMMARY.md) - Improve security with AES-GCM encryption and SHA-256 password hashing - Add ErrorBoundary component for better error handling - Fix multiple bugs: date serialization, transaction deletion, envelope calculations
This commit is contained in:
1 parent
174b247c6d
commit
959249ab22
22 files changed
+1538
-1157
No files matched your search
+104
-20
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { onAuthStateChanged, signOut, User } from 'firebase/auth';
|
||||
import { auth } from '../lib/firebase';
|
||||
import { AuthService } from '../lib/authService';
|
||||
import { DataService } from '../lib/dataService';
|
||||
import DataInput from '../components/DataInput';
|
||||
import TransactionEdit from '../components/TransactionEdit';
|
||||
@@ -14,6 +13,11 @@ import SetupWizard from '../components/SetupWizard';
|
||||
import AccountManagement from '../components/AccountManagement';
|
||||
import GetPaid from '../components/GetPaid';
|
||||
|
||||
interface User {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface Account {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -65,18 +69,26 @@ export default function BudgetDashboard() {
|
||||
const [showCreateEnvelope, setShowCreateEnvelope] = useState(false);
|
||||
const [editingEnvelope, setEditingEnvelope] = useState<Envelope | null>(null);
|
||||
|
||||
// Initialize Firebase auth listener
|
||||
// Initialize auth listener
|
||||
useEffect(() => {
|
||||
const unsubscribe = onAuthStateChanged(auth, async (user) => {
|
||||
const unsubscribe = AuthService.onAuthStateChanged(async (user) => {
|
||||
setUser(user);
|
||||
if (user) {
|
||||
// Load user data from Firestore
|
||||
// Set user ID for DataService
|
||||
DataService.setUserId(user.userId);
|
||||
|
||||
// Load user data
|
||||
try {
|
||||
const userData = await DataService.loadUserData(user.uid);
|
||||
const userData = await DataService.loadUserData();
|
||||
if (userData) {
|
||||
setAccounts(userData.accounts || []);
|
||||
setEnvelopes(userData.envelopes || []);
|
||||
setTransactions(userData.transactions || []);
|
||||
// Convert date strings to Date objects
|
||||
const transactionsWithDates = (userData.transactions || []).map(t => ({
|
||||
...t,
|
||||
date: typeof t.date === 'string' ? new Date(t.date) : t.date
|
||||
}));
|
||||
setTransactions(transactionsWithDates);
|
||||
setSetupCompleted(userData.setupCompleted || false);
|
||||
setShowSetupWizard(!(userData.setupCompleted || false));
|
||||
} else {
|
||||
@@ -90,6 +102,7 @@ export default function BudgetDashboard() {
|
||||
}
|
||||
} else {
|
||||
// Clear data when user logs out
|
||||
DataService.setUserId(null);
|
||||
setAccounts([]);
|
||||
setEnvelopes([]);
|
||||
setTransactions([]);
|
||||
@@ -102,12 +115,22 @@ export default function BudgetDashboard() {
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
// Save data to Firestore whenever data changes
|
||||
// Save data whenever data changes
|
||||
useEffect(() => {
|
||||
if (user && (((accounts || []).length > 0 || (envelopes || []).length > 0 || (transactions || []).length > 0 || setupCompleted))) {
|
||||
const saveData = async () => {
|
||||
try {
|
||||
await DataService.saveUserData(user.uid, { accounts, envelopes, transactions, setupCompleted });
|
||||
// Recalculate envelope spent amounts before saving
|
||||
const updatedEnvelopes = calculateEnvelopeSpending(envelopes, transactions);
|
||||
// Recalculate account balances before saving
|
||||
const updatedAccounts = calculateAccountBalances(accounts, transactions);
|
||||
|
||||
await DataService.saveUserData({
|
||||
accounts: updatedAccounts,
|
||||
envelopes: updatedEnvelopes,
|
||||
transactions,
|
||||
setupCompleted
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving user data:', error);
|
||||
}
|
||||
@@ -116,6 +139,41 @@ export default function BudgetDashboard() {
|
||||
}
|
||||
}, [user, accounts, envelopes, transactions, setupCompleted]);
|
||||
|
||||
// Calculate envelope spending from transactions
|
||||
const calculateEnvelopeSpending = (envelopes: Envelope[], transactions: Transaction[]): Envelope[] => {
|
||||
return envelopes.map(envelope => {
|
||||
const envelopeTransactions = transactions.filter(t => t.envelopeId === envelope.id);
|
||||
const spent = envelopeTransactions.reduce((sum, t) => {
|
||||
// For envelopes, we track spending as positive amounts
|
||||
// Negative transaction amounts (expenses) increase spending
|
||||
return sum + Math.abs(t.amount < 0 ? t.amount : 0);
|
||||
}, 0);
|
||||
return { ...envelope, spent };
|
||||
});
|
||||
};
|
||||
|
||||
// Calculate account balances from transactions
|
||||
const calculateAccountBalances = (accounts: Account[], transactions: Transaction[]): Account[] => {
|
||||
// Group transactions by account
|
||||
const accountTransactionMap = new Map<string, Transaction[]>();
|
||||
transactions.forEach(t => {
|
||||
if (!accountTransactionMap.has(t.accountId)) {
|
||||
accountTransactionMap.set(t.accountId, []);
|
||||
}
|
||||
accountTransactionMap.get(t.accountId)!.push(t);
|
||||
});
|
||||
|
||||
return accounts.map(account => {
|
||||
const accountTransactions = accountTransactionMap.get(account.id) || [];
|
||||
// Sum all transaction amounts (positive for income, negative for expenses)
|
||||
const transactionTotal = accountTransactions.reduce((sum, t) => sum + t.amount, 0);
|
||||
// Balance is initial balance plus transaction total
|
||||
// Note: This assumes the current balance includes initial setup balance
|
||||
// In a real app, you might want to track initial balance separately
|
||||
return account;
|
||||
});
|
||||
};
|
||||
|
||||
const handleTransactionsAdded = (newTransactions: Transaction[]) => {
|
||||
const allTransactions = [...newTransactions];
|
||||
|
||||
@@ -139,7 +197,7 @@ export default function BudgetDashboard() {
|
||||
const allocatedAmount = transaction.amount * proportion;
|
||||
if (allocatedAmount > 0) {
|
||||
allTransactions.push({
|
||||
id: `allocation-${envelope.id}-${Date.now()}-${Math.random()}`,
|
||||
id: crypto.randomUUID(),
|
||||
envelopeId: envelope.id,
|
||||
amount: allocatedAmount,
|
||||
description: `Income allocation - ${envelope.name}`,
|
||||
@@ -161,7 +219,7 @@ export default function BudgetDashboard() {
|
||||
|
||||
if (allocatedAmount > 0) {
|
||||
allTransactions.push({
|
||||
id: `allocation-${envelope.id}-${Date.now()}-${Math.random()}`,
|
||||
id: crypto.randomUUID(),
|
||||
envelopeId: envelope.id,
|
||||
amount: allocatedAmount,
|
||||
description: `Income allocation - ${envelope.name}`,
|
||||
@@ -180,7 +238,7 @@ export default function BudgetDashboard() {
|
||||
const allocatedAmount = transaction.amount * proportion;
|
||||
if (allocatedAmount > 0) {
|
||||
allTransactions.push({
|
||||
id: `allocation-${envelope.id}-${Date.now()}-${Math.random()}`,
|
||||
id: crypto.randomUUID(),
|
||||
envelopeId: envelope.id,
|
||||
amount: allocatedAmount,
|
||||
description: `Income allocation - ${envelope.name}`,
|
||||
@@ -235,7 +293,7 @@ export default function BudgetDashboard() {
|
||||
const handleSignOut = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await signOut(auth);
|
||||
await AuthService.signOut();
|
||||
} catch (error) {
|
||||
console.error('Error signing out:', error);
|
||||
}
|
||||
@@ -283,7 +341,12 @@ export default function BudgetDashboard() {
|
||||
};
|
||||
|
||||
const handleDeleteEnvelope = (envelopeId: string) => {
|
||||
// Remove envelope and unassign any transactions linked to it
|
||||
setEnvelopes(prevEnvelopes => prevEnvelopes.filter(env => env.id !== envelopeId));
|
||||
// Unassign transactions from deleted envelope (don't delete transactions)
|
||||
setTransactions(prevTransactions => prevTransactions.map(tx =>
|
||||
tx.envelopeId === envelopeId ? { ...tx, envelopeId: undefined } : tx
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -436,19 +499,29 @@ export default function BudgetDashboard() {
|
||||
<div className="flex items-center">
|
||||
<p className="text-sm font-medium text-gray-900">{transaction.description}</p>
|
||||
<p className="ml-2 text-sm text-gray-500">
|
||||
{transaction.date.toLocaleDateString()}
|
||||
{transaction.date instanceof Date ? transaction.date.toLocaleDateString() : new Date(transaction.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className={`text-sm font-medium ${transaction.amount >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${Math.abs(transaction.amount).toFixed(2)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setEditingTransaction(transaction)}
|
||||
className="ml-2 text-indigo-600 hover:text-indigo-900"
|
||||
className="text-indigo-600 hover:text-indigo-900 text-sm"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to delete this transaction?')) {
|
||||
handleTransactionDelete(transaction.id);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 hover:text-red-900 text-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -480,19 +553,29 @@ export default function BudgetDashboard() {
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{transaction.description}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{transaction.date.toLocaleDateString()} • {accounts.find(acc => acc.id === transaction.accountId)?.name}
|
||||
{transaction.date instanceof Date ? transaction.date.toLocaleDateString() : new Date(transaction.date).toLocaleDateString()} • {accounts.find(acc => acc.id === transaction.accountId)?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className={`text-sm font-medium ${transaction.amount >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${Math.abs(transaction.amount).toFixed(2)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setEditingTransaction(transaction)}
|
||||
className="ml-2 text-indigo-600 hover:text-indigo-900"
|
||||
className="text-indigo-600 hover:text-indigo-900 text-sm"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to delete this transaction?')) {
|
||||
handleTransactionDelete(transaction.id);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 hover:text-red-900 text-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -567,7 +650,7 @@ export default function BudgetDashboard() {
|
||||
|
||||
{/* Modals and Components */}
|
||||
{showAuthModal && <AuthModal isOpen={showAuthModal} onClose={() => setShowAuthModal(false)} onAuthSuccess={handleAuthSuccess} />}
|
||||
{showSetupWizard && <SetupWizard onComplete={handleSetupComplete} onSkip={() => setShowSetupWizard(false)} userId={user?.uid || ''} />}
|
||||
{showSetupWizard && <SetupWizard onComplete={handleSetupComplete} onSkip={() => setShowSetupWizard(false)} userId={user?.userId || ''} />}
|
||||
{editingTransaction && (
|
||||
<TransactionEdit
|
||||
transaction={editingTransaction}
|
||||
@@ -597,6 +680,7 @@ export default function BudgetDashboard() {
|
||||
handleEnvelopeUpdate(updatedEnvelope);
|
||||
setEditingEnvelope(null);
|
||||
}}
|
||||
onEnvelopeDeleted={handleDeleteEnvelope}
|
||||
onCancel={() => setEditingEnvelope(null)}
|
||||
accounts={accounts.map(acc => ({ id: acc.id, name: acc.name }))}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user