the start
This commit is contained in:
1 parent
e41e78f9a3
commit
a79dddff60
22 files changed
+4856
-99
No files matched your search
@@ -0,0 +1,81 @@
|
||||
// Simple encryption utility using Web Crypto API
|
||||
// Note: In production, consider using a more robust encryption library
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export async function encryptData(data: string, key: string): Promise<string> {
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(key),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: iv },
|
||||
derivedKey,
|
||||
encoder.encode(data)
|
||||
);
|
||||
|
||||
// Combine salt, iv, and encrypted data
|
||||
const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
|
||||
combined.set(salt, 0);
|
||||
combined.set(iv, salt.length);
|
||||
combined.set(new Uint8Array(encrypted), salt.length + iv.length);
|
||||
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
}
|
||||
|
||||
export async function decryptData(encryptedData: string, key: string): Promise<string> {
|
||||
const combined = new Uint8Array(atob(encryptedData).split('').map(c => c.charCodeAt(0)));
|
||||
|
||||
const salt = combined.slice(0, 16);
|
||||
const iv = combined.slice(16, 28);
|
||||
const encrypted = combined.slice(28);
|
||||
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(key),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: iv },
|
||||
derivedKey,
|
||||
encrypted
|
||||
);
|
||||
|
||||
return decoder.decode(decrypted);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { doc, getDoc, setDoc, updateDoc } from 'firebase/firestore';
|
||||
import { db } from './firebase';
|
||||
import { encryptData, decryptData } from './crypto';
|
||||
|
||||
interface Envelope {
|
||||
id: string;
|
||||
name: string;
|
||||
allocated: number;
|
||||
spent: number;
|
||||
color: string;
|
||||
accountId: string; // Link envelope to specific account
|
||||
incomeAllocation?: number; // Percentage (0-100) or fixed amount if negative
|
||||
incomeAllocationType?: 'percentage' | 'fixed'; // How to interpret incomeAllocation
|
||||
}
|
||||
|
||||
interface Transaction {
|
||||
id: string;
|
||||
envelopeId?: string;
|
||||
accountId: string; // Link transaction to specific account
|
||||
amount: number;
|
||||
description: string;
|
||||
date: Date;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
interface Account {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'checking' | 'savings' | 'credit_card' | 'mortgage' | 'investment' | 'loan';
|
||||
balance: number;
|
||||
institution?: string;
|
||||
accountNumber?: string;
|
||||
color: string;
|
||||
isActive: boolean;
|
||||
defaultPaycheckAmount?: number;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
accounts: Account[];
|
||||
envelopes: Envelope[];
|
||||
transactions: Transaction[];
|
||||
setupCompleted: boolean;
|
||||
}
|
||||
|
||||
const ENCRYPTION_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY || 'default-key-change-in-production';
|
||||
|
||||
export class DataService {
|
||||
private static async encryptUserData(data: UserData): Promise<string> {
|
||||
return await encryptData(JSON.stringify(data), ENCRYPTION_KEY);
|
||||
}
|
||||
|
||||
private static async decryptUserData(encryptedData: string): Promise<UserData> {
|
||||
const decrypted = await decryptData(encryptedData, ENCRYPTION_KEY);
|
||||
return JSON.parse(decrypted);
|
||||
}
|
||||
|
||||
static async saveUserData(userId: string, data: UserData): Promise<void> {
|
||||
try {
|
||||
const encryptedData = await this.encryptUserData(data);
|
||||
const userDocRef = doc(db, 'users', userId);
|
||||
await setDoc(userDocRef, {
|
||||
data: encryptedData,
|
||||
lastUpdated: new Date(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving user data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async loadUserData(userId: string): Promise<UserData | null> {
|
||||
try {
|
||||
const userDocRef = doc(db, 'users', userId);
|
||||
const userDoc = await getDoc(userDocRef);
|
||||
|
||||
if (userDoc.exists()) {
|
||||
const encryptedData = userDoc.data()?.data;
|
||||
if (encryptedData) {
|
||||
return await this.decryptUserData(encryptedData);
|
||||
}
|
||||
}
|
||||
|
||||
// Return default data if no data exists
|
||||
return {
|
||||
accounts: [
|
||||
{
|
||||
id: 'checking-1',
|
||||
name: 'Main Checking',
|
||||
type: 'checking' as const,
|
||||
balance: 2500.00,
|
||||
institution: 'Bank of America',
|
||||
color: 'bg-blue-500',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'savings-1',
|
||||
name: 'Emergency Savings',
|
||||
type: 'savings' as const,
|
||||
balance: 5000.00,
|
||||
institution: 'Bank of America',
|
||||
color: 'bg-green-500',
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
envelopes: [
|
||||
{ id: '1', name: 'Groceries', allocated: 500, spent: 0, color: 'bg-green-500', accountId: 'checking-1' },
|
||||
{ id: '2', name: 'Transportation', allocated: 300, spent: 0, color: 'bg-blue-500', accountId: 'checking-1' },
|
||||
{ id: '3', name: 'Entertainment', allocated: 200, spent: 0, color: 'bg-purple-500', accountId: 'checking-1' },
|
||||
{ id: '4', name: 'Utilities', allocated: 250, spent: 0, color: 'bg-orange-500', accountId: 'checking-1' },
|
||||
],
|
||||
transactions: [],
|
||||
setupCompleted: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading user data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async updateUserData(userId: string, updates: Partial<UserData>): Promise<void> {
|
||||
try {
|
||||
const currentData = await this.loadUserData(userId);
|
||||
if (currentData) {
|
||||
const updatedData = { ...currentData, ...updates };
|
||||
await this.saveUserData(userId, updatedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth } from 'firebase/auth';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
const app = initializeApp(firebaseConfig);
|
||||
|
||||
// Initialize Firebase services
|
||||
export const auth = getAuth(app);
|
||||
export const db = getFirestore(app);
|
||||
|
||||
export default app;
|
||||
Reference in new issue
Block a user