MVP Build
This commit is contained in:
164
backend/src/features/vehicles/api/vehicles.controller.ts
Normal file
164
backend/src/features/vehicles/api/vehicles.controller.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @ai-summary HTTP request handlers for vehicles API
|
||||
* @ai-context Handles validation, auth, and delegates to service layer
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { VehiclesService } from '../domain/vehicles.service';
|
||||
import { VehiclesRepository } from '../data/vehicles.repository';
|
||||
import { pool } from '../../../core/config/database';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
import { ZodError } from 'zod';
|
||||
import {
|
||||
createVehicleSchema,
|
||||
updateVehicleSchema,
|
||||
vehicleIdSchema,
|
||||
CreateVehicleInput,
|
||||
UpdateVehicleInput,
|
||||
} from './vehicles.validation';
|
||||
|
||||
export class VehiclesController {
|
||||
private service: VehiclesService;
|
||||
|
||||
constructor() {
|
||||
const repository = new VehiclesRepository(pool);
|
||||
this.service = new VehiclesService(repository);
|
||||
}
|
||||
|
||||
createVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
// Validate request body
|
||||
const data = createVehicleSchema.parse(req.body) as CreateVehicleInput;
|
||||
|
||||
// Get user ID from JWT token
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicle = await this.service.createVehicle(data, userId);
|
||||
|
||||
logger.info('Vehicle created successfully', { vehicleId: vehicle.id, userId });
|
||||
res.status(201).json(vehicle);
|
||||
} catch (error: any) {
|
||||
if (error instanceof ZodError) {
|
||||
res.status(400).json({ error: error.errors[0].message });
|
||||
return;
|
||||
}
|
||||
if (error.message === 'Invalid VIN format' ||
|
||||
error.message === 'Vehicle with this VIN already exists') {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getUserVehicles = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicles = await this.service.getUserVehicles(userId);
|
||||
res.json(vehicles);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
const { id } = vehicleIdSchema.parse(req.params);
|
||||
const userId = req.user?.sub;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicle = await this.service.getVehicle(id, userId);
|
||||
res.json(vehicle);
|
||||
} catch (error: any) {
|
||||
if (error instanceof ZodError) {
|
||||
res.status(400).json({ error: error.errors[0].message });
|
||||
return;
|
||||
}
|
||||
if (error.message === 'Vehicle not found') {
|
||||
res.status(404).json({ error: 'Vehicle not found' });
|
||||
return;
|
||||
}
|
||||
if (error.message === 'Unauthorized') {
|
||||
res.status(403).json({ error: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
updateVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
const { id } = vehicleIdSchema.parse(req.params);
|
||||
const data = updateVehicleSchema.parse(req.body) as UpdateVehicleInput;
|
||||
const userId = req.user?.sub;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicle = await this.service.updateVehicle(id, data, userId);
|
||||
|
||||
logger.info('Vehicle updated successfully', { vehicleId: id, userId });
|
||||
res.json(vehicle);
|
||||
} catch (error: any) {
|
||||
if (error instanceof ZodError) {
|
||||
res.status(400).json({ error: error.errors[0].message });
|
||||
return;
|
||||
}
|
||||
if (error.message === 'Vehicle not found') {
|
||||
res.status(404).json({ error: 'Vehicle not found' });
|
||||
return;
|
||||
}
|
||||
if (error.message === 'Unauthorized') {
|
||||
res.status(403).json({ error: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
deleteVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
const { id } = vehicleIdSchema.parse(req.params);
|
||||
const userId = req.user?.sub;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.service.deleteVehicle(id, userId);
|
||||
|
||||
logger.info('Vehicle deleted successfully', { vehicleId: id, userId });
|
||||
res.status(204).send();
|
||||
} catch (error: any) {
|
||||
if (error instanceof ZodError) {
|
||||
res.status(400).json({ error: error.errors[0].message });
|
||||
return;
|
||||
}
|
||||
if (error.message === 'Vehicle not found') {
|
||||
res.status(404).json({ error: 'Vehicle not found' });
|
||||
return;
|
||||
}
|
||||
if (error.message === 'Unauthorized') {
|
||||
res.status(403).json({ error: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
25
backend/src/features/vehicles/api/vehicles.routes.ts
Normal file
25
backend/src/features/vehicles/api/vehicles.routes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @ai-summary Express routes for vehicles API
|
||||
* @ai-context Defines REST endpoints with auth middleware
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { VehiclesController } from './vehicles.controller';
|
||||
import { authMiddleware } from '../../../core/security/auth.middleware';
|
||||
|
||||
export function registerVehiclesRoutes(): Router {
|
||||
const router = Router();
|
||||
const controller = new VehiclesController();
|
||||
|
||||
// All vehicle routes require authentication
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Routes
|
||||
router.post('/api/vehicles', controller.createVehicle);
|
||||
router.get('/api/vehicles', controller.getUserVehicles);
|
||||
router.get('/api/vehicles/:id', controller.getVehicle);
|
||||
router.put('/api/vehicles/:id', controller.updateVehicle);
|
||||
router.delete('/api/vehicles/:id', controller.deleteVehicle);
|
||||
|
||||
return router;
|
||||
}
|
||||
32
backend/src/features/vehicles/api/vehicles.validation.ts
Normal file
32
backend/src/features/vehicles/api/vehicles.validation.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @ai-summary Request validation schemas for vehicles API
|
||||
* @ai-context Uses Zod for runtime validation and type safety
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { isValidVIN } from '../../../shared-minimal/utils/validators';
|
||||
|
||||
export const createVehicleSchema = z.object({
|
||||
vin: z.string()
|
||||
.length(17, 'VIN must be exactly 17 characters')
|
||||
.refine(isValidVIN, 'Invalid VIN format'),
|
||||
nickname: z.string().min(1).max(100).optional(),
|
||||
color: z.string().min(1).max(50).optional(),
|
||||
licensePlate: z.string().min(1).max(20).optional(),
|
||||
odometerReading: z.number().min(0).max(9999999).optional(),
|
||||
});
|
||||
|
||||
export const updateVehicleSchema = z.object({
|
||||
nickname: z.string().min(1).max(100).optional(),
|
||||
color: z.string().min(1).max(50).optional(),
|
||||
licensePlate: z.string().min(1).max(20).optional(),
|
||||
odometerReading: z.number().min(0).max(9999999).optional(),
|
||||
}).strict();
|
||||
|
||||
export const vehicleIdSchema = z.object({
|
||||
id: z.string().uuid('Invalid vehicle ID format'),
|
||||
});
|
||||
|
||||
export type CreateVehicleInput = z.infer<typeof createVehicleSchema>;
|
||||
export type UpdateVehicleInput = z.infer<typeof updateVehicleSchema>;
|
||||
export type VehicleIdInput = z.infer<typeof vehicleIdSchema>;
|
||||
Reference in New Issue
Block a user