MVP Build

This commit is contained in:
Eric Gullickson
2025-08-09 12:47:15 -05:00
parent 2e8816df7f
commit 8f5117a4e2
92 changed files with 5910 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
/**
* @ai-summary PostgreSQL connection pool configuration
* @ai-context Shared pool for all feature repositories
*/
import { Pool } from 'pg';
import { logger } from '../logging/logger';
import { env } from './environment';
export const pool = new Pool({
host: env.DB_HOST,
port: env.DB_PORT,
database: env.DB_NAME,
user: env.DB_USER,
password: env.DB_PASSWORD,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
});
pool.on('connect', () => {
logger.debug('Database pool: client connected');
});
pool.on('error', (err) => {
logger.error('Database pool error', { error: err.message });
});
// Graceful shutdown
process.on('SIGTERM', async () => {
await pool.end();
});
export default pool;

View File

@@ -0,0 +1,51 @@
/**
* @ai-summary Environment configuration with validation
* @ai-context Validates all env vars at startup, single source of truth
*/
import { z } from 'zod';
import dotenv from 'dotenv';
dotenv.config();
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.string().transform(Number).default('3001'),
// Database
DB_HOST: z.string(),
DB_PORT: z.string().transform(Number),
DB_NAME: z.string(),
DB_USER: z.string(),
DB_PASSWORD: z.string(),
// Redis
REDIS_HOST: z.string(),
REDIS_PORT: z.string().transform(Number),
// Auth0
AUTH0_DOMAIN: z.string(),
AUTH0_CLIENT_ID: z.string(),
AUTH0_CLIENT_SECRET: z.string(),
AUTH0_AUDIENCE: z.string(),
// External APIs
GOOGLE_MAPS_API_KEY: z.string(),
VPIC_API_URL: z.string().default('https://vpic.nhtsa.dot.gov/api/vehicles'),
// MinIO
MINIO_ENDPOINT: z.string(),
MINIO_PORT: z.string().transform(Number),
MINIO_ACCESS_KEY: z.string(),
MINIO_SECRET_KEY: z.string(),
MINIO_BUCKET: z.string().default('motovaultpro'),
});
export type Environment = z.infer<typeof envSchema>;
// Validate and export
export const env = envSchema.parse(process.env);
// Convenience exports
export const isDevelopment = env.NODE_ENV === 'development';
export const isProduction = env.NODE_ENV === 'production';
export const isTest = env.NODE_ENV === 'test';

View File

@@ -0,0 +1,58 @@
/**
* @ai-summary Redis client and caching service
* @ai-context Used by all features for caching external API responses
*/
import Redis from 'ioredis';
import { logger } from '../logging/logger';
import { env } from './environment';
export const redis = new Redis({
host: env.REDIS_HOST,
port: env.REDIS_PORT,
retryStrategy: (times) => Math.min(times * 50, 2000),
});
redis.on('connect', () => {
logger.info('Redis connected');
});
redis.on('error', (err) => {
logger.error('Redis error', { error: err.message });
});
export class CacheService {
private prefix = 'mvp:';
async get<T>(key: string): Promise<T | null> {
try {
const data = await redis.get(this.prefix + key);
return data ? JSON.parse(data) : null;
} catch (error) {
logger.error('Cache get error', { key, error });
return null;
}
}
async set(key: string, value: any, ttl?: number): Promise<void> {
try {
const data = JSON.stringify(value);
if (ttl) {
await redis.setex(this.prefix + key, ttl, data);
} else {
await redis.set(this.prefix + key, data);
}
} catch (error) {
logger.error('Cache set error', { key, error });
}
}
async del(key: string): Promise<void> {
try {
await redis.del(this.prefix + key);
} catch (error) {
logger.error('Cache delete error', { key, error });
}
}
}
export const cacheService = new CacheService();

View File

@@ -0,0 +1,31 @@
/**
* @ai-summary Structured logging with Winston
* @ai-context All features use this for consistent logging
*/
import winston from 'winston';
import { env, isDevelopment } from '../config/environment';
export const logger = winston.createLogger({
level: isDevelopment ? 'debug' : 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: 'motovaultpro-backend',
environment: env.NODE_ENV,
},
transports: [
new winston.transports.Console({
format: isDevelopment
? winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
: winston.format.json(),
}),
],
});
export default logger;

View File

@@ -0,0 +1,24 @@
/**
* @ai-summary Global error handling middleware
*/
import { Request, Response, NextFunction } from 'express';
import { logger } from '../logging/logger';
export const errorHandler = (
err: Error,
req: Request,
res: Response,
_next: NextFunction
) => {
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
});
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
});
};

View File

@@ -0,0 +1,26 @@
/**
* @ai-summary Request logging middleware
*/
import { Request, Response, NextFunction } from 'express';
import { logger } from '../logging/logger';
export const requestLogger = (
req: Request,
res: Response,
next: NextFunction
) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('Request processed', {
method: req.method,
path: req.path,
status: res.statusCode,
duration,
ip: req.ip,
});
});
next();
};

View File

@@ -0,0 +1,48 @@
/**
* @ai-summary JWT authentication middleware using Auth0
* @ai-context Validates JWT tokens, adds user context to requests
*/
import { Request, Response, NextFunction } from 'express';
import { expressjwt as jwt } from 'express-jwt';
import jwks from 'jwks-rsa';
import { env } from '../config/environment';
import { logger } from '../logging/logger';
// Extend Express Request type
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export const authMiddleware = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${env.AUTH0_DOMAIN}/.well-known/jwks.json`,
}),
audience: env.AUTH0_AUDIENCE,
issuer: `https://${env.AUTH0_DOMAIN}/`,
algorithms: ['RS256'],
});
export const errorHandler = (
err: any,
req: Request,
res: Response,
next: NextFunction
) => {
if (err.name === 'UnauthorizedError') {
logger.warn('Unauthorized request', {
path: req.path,
ip: req.ip,
error: err.message,
});
res.status(401).json({ error: 'Unauthorized' });
} else {
next(err);
}
};