Vehicle drop down and Gas Station fixes
This commit is contained in:
@@ -10,15 +10,12 @@ import { pool } from '../../../core/config/database';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
import { CreateVehicleBody, UpdateVehicleBody, VehicleParams } from '../domain/vehicles.types';
|
||||
import { getStorageService } from '../../../core/storage/storage.service';
|
||||
import { Transform, TransformCallback, Readable } from 'stream';
|
||||
import crypto from 'crypto';
|
||||
import FileType from 'file-type';
|
||||
import path from 'path';
|
||||
|
||||
export class VehiclesController {
|
||||
private vehiclesService: VehiclesService;
|
||||
private static readonly MIN_YEAR = 2017;
|
||||
private static readonly MAX_YEAR = 2022;
|
||||
|
||||
constructor() {
|
||||
const repository = new VehiclesRepository(pool);
|
||||
@@ -160,13 +157,13 @@ export class VehiclesController {
|
||||
async getDropdownMakes(request: FastifyRequest<{ Querystring: { year: number } }>, reply: FastifyReply) {
|
||||
try {
|
||||
const { year } = request.query;
|
||||
if (!year || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR) {
|
||||
if (!year || isNaN(year)) {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: `Valid year parameter is required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
|
||||
message: 'Valid year parameter is required'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const makes = await this.vehiclesService.getDropdownMakes(year);
|
||||
return reply.code(200).send(makes);
|
||||
} catch (error) {
|
||||
@@ -181,10 +178,10 @@ export class VehiclesController {
|
||||
async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make: string } }>, reply: FastifyReply) {
|
||||
try {
|
||||
const { year, make } = request.query;
|
||||
if (!year || !make || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0) {
|
||||
if (!year || isNaN(year) || !make || make.trim().length === 0) {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: `Valid year and make parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
|
||||
message: 'Valid year and make parameters are required'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,10 +199,10 @@ export class VehiclesController {
|
||||
async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string } }>, reply: FastifyReply) {
|
||||
try {
|
||||
const { year, make, model, trim } = request.query;
|
||||
if (!year || !make || !model || !trim || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
|
||||
if (!year || isNaN(year) || !make || !model || !trim || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: `Valid year, make, model, and trim parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
|
||||
message: 'Valid year, make, model, and trim parameters are required'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,10 +220,10 @@ export class VehiclesController {
|
||||
async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string } }>, reply: FastifyReply) {
|
||||
try {
|
||||
const { year, make, model, trim } = request.query;
|
||||
if (!year || !make || !model || !trim || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
|
||||
if (!year || isNaN(year) || !make || !model || !trim || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: `Valid year, make, model, and trim parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
|
||||
message: 'Valid year, make, model, and trim parameters are required'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -244,10 +241,10 @@ export class VehiclesController {
|
||||
async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
|
||||
try {
|
||||
const { year, make, model } = request.query;
|
||||
if (!year || !make || !model || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0) {
|
||||
if (!year || isNaN(year) || !make || !model || make.trim().length === 0 || model.trim().length === 0) {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: `Valid year, make, and model parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
|
||||
message: 'Valid year, make, and model parameters are required'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -279,10 +276,10 @@ export class VehiclesController {
|
||||
async getDropdownOptions(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string; engine?: string; transmission?: string } }>, reply: FastifyReply) {
|
||||
try {
|
||||
const { year, make, model, trim, engine, transmission } = request.query;
|
||||
if (!year || !make || !model || !trim || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
|
||||
if (!year || isNaN(year) || !make || !model || !trim || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: `Valid year, make, model, and trim parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
|
||||
message: 'Valid year, make, model, and trim parameters are required'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -350,23 +347,16 @@ export class VehiclesController {
|
||||
});
|
||||
}
|
||||
|
||||
// Read first 4100 bytes to detect file type via magic bytes
|
||||
// Buffer the entire file for reliable processing
|
||||
// Vehicle images are typically small (< 10MB) so this is safe
|
||||
const chunks: Buffer[] = [];
|
||||
let totalBytes = 0;
|
||||
const targetBytes = 4100;
|
||||
|
||||
for await (const chunk of mp.file) {
|
||||
chunks.push(chunk);
|
||||
totalBytes += chunk.length;
|
||||
if (totalBytes >= targetBytes) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const headerBuffer = Buffer.concat(chunks);
|
||||
const fileBuffer = Buffer.concat(chunks);
|
||||
|
||||
// Validate actual file content using magic bytes
|
||||
const detectedType = await FileType.fromBuffer(headerBuffer);
|
||||
const detectedType = await FileType.fromBuffer(fileBuffer);
|
||||
|
||||
if (!detectedType) {
|
||||
logger.warn('Unable to detect file type from content', {
|
||||
@@ -424,39 +414,20 @@ export class VehiclesController {
|
||||
const originalName: string = mp.filename || 'vehicle-image';
|
||||
const ext = contentType === 'image/jpeg' ? 'jpg' : 'png';
|
||||
|
||||
class CountingStream extends Transform {
|
||||
public bytes = 0;
|
||||
override _transform(chunk: any, _enc: BufferEncoding, cb: TransformCallback) {
|
||||
this.bytes += chunk.length || 0;
|
||||
cb(null, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
const counter = new CountingStream();
|
||||
|
||||
// Create a new readable stream from the header buffer + remaining file chunks
|
||||
const headerStream = Readable.from([headerBuffer]);
|
||||
const remainingStream = mp.file;
|
||||
|
||||
// Pipe header first, then remaining content through counter
|
||||
headerStream.pipe(counter, { end: false });
|
||||
headerStream.on('end', () => {
|
||||
remainingStream.pipe(counter);
|
||||
});
|
||||
|
||||
const storage = getStorageService();
|
||||
const bucket = 'vehicle-images';
|
||||
const unique = crypto.randomBytes(32).toString('hex');
|
||||
const key = `vehicle-images/${userId}/${vehicleId}/${unique}.${ext}`;
|
||||
|
||||
await storage.putObject(bucket, key, counter, contentType, { 'x-original-filename': originalName });
|
||||
// Write buffer directly to storage
|
||||
await storage.putObject(bucket, key, fileBuffer, contentType, { 'x-original-filename': originalName });
|
||||
|
||||
const updated = await this.vehiclesService.updateVehicleImage(vehicleId, userId, {
|
||||
imageStorageBucket: bucket,
|
||||
imageStorageKey: key,
|
||||
imageFileName: originalName,
|
||||
imageContentType: contentType,
|
||||
imageFileSize: counter.bytes,
|
||||
imageFileSize: fileBuffer.length,
|
||||
});
|
||||
|
||||
logger.info('Vehicle image upload completed', {
|
||||
@@ -466,7 +437,7 @@ export class VehiclesController {
|
||||
fileName: originalName,
|
||||
contentType,
|
||||
detectedType: detectedType.mime,
|
||||
fileSize: counter.bytes,
|
||||
fileSize: fileBuffer.length,
|
||||
storageKey: key,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user