MVP Build
This commit is contained in:
33
backend/src/core/config/database.ts
Normal file
33
backend/src/core/config/database.ts
Normal 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;
|
||||
51
backend/src/core/config/environment.ts
Normal file
51
backend/src/core/config/environment.ts
Normal 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';
|
||||
58
backend/src/core/config/redis.ts
Normal file
58
backend/src/core/config/redis.ts
Normal 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();
|
||||
31
backend/src/core/logging/logger.ts
Normal file
31
backend/src/core/logging/logger.ts
Normal 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;
|
||||
24
backend/src/core/middleware/error.middleware.ts
Normal file
24
backend/src/core/middleware/error.middleware.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
26
backend/src/core/middleware/logging.middleware.ts
Normal file
26
backend/src/core/middleware/logging.middleware.ts
Normal 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();
|
||||
};
|
||||
48
backend/src/core/security/auth.middleware.ts
Normal file
48
backend/src/core/security/auth.middleware.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user