Add backend API for personal finance management application

- Introduced a comprehensive backend API using TypeScript, Fastify, and PostgreSQL.
- Added essential files including architecture documentation, environment configuration, and Docker setup.
- Implemented RESTful routes for managing assets, liabilities, clients, invoices, and cashflow.
- Established a robust database schema with Prisma for data management.
- Integrated middleware for authentication and error handling.
- Created service and repository layers to adhere to SOLID principles and clean architecture.
- Included example environment variables for development, staging, and production setups.
This commit is contained in:
2025-12-07 12:59:09 -05:00
parent 9d493ba82f
commit cd93dcbfd2
70 changed files with 8649 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
import {NavLink, Outlet} from 'react-router-dom';
import {TrendingUp, CreditCard, FileText, Users, ArrowLeftRight} from 'lucide-react';
import {TrendingUp, CreditCard, FileText, Users, ArrowLeftRight, LogOut} from 'lucide-react';
import {useAppSelector, useAppDispatch, clearUser} from '@/store';
const navItems = [
{to: '/', label: 'Net Worth', icon: TrendingUp},
@@ -10,6 +11,13 @@ const navItems = [
];
export default function Layout() {
const dispatch = useAppDispatch();
const {currentUser} = useAppSelector(state => state.user);
const handleLogout = () => {
dispatch(clearUser());
};
return (
<div className="flex min-h-screen">
{/* Sidebar */}
@@ -35,6 +43,26 @@ export default function Layout() {
</NavLink>
))}
</nav>
{/* User section */}
<div className="border-t border-border p-2">
<div className="flex items-center gap-2 px-2.5 py-2 rounded-lg">
<div className="h-7 w-7 rounded-full bg-accent flex items-center justify-center text-xs font-medium shrink-0">
{currentUser?.name?.charAt(0).toUpperCase() || '?'}
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-200 overflow-hidden">
<p className="text-sm font-medium truncate max-w-24">{currentUser?.name}</p>
<p className="text-xs text-muted-foreground truncate max-w-24">{currentUser?.email}</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex h-9 w-full items-center gap-3 rounded-lg px-2.5 text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors"
>
<LogOut className="h-[18px] w-[18px] shrink-0" />
<span className="text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">Log out</span>
</button>
</div>
</aside>
{/* Main content */}

View File

@@ -0,0 +1,13 @@
import {Navigate, Outlet} from 'react-router-dom';
import {useAppSelector} from '@/store';
export default function ProtectedRoute() {
const {isAuthenticated} = useAppSelector(state => state.user);
if (!isAuthenticated) {
return <Navigate to="/welcome" replace />;
}
return <Outlet />;
}

View File

@@ -0,0 +1,88 @@
import {useState} from 'react';
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import {useAppDispatch, setUser} from '@/store';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
onSwitchToSignUp: () => void;
}
export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Props) {
const dispatch = useAppDispatch();
const [form, setForm] = useState({
email: '',
password: '',
});
const [error, setError] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError('');
// Mock login - in production this would validate against an API
if (!form.email || !form.password) {
setError('Please enter your email and password');
return;
}
// Mock successful login
dispatch(setUser({
id: crypto.randomUUID(),
email: form.email,
name: form.email.split('@')[0],
}));
onOpenChange(false);
setForm({email: '', password: ''});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="card-elevated sm:max-w-md">
<DialogHeader>
<DialogTitle>Welcome back</DialogTitle>
<DialogDescription>Log in to your account</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="login-email">Email</Label>
<Input
id="login-email"
type="email"
placeholder="john@example.com"
value={form.email}
onChange={e => setForm({...form, email: e.target.value})}
className="input-depth"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="login-password">Password</Label>
<Input
id="login-password"
type="password"
placeholder="••••••••"
value={form.password}
onChange={e => setForm({...form, password: e.target.value})}
className="input-depth"
required
/>
</div>
{error && <p className="text-sm text-red-400">{error}</p>}
</div>
<DialogFooter className="flex-col gap-2 sm:flex-col">
<Button type="submit" className="w-full">Log in</Button>
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToSignUp}>
Don't have an account? Sign up
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,122 @@
import {useState} from 'react';
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import {useAppDispatch, setUser} from '@/store';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
onSwitchToLogin: () => void;
}
export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Props) {
const dispatch = useAppDispatch();
const [form, setForm] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (form.password !== form.confirmPassword) {
setError('Passwords do not match');
return;
}
if (form.password.length < 6) {
setError('Password must be at least 6 characters');
return;
}
// Mock sign up - in production this would call an API
dispatch(setUser({
id: crypto.randomUUID(),
email: form.email,
name: form.name,
}));
onOpenChange(false);
setForm({name: '', email: '', password: '', confirmPassword: ''});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="card-elevated sm:max-w-md">
<DialogHeader>
<DialogTitle>Create an account</DialogTitle>
<DialogDescription>Enter your details to get started</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="signup-name">Name</Label>
<Input
id="signup-name"
type="text"
placeholder="John Doe"
value={form.name}
onChange={e => setForm({...form, name: e.target.value})}
className="input-depth"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="signup-email">Email</Label>
<Input
id="signup-email"
type="email"
placeholder="john@example.com"
value={form.email}
onChange={e => setForm({...form, email: e.target.value})}
className="input-depth"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="signup-password">Password</Label>
<Input
id="signup-password"
type="password"
placeholder="••••••••"
value={form.password}
onChange={e => setForm({...form, password: e.target.value})}
className="input-depth"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="signup-confirm">Confirm Password</Label>
<Input
id="signup-confirm"
type="password"
placeholder="••••••••"
value={form.confirmPassword}
onChange={e => setForm({...form, confirmPassword: e.target.value})}
className="input-depth"
required
/>
</div>
{error && <p className="text-sm text-red-400">{error}</p>}
<p className="text-xs text-muted-foreground">
By signing up, you agree to our Terms of Service and Privacy Policy.
</p>
</div>
<DialogFooter className="flex-col gap-2 sm:flex-col">
<Button type="submit" className="w-full">Create account</Button>
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToLogin}>
Already have an account? Log in
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}