Security: - Replace SHA-256 password hashing with bcrypt (cost 12) + timing-safe compare - Introduce server-side session tokens (x-session-token header); drop x-user-id - Add /api/auth/logout endpoint to invalidate sessions - Enforce 8-char minimum password server-side - Fix password reset: now uses real signed tokens with 1-hour TTL Features: - Goals: savings goals with progress bars, target dates, add/withdraw funds - Auto-categorization Rules: keyword → envelope mapping applied on transaction add - Transaction search/filter: description, account, envelope, date range - Transaction table: shows envelope color dot, running balance column - Monthly Spending History chart (last 6 months income vs expenses bar chart) - Spending Insights: month-over-month comparison + overspent envelope callouts - Budget progress expanded: per-envelope bars with rollover indicator - Envelope rollover toggle: unspent funds can carry forward - Recurring transactions auto-applied on login - Save indicator in nav (Saving… / Saved ✓ / Save failed) - Nav overflow fixed: all tabs scroll horizontally instead of clipping - PWA manifest added for home-screen install support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
2.3 KiB
TypeScript
90 lines
2.3 KiB
TypeScript
import { AuthService } from './authService';
|
|
|
|
interface Envelope {
|
|
id: string;
|
|
name: string;
|
|
allocated: number;
|
|
spent: number;
|
|
color: string;
|
|
accountId: string;
|
|
incomeAllocation?: number;
|
|
incomeAllocationType?: 'percentage' | 'fixed';
|
|
}
|
|
|
|
interface Transaction {
|
|
id: string;
|
|
envelopeId?: string;
|
|
accountId: string;
|
|
amount: number;
|
|
description: string;
|
|
date: Date | string;
|
|
category?: string;
|
|
}
|
|
|
|
interface Account {
|
|
id: string;
|
|
name: string;
|
|
type: 'checking' | 'savings' | 'credit_card' | 'mortgage' | 'investment' | 'loan';
|
|
balance: number;
|
|
startingBalance?: number;
|
|
institution?: string;
|
|
accountNumber?: string;
|
|
color: string;
|
|
isActive: boolean;
|
|
defaultPaycheckAmount?: number;
|
|
}
|
|
|
|
interface UserData {
|
|
accounts: Account[];
|
|
envelopes: Envelope[];
|
|
transactions: Transaction[];
|
|
setupCompleted: boolean;
|
|
goals?: any[];
|
|
transactionRules?: any[];
|
|
}
|
|
|
|
function getAuthHeaders(): HeadersInit {
|
|
const token = AuthService.getSessionToken();
|
|
if (!token) throw new Error('Not authenticated');
|
|
return { 'Content-Type': 'application/json', 'x-session-token': token };
|
|
}
|
|
|
|
export class DataService {
|
|
// kept for legacy callers in page.tsx — no-op now that session handles identity
|
|
static setUserId(_userId: string | null) {}
|
|
static getUserId(): string | null { return AuthService.getCurrentUser()?.userId ?? null; }
|
|
static clearUserId() {}
|
|
|
|
static async saveUserData(data: UserData): Promise<void> {
|
|
const response = await fetch('/api/data', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to save data');
|
|
}
|
|
}
|
|
|
|
static async loadUserData(): Promise<UserData | null> {
|
|
const response = await fetch('/api/data', {
|
|
method: 'GET',
|
|
headers: getAuthHeaders(),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to load data');
|
|
}
|
|
const result = await response.json();
|
|
return result.data;
|
|
}
|
|
|
|
static async updateUserData(updates: Partial<UserData>): Promise<void> {
|
|
const currentData = await this.loadUserData();
|
|
if (currentData) {
|
|
await this.saveUserData({ ...currentData, ...updates });
|
|
}
|
|
}
|
|
}
|