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
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut } from 'firebase/auth';
|
||||
import { auth } from '../lib/firebase';
|
||||
import { AuthService } from '../lib/authService';
|
||||
import { DataService } from '../lib/dataService';
|
||||
|
||||
interface AuthModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -25,12 +25,14 @@ export default function AuthModal({ isOpen, onClose, onAuthSuccess }: AuthModalP
|
||||
|
||||
try {
|
||||
if (isLogin) {
|
||||
await signInWithEmailAndPassword(auth, email, password);
|
||||
const user = await AuthService.signIn(email, password);
|
||||
DataService.setUserId(user.userId);
|
||||
} else {
|
||||
if (password !== confirmPassword) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
await createUserWithEmailAndPassword(auth, email, password);
|
||||
const user = await AuthService.signUp(email, password);
|
||||
DataService.setUserId(user.userId);
|
||||
}
|
||||
onAuthSuccess();
|
||||
onClose();
|
||||
|
||||
@@ -23,8 +23,8 @@ export default function DataInput({ onTransactionsAdded, envelopes, accounts }:
|
||||
const [inputMode, setInputMode] = useState<InputMode>('manual');
|
||||
|
||||
const handleFileTransactionsParsed = (parsedTransactions: ParsedTransaction[]) => {
|
||||
const transactions = parsedTransactions.map((t, index) => ({
|
||||
id: `imported-${Date.now()}-${index}`,
|
||||
const transactions = parsedTransactions.map((t) => ({
|
||||
id: crypto.randomUUID(),
|
||||
amount: t.amount,
|
||||
description: t.description,
|
||||
date: t.date,
|
||||
@@ -43,7 +43,7 @@ export default function DataInput({ onTransactionsAdded, envelopes, accounts }:
|
||||
accountId: string;
|
||||
}) => {
|
||||
const newTransaction = {
|
||||
id: `manual-${Date.now()}`,
|
||||
id: crypto.randomUUID(),
|
||||
...transaction,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ interface EnvelopeEditProps {
|
||||
incomeAllocation?: number;
|
||||
incomeAllocationType?: 'percentage' | 'fixed';
|
||||
}) => void;
|
||||
onEnvelopeDeleted?: (envelopeId: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ const colorOptions = [
|
||||
{ value: 'bg-indigo-500', label: 'Indigo', color: '#6366F1' },
|
||||
];
|
||||
|
||||
export default function EnvelopeEdit({ envelope, onEnvelopeUpdated, onCancel, accounts }: EnvelopeEditProps) {
|
||||
export default function EnvelopeEdit({ envelope, onEnvelopeUpdated, onEnvelopeDeleted, onCancel, accounts }: EnvelopeEditProps) {
|
||||
const [name, setName] = useState(envelope.name);
|
||||
const [allocated, setAllocated] = useState(envelope.allocated.toString());
|
||||
const [selectedColor, setSelectedColor] = useState(envelope.color);
|
||||
@@ -46,6 +47,15 @@ export default function EnvelopeEdit({ envelope, onEnvelopeUpdated, onCancel, ac
|
||||
const [incomeAllocationType, setIncomeAllocationType] = useState<'percentage' | 'fixed'>(envelope.incomeAllocationType || 'percentage');
|
||||
const [incomeAllocation, setIncomeAllocation] = useState(envelope.incomeAllocation?.toString() || '');
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm(`Are you sure you want to delete the envelope "${envelope.name}"? Transactions linked to this envelope will be unassigned.`)) {
|
||||
if (onEnvelopeDeleted) {
|
||||
onEnvelopeDeleted(envelope.id);
|
||||
}
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -215,6 +225,13 @@ export default function EnvelopeEdit({ envelope, onEnvelopeUpdated, onCancel, ac
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
Delete Envelope
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { Component, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Error boundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl p-8 max-w-lg w-full">
|
||||
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full mb-4">
|
||||
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 text-center mb-4">
|
||||
Oops! Something went wrong
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center mb-6">
|
||||
We encountered an unexpected error. Don't worry, your data is safe.
|
||||
</p>
|
||||
{this.state.error && (
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<p className="text-sm font-medium text-gray-900 mb-2">Error details:</p>
|
||||
<p className="text-sm text-gray-600 font-mono break-all">
|
||||
{this.state.error.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="flex-1 bg-gray-300 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export default function GetPaid({ accounts, envelopes, onIncomeAdded, onAccountU
|
||||
|
||||
// Add the main paycheck transaction to the account
|
||||
transactions.push({
|
||||
id: `paycheck-${Date.now()}`,
|
||||
id: crypto.randomUUID(),
|
||||
amount: paycheckAmount,
|
||||
description: description || 'Paycheck',
|
||||
date: new Date(),
|
||||
@@ -118,7 +118,7 @@ export default function GetPaid({ accounts, envelopes, onIncomeAdded, onAccountU
|
||||
Object.entries(distribution).forEach(([envelopeId, allocatedAmount]) => {
|
||||
if (allocatedAmount > 0) {
|
||||
transactions.push({
|
||||
id: `allocation-${envelopeId}-${Date.now()}`,
|
||||
id: crypto.randomUUID(),
|
||||
envelopeId,
|
||||
amount: allocatedAmount,
|
||||
description: `Paycheck allocation - ${envelopes.find(e => e.id === envelopeId)?.name}`,
|
||||
@@ -374,52 +374,6 @@ export default function GetPaid({ accounts, envelopes, onIncomeAdded, onAccountU
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Default Amount */}
|
||||
{selectedAccountId && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDefaultAmountField(!showDefaultAmountField)}
|
||||
className="text-sm text-blue-600 hover:underline mb-2"
|
||||
>
|
||||
{showDefaultAmountField ? 'Hide' : 'Set'} Default Paycheck Amount
|
||||
</button>
|
||||
|
||||
{showDefaultAmountField && (
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">Default Paycheck Amount</h4>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={defaultAmountInput}
|
||||
onChange={(e) => setDefaultAmountInput(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSetDefaultAmount}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Set Default
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUseDefaultAmount}
|
||||
className="w-full px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
|
||||
>
|
||||
Use Default Amount
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||
<button
|
||||
|
||||
Reference in new issue
Block a user