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:
36
backend-api/src/middleware/auth.ts
Normal file
36
backend-api/src/middleware/auth.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {FastifyRequest, FastifyReply} from 'fastify';
|
||||
import {UnauthorizedError} from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Extend Fastify Request with user property
|
||||
*/
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication Middleware
|
||||
* Verifies JWT token and attaches user to request
|
||||
*/
|
||||
export async function authenticate(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch (err) {
|
||||
throw new UnauthorizedError('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract user ID from authenticated request
|
||||
*/
|
||||
export function getUserId(request: FastifyRequest): string {
|
||||
if (!request.user || !request.user.id) {
|
||||
throw new UnauthorizedError('User not authenticated');
|
||||
}
|
||||
return request.user.id;
|
||||
}
|
||||
64
backend-api/src/middleware/errorHandler.ts
Normal file
64
backend-api/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {FastifyError, FastifyReply, FastifyRequest} from 'fastify';
|
||||
import {AppError} from '../utils/errors';
|
||||
import {ZodError} from 'zod';
|
||||
|
||||
/**
|
||||
* Global Error Handler
|
||||
* Implements Single Responsibility: Handles all error responses
|
||||
*/
|
||||
export async function errorHandler(error: FastifyError, request: FastifyRequest, reply: FastifyReply) {
|
||||
// Log error
|
||||
request.log.error(error);
|
||||
|
||||
// Handle custom app errors
|
||||
if (error instanceof AppError) {
|
||||
return reply.status(error.statusCode).send({
|
||||
error: error.name,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (error instanceof ZodError) {
|
||||
return reply.status(400).send({
|
||||
error: 'ValidationError',
|
||||
message: 'Invalid request data',
|
||||
details: error.errors,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Fastify validation errors
|
||||
if (error.validation) {
|
||||
return reply.status(400).send({
|
||||
error: 'ValidationError',
|
||||
message: error.message,
|
||||
details: error.validation,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Prisma errors
|
||||
if (error.name === 'PrismaClientKnownRequestError') {
|
||||
const prismaError = error as any;
|
||||
if (prismaError.code === 'P2002') {
|
||||
return reply.status(409).send({
|
||||
error: 'ConflictError',
|
||||
message: 'A record with this value already exists',
|
||||
});
|
||||
}
|
||||
if (prismaError.code === 'P2025') {
|
||||
return reply.status(404).send({
|
||||
error: 'NotFoundError',
|
||||
message: 'Record not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Default server error
|
||||
const statusCode = error.statusCode || 500;
|
||||
const message = statusCode === 500 ? 'Internal server error' : error.message;
|
||||
|
||||
return reply.status(statusCode).send({
|
||||
error: 'ServerError',
|
||||
message,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user