diff --git a/apps/api/package.json b/apps/api/package.json index 83fb984..9dd4d78 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -41,8 +41,10 @@ "react-dom": "^18.3.1", "resend": "^3.2.0", "socket.io": "^4.7.5", + "swagger-ui-express": "^5.0.1", "twilio": "^5.1.0", - "zod": "^3.23.0" + "zod": "^3.23.0", + "zod-to-json-schema": "^3.25.2" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", @@ -58,6 +60,7 @@ "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@types/supertest": "^6.0.2", + "@types/swagger-ui-express": "^4.1.8", "supertest": "^7.0.0", "ts-node-dev": "^2.0.0", "typescript": "^5.4.0", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 70f6b59..55f1a4e 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -2,6 +2,8 @@ import express from 'express' import cors from 'cors' import helmet from 'helmet' import morgan from 'morgan' +import swaggerUi from 'swagger-ui-express' +import { openApiDocument } from './swagger/openapi' import { getLegacyStorageRoots, getStorageRoot } from './lib/storage' import { authLimiter, apiLimiter, publicLimiter, adminLimiter } from './middleware/rateLimiter' @@ -100,6 +102,10 @@ export function createApp() { app.use('/storage', express.static(legacyRoot)) } + // Swagger UI — mounted before helmet so its assets are not blocked by CSP + app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument, { customSiteTitle: 'RentalDriveGo API Docs' })) + app.get('/api/v1/openapi.json', (_req, res) => res.json(openApiDocument)) + // Webhook must use raw body BEFORE express.json() app.use('/api/v1/webhooks', express.raw({ type: 'application/json' }), webhookRouter) @@ -141,14 +147,14 @@ export function createApp() { }) app.get(`${v1}/docs`, (_req, res) => { - res.json({ name: 'rentaldrivego-api', version: '1.0.0', baseUrl: v1, docsUrl: '/docs', routes: routeDocs }) - }) - - app.get('/docs', (_req, res) => { - const rows = routeDocs - .map((r) => `${r.method}${r.path}${r.description}`) - .join('') - res.type('html').send(`RentalDriveGo API Docs

RentalDriveGo API

${rows}
MethodPathDescription
`) + res.json({ + name: 'rentaldrivego-api', + version: '1.0.0', + baseUrl: v1, + routes: routeDocs, + swaggerUi: '/docs', + openApi: '/api/v1/openapi.json', + }) }) // ─── Error handler ────────────────────────────────────────── diff --git a/apps/api/src/swagger/openapi.ts b/apps/api/src/swagger/openapi.ts new file mode 100644 index 0000000..33c0b2d --- /dev/null +++ b/apps/api/src/swagger/openapi.ts @@ -0,0 +1,1146 @@ +import { zodToJsonSchema } from 'zod-to-json-schema' + +// ── Vehicles ───────────────────────────────────────────────────────────────── +import { + vehicleSchema, + calendarBlockSchema, + maintenanceLogSchema, + publishSchema, + statusSchema, +} from '../modules/vehicles/vehicle.schemas' + +// ── Reservations ───────────────────────────────────────────────────────────── +import { + createSchema as reservationCreateSchema, + updateSchema as reservationUpdateSchema, + checkinSchema, + checkoutSchema, + extendSchema, + cancelSchema, + approvalSchema, + inspectionSchema, + additionalDriverSchema, +} from '../modules/reservations/reservation.schemas' + +// ── Customers ───────────────────────────────────────────────────────────────── +import { + customerSchema, + approveLicenseSchema, + flagSchema, +} from '../modules/customers/customer.schemas' + +// ── Offers ──────────────────────────────────────────────────────────────────── +import { offerSchema } from '../modules/offers/offer.schemas' + +// ── Payments ───────────────────────────────────────────────────────────────── +import { + chargeSchema, + manualPaymentSchema, + refundSchema, +} from '../modules/payments/payment.schemas' + +// ── Reviews ─────────────────────────────────────────────────────────────────── +import { replySchema } from '../modules/reviews/review.schemas' + +// ── Complaints ──────────────────────────────────────────────────────────────── +import { + createSchema as complaintCreateSchema, + updateSchema as complaintUpdateSchema, +} from '../modules/complaints/complaint.schemas' + +// ── Auth ────────────────────────────────────────────────────────────────────── +import { companySignupSchema } from '../modules/auth/auth.company.schemas' +import { + employeeLoginSchema, + employeeForgotPasswordSchema, + employeeResetPasswordSchema, + employeeLanguageSchema, +} from '../modules/auth/auth.employee.schemas' +import { renterUpdateSchema, renterFcmTokenSchema } from '../modules/auth/auth.renter.schemas' + +// ───────────────────────────────────────────────────────────────────────────── + +function s(schema: Parameters[0]): Record { + const { $schema, ...rest } = zodToJsonSchema(schema, { target: 'openApi3' }) as Record + return rest +} + +const idPath = (name = 'id') => ({ + name, + in: 'path' as const, + required: true, + schema: { type: 'string' }, +}) + +const pagingParams = [ + { name: 'page', in: 'query', schema: { type: 'integer', default: 1 } }, + { name: 'pageSize', in: 'query', schema: { type: 'integer', default: 20 } }, +] + +const jsonBody = (ref: string) => ({ + required: true, + content: { 'application/json': { schema: { $ref: ref } } }, +}) + +const ok = { description: 'Success' } +const err4 = { description: 'Validation error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } } +const err401 = { description: 'Unauthorized' } +const err404 = { description: 'Not found' } + +export const openApiDocument = { + openapi: '3.0.3', + info: { + title: 'RentalDriveGo API', + description: 'REST API for the RentalDriveGo car rental management platform.\n\n**Auth:** Most endpoints require a Bearer JWT. Obtain a token via `/auth/employee/login` (dashboard/admin users) or via Firebase for renters.', + version: '1.0.0', + contact: { name: 'RentalDriveGo', email: 'support@rentaldrivego.com' }, + }, + servers: [{ url: '/api/v1', description: 'v1' }], + security: [{ bearerAuth: [] }], + tags: [ + { name: 'Health', description: 'Service health' }, + { name: 'Auth — Company', description: 'Company account registration' }, + { name: 'Auth — Employee', description: 'Employee (dashboard) authentication' }, + { name: 'Auth — Renter', description: 'Renter (mobile) authentication' }, + { name: 'Vehicles', description: 'Fleet management' }, + { name: 'Reservations', description: 'Booking lifecycle' }, + { name: 'Customers', description: 'Customer profiles & documents' }, + { name: 'Offers', description: 'Discounts & promo codes' }, + { name: 'Payments', description: 'Charges, manual payments, refunds' }, + { name: 'Reviews', description: 'Customer reviews' }, + { name: 'Complaints', description: 'Complaint tracking' }, + { name: 'Analytics', description: 'Dashboard & reports' }, + { name: 'Notifications', description: 'In-app notifications' }, + { name: 'Companies', description: 'Company profile & settings' }, + { name: 'Team', description: 'Employee management' }, + { name: 'Subscriptions', description: 'Plan management & billing' }, + { name: 'Marketplace', description: 'Public booking portal' }, + { name: 'Site', description: 'White-label site API' }, + { name: 'Admin', description: 'Platform administration' }, + ], + components: { + securitySchemes: { + bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + }, + schemas: { + // ── Vehicles ──────────────────────────────────────────────── + VehicleInput: s(vehicleSchema), + CalendarBlock: s(calendarBlockSchema), + MaintenanceLog: s(maintenanceLogSchema), + VehiclePublish: s(publishSchema), + VehicleStatus: s(statusSchema), + // ── Reservations ──────────────────────────────────────────── + ReservationCreate: s(reservationCreateSchema), + ReservationUpdate: s(reservationUpdateSchema), + AdditionalDriver: s(additionalDriverSchema), + Inspection: s(inspectionSchema), + ReservationCheckin: s(checkinSchema), + ReservationCheckout:s(checkoutSchema), + ReservationExtend: s(extendSchema), + ReservationCancel: s(cancelSchema), + ReservationApproval:s(approvalSchema), + // ── Customers ─────────────────────────────────────────────── + CustomerInput: s(customerSchema), + ApproveLicense: s(approveLicenseSchema), + FlagCustomer: s(flagSchema), + // ── Offers ────────────────────────────────────────────────── + OfferInput: s(offerSchema), + // ── Payments ──────────────────────────────────────────────── + PaymentCharge: s(chargeSchema), + ManualPayment: s(manualPaymentSchema), + Refund: s(refundSchema), + // ── Reviews ───────────────────────────────────────────────── + ReviewReply: s(replySchema), + // ── Complaints ────────────────────────────────────────────── + ComplaintCreate: s(complaintCreateSchema), + ComplaintUpdate: s(complaintUpdateSchema), + // ── Auth ──────────────────────────────────────────────────── + CompanySignup: s(companySignupSchema), + EmployeeLogin: s(employeeLoginSchema), + EmployeeForgotPassword: s(employeeForgotPasswordSchema), + EmployeeResetPassword: s(employeeResetPasswordSchema), + EmployeeLanguage: s(employeeLanguageSchema), + RenterUpdate: s(renterUpdateSchema), + RenterFcmToken: s(renterFcmTokenSchema), + }, + }, + paths: { + // ════════════════════════════════════════════════════════════════ + // HEALTH (no /api/v1 prefix — served directly from app root) + // ════════════════════════════════════════════════════════════════ + '/health': { + get: { + tags: ['Health'], + summary: 'Health check', + security: [], + servers: [{ url: '/' }], + responses: { + '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string', example: 'ok' }, version: { type: 'string' }, timestamp: { type: 'string', format: 'date-time' } } } } } }, + }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // AUTH — COMPANY + // ════════════════════════════════════════════════════════════════ + '/auth/company/signup': { + post: { + tags: ['Auth — Company'], + summary: 'Register a new company', + security: [], + requestBody: jsonBody('#/components/schemas/CompanySignup'), + responses: { '201': ok, '400': err4, '409': { description: 'Email already in use' } }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // AUTH — EMPLOYEE + // ════════════════════════════════════════════════════════════════ + '/auth/employee/login': { + post: { + tags: ['Auth — Employee'], + summary: 'Login', + security: [], + requestBody: jsonBody('#/components/schemas/EmployeeLogin'), + responses: { + '200': { description: 'Token + employee object', content: { 'application/json': { schema: { type: 'object', properties: { token: { type: 'string' }, employee: { type: 'object' } } } } } }, + '401': err401, + }, + }, + }, + '/auth/employee/forgot-password': { + post: { + tags: ['Auth — Employee'], + summary: 'Request password-reset email', + security: [], + requestBody: jsonBody('#/components/schemas/EmployeeForgotPassword'), + responses: { '200': ok }, + }, + }, + '/auth/employee/reset-password': { + post: { + tags: ['Auth — Employee'], + summary: 'Reset password with token', + security: [], + requestBody: jsonBody('#/components/schemas/EmployeeResetPassword'), + responses: { '200': ok, '400': err4 }, + }, + }, + '/auth/employee/me': { + get: { + tags: ['Auth — Employee'], + summary: 'Current employee profile', + responses: { '200': ok, '401': err401 }, + }, + }, + '/auth/employee/me/language': { + patch: { + tags: ['Auth — Employee'], + summary: 'Update preferred language', + requestBody: jsonBody('#/components/schemas/EmployeeLanguage'), + responses: { '200': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // AUTH — RENTER + // ════════════════════════════════════════════════════════════════ + '/auth/renter/me': { + get: { + tags: ['Auth — Renter'], + summary: 'Current renter profile', + responses: { '200': ok, '401': err401 }, + }, + patch: { + tags: ['Auth — Renter'], + summary: 'Update renter profile', + requestBody: jsonBody('#/components/schemas/RenterUpdate'), + responses: { '200': ok }, + }, + }, + '/auth/renter/me/fcm-token': { + post: { + tags: ['Auth — Renter'], + summary: 'Register FCM push token', + requestBody: jsonBody('#/components/schemas/RenterFcmToken'), + responses: { '200': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // VEHICLES + // ════════════════════════════════════════════════════════════════ + '/vehicles': { + get: { + tags: ['Vehicles'], + summary: 'List vehicles', + parameters: [ + { name: 'status', in: 'query', schema: { type: 'string', enum: ['AVAILABLE','RESERVED','READY','RENTED','RETURNED','NEEDS_CLEANING','MAINTENANCE','DAMAGE_REVIEW','OUT_OF_SERVICE'] } }, + { name: 'category', in: 'query', schema: { type: 'string', enum: ['ECONOMY','COMPACT','MIDSIZE','FULLSIZE','SUV','LUXURY','VAN','TRUCK'] } }, + { name: 'published', in: 'query', schema: { type: 'string', enum: ['true','false'] } }, + ...pagingParams, + ], + responses: { '200': ok }, + }, + post: { + tags: ['Vehicles'], + summary: 'Create vehicle', + requestBody: jsonBody('#/components/schemas/VehicleInput'), + responses: { '201': ok, '400': err4 }, + }, + }, + '/vehicles/{id}': { + get: { + tags: ['Vehicles'], + summary: 'Get vehicle', + parameters: [idPath()], + responses: { '200': ok, '404': err404 }, + }, + patch: { + tags: ['Vehicles'], + summary: 'Update vehicle', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/VehicleInput'), + responses: { '200': ok, '404': err404 }, + }, + delete: { + tags: ['Vehicles'], + summary: 'Delete vehicle', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/status': { + patch: { + tags: ['Vehicles'], + summary: 'Set vehicle status', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/VehicleStatus'), + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/publish': { + patch: { + tags: ['Vehicles'], + summary: 'Publish / unpublish vehicle', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/VehiclePublish'), + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/photos': { + post: { + tags: ['Vehicles'], + summary: 'Upload photos (multipart, max 10)', + parameters: [idPath()], + requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { photos: { type: 'array', items: { type: 'string', format: 'binary' } } } } } } }, + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/photos/{idx}': { + delete: { + tags: ['Vehicles'], + summary: 'Delete photo by index', + parameters: [idPath(), { name: 'idx', in: 'path', required: true, schema: { type: 'integer' } }], + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/availability': { + get: { + tags: ['Vehicles'], + summary: 'Check availability for a date range', + parameters: [ + idPath(), + { name: 'startDate', in: 'query', required: true, schema: { type: 'string', format: 'date-time' } }, + { name: 'endDate', in: 'query', required: true, schema: { type: 'string', format: 'date-time' } }, + ], + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/calendar': { + get: { + tags: ['Vehicles'], + summary: 'Get monthly calendar events', + parameters: [ + idPath(), + { name: 'year', in: 'query', required: true, schema: { type: 'integer' } }, + { name: 'month', in: 'query', required: true, schema: { type: 'integer', minimum: 1, maximum: 12 } }, + ], + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/calendar/blocks': { + post: { + tags: ['Vehicles'], + summary: 'Create calendar block', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/CalendarBlock'), + responses: { '201': ok }, + }, + }, + '/vehicles/{id}/calendar/blocks/{blockId}': { + delete: { + tags: ['Vehicles'], + summary: 'Delete calendar block', + parameters: [idPath(), idPath('blockId')], + responses: { '200': ok }, + }, + }, + '/vehicles/{id}/maintenance': { + get: { + tags: ['Vehicles'], + summary: 'List maintenance logs', + parameters: [idPath()], + responses: { '200': ok }, + }, + post: { + tags: ['Vehicles'], + summary: 'Add maintenance log', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/MaintenanceLog'), + responses: { '201': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // RESERVATIONS + // ════════════════════════════════════════════════════════════════ + '/reservations': { + get: { + tags: ['Reservations'], + summary: 'List reservations', + parameters: [ + { name: 'status', in: 'query', schema: { type: 'string' } }, + { name: 'vehicleId', in: 'query', schema: { type: 'string' } }, + { name: 'source', in: 'query', schema: { type: 'string' } }, + { name: 'startDate', in: 'query', schema: { type: 'string' } }, + { name: 'endDate', in: 'query', schema: { type: 'string' } }, + ...pagingParams, + ], + responses: { '200': ok }, + }, + post: { + tags: ['Reservations'], + summary: 'Create reservation', + requestBody: jsonBody('#/components/schemas/ReservationCreate'), + responses: { '201': ok, '400': err4 }, + }, + }, + '/reservations/{id}': { + get: { + tags: ['Reservations'], + summary: 'Get reservation', + parameters: [idPath()], + responses: { '200': ok, '404': err404 }, + }, + patch: { + tags: ['Reservations'], + summary: 'Update reservation', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ReservationUpdate'), + responses: { '200': ok }, + }, + }, + '/reservations/{id}/confirm': { + post: { + tags: ['Reservations'], + summary: 'Confirm reservation', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/reservations/{id}/approve': { + patch: { + tags: ['Reservations'], + summary: 'Approve or reject pending reservation', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ReservationApproval'), + responses: { '200': ok }, + }, + }, + '/reservations/{id}/checkin': { + post: { + tags: ['Reservations'], + summary: 'Check in', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ReservationCheckin'), + responses: { '200': ok }, + }, + }, + '/reservations/{id}/checkout': { + post: { + tags: ['Reservations'], + summary: 'Check out', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ReservationCheckout'), + responses: { '200': ok }, + }, + }, + '/reservations/{id}/close': { + post: { + tags: ['Reservations'], + summary: 'Close reservation', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/reservations/{id}/extend': { + post: { + tags: ['Reservations'], + summary: 'Extend reservation end date', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ReservationExtend'), + responses: { '200': ok }, + }, + }, + '/reservations/{id}/cancel': { + post: { + tags: ['Reservations'], + summary: 'Cancel reservation', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ReservationCancel'), + responses: { '200': ok }, + }, + }, + '/reservations/{id}/contract': { + get: { + tags: ['Reservations'], + summary: 'Download rental contract (PDF)', + parameters: [idPath()], + responses: { '200': { description: 'PDF file', content: { 'application/pdf': {} } } }, + }, + }, + '/reservations/{id}/billing': { + get: { + tags: ['Reservations'], + summary: 'Get billing summary', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/reservations/{id}/inspections': { + get: { + tags: ['Reservations'], + summary: 'List inspections', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/reservations/{id}/inspections/{type}': { + put: { + tags: ['Reservations'], + summary: 'Save inspection (CHECKIN or CHECKOUT)', + parameters: [ + idPath(), + { name: 'type', in: 'path', required: true, schema: { type: 'string', enum: ['CHECKIN', 'CHECKOUT'] } }, + ], + requestBody: jsonBody('#/components/schemas/Inspection'), + responses: { '200': ok }, + }, + }, + '/reservations/{id}/photos': { + get: { + tags: ['Reservations'], + summary: 'List reservation photos', + parameters: [idPath()], + responses: { '200': ok }, + }, + post: { + tags: ['Reservations'], + summary: 'Upload reservation photo', + parameters: [idPath()], + requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { photo: { type: 'string', format: 'binary' } } } } } }, + responses: { '201': ok }, + }, + }, + '/reservations/{id}/photos/{photoId}': { + delete: { + tags: ['Reservations'], + summary: 'Delete reservation photo', + parameters: [idPath(), idPath('photoId')], + responses: { '200': ok }, + }, + }, + '/reservations/{id}/additional-drivers/{driverId}/approval': { + patch: { + tags: ['Reservations'], + summary: 'Approve/reject additional driver', + parameters: [idPath(), idPath('driverId')], + requestBody: jsonBody('#/components/schemas/ReservationApproval'), + responses: { '200': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // CUSTOMERS + // ════════════════════════════════════════════════════════════════ + '/customers': { + get: { + tags: ['Customers'], + summary: 'List customers', + parameters: [ + { name: 'q', in: 'query', schema: { type: 'string' } }, + { name: 'flagged', in: 'query', schema: { type: 'string', enum: ['true', 'false'] } }, + ...pagingParams, + ], + responses: { '200': ok }, + }, + post: { + tags: ['Customers'], + summary: 'Create customer', + requestBody: jsonBody('#/components/schemas/CustomerInput'), + responses: { '201': ok, '400': err4 }, + }, + }, + '/customers/{id}': { + get: { + tags: ['Customers'], + summary: 'Get customer', + parameters: [idPath()], + responses: { '200': ok, '404': err404 }, + }, + patch: { + tags: ['Customers'], + summary: 'Update customer', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/CustomerInput'), + responses: { '200': ok }, + }, + }, + '/customers/{id}/flag': { + post: { + tags: ['Customers'], + summary: 'Flag customer', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/FlagCustomer'), + responses: { '200': ok }, + }, + delete: { + tags: ['Customers'], + summary: 'Unflag customer', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/customers/{id}/validate-license': { + post: { + tags: ['Customers'], + summary: 'Validate customer driving license', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/customers/{id}/license-image': { + post: { + tags: ['Customers'], + summary: 'Upload license image', + parameters: [idPath()], + requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } } } }, + responses: { '200': ok }, + }, + }, + '/customers/{id}/approve-license': { + post: { + tags: ['Customers'], + summary: 'Approve or deny license', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ApproveLicense'), + responses: { '200': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // OFFERS + // ════════════════════════════════════════════════════════════════ + '/offers': { + get: { + tags: ['Offers'], + summary: 'List offers', + parameters: [ + { name: 'active', in: 'query', schema: { type: 'string', enum: ['true', 'false'] } }, + { name: 'public', in: 'query', schema: { type: 'string', enum: ['true', 'false'] } }, + ], + responses: { '200': ok }, + }, + post: { + tags: ['Offers'], + summary: 'Create offer', + requestBody: jsonBody('#/components/schemas/OfferInput'), + responses: { '201': ok, '400': err4 }, + }, + }, + '/offers/{id}': { + get: { + tags: ['Offers'], + summary: 'Get offer', + parameters: [idPath()], + responses: { '200': ok, '404': err404 }, + }, + patch: { + tags: ['Offers'], + summary: 'Update offer', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/OfferInput'), + responses: { '200': ok }, + }, + delete: { + tags: ['Offers'], + summary: 'Delete offer', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/offers/{id}/activate': { + post: { tags: ['Offers'], summary: 'Activate offer', parameters: [idPath()], responses: { '200': ok } }, + }, + '/offers/{id}/deactivate': { + post: { tags: ['Offers'], summary: 'Deactivate offer', parameters: [idPath()], responses: { '200': ok } }, + }, + '/offers/{id}/stats': { + get: { tags: ['Offers'], summary: 'Offer usage stats', parameters: [idPath()], responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // PAYMENTS + // ════════════════════════════════════════════════════════════════ + '/payments/company': { + get: { + tags: ['Payments'], + summary: 'List all company payments', + responses: { '200': ok }, + }, + }, + '/payments/reservations/{id}': { + get: { + tags: ['Payments'], + summary: 'Get payments for a reservation', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/payments/reservations/{id}/charge': { + post: { + tags: ['Payments'], + summary: 'Initiate online payment (AmanPay / PayPal)', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/PaymentCharge'), + responses: { '200': ok, '400': err4 }, + }, + }, + '/payments/reservations/{id}/capture-paypal': { + post: { + tags: ['Payments'], + summary: 'Capture PayPal order', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + '/payments/reservations/{id}/manual': { + post: { + tags: ['Payments'], + summary: 'Record manual payment', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ManualPayment'), + responses: { '201': ok, '400': err4 }, + }, + }, + '/payments/reservations/{reservationId}/payments/{paymentId}/refund': { + post: { + tags: ['Payments'], + summary: 'Refund a payment', + parameters: [idPath('reservationId'), idPath('paymentId')], + requestBody: jsonBody('#/components/schemas/Refund'), + responses: { '200': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // REVIEWS + // ════════════════════════════════════════════════════════════════ + '/reviews': { + get: { + tags: ['Reviews'], + summary: 'List reviews', + parameters: [ + { name: 'rating', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 5 } }, + ...pagingParams, + ], + responses: { '200': ok }, + }, + }, + '/reviews/stats': { + get: { tags: ['Reviews'], summary: 'Review statistics', responses: { '200': ok } }, + }, + '/reviews/{id}': { + get: { tags: ['Reviews'], summary: 'Get review', parameters: [idPath()], responses: { '200': ok, '404': err404 } }, + }, + '/reviews/{id}/reply': { + patch: { + tags: ['Reviews'], + summary: 'Reply to review', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ReviewReply'), + responses: { '200': ok }, + }, + }, + '/reviews/{id}/remind': { + post: { tags: ['Reviews'], summary: 'Send review reminder', parameters: [idPath()], responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // COMPLAINTS + // ════════════════════════════════════════════════════════════════ + '/complaints': { + get: { + tags: ['Complaints'], + summary: 'List complaints', + parameters: [ + { name: 'status', in: 'query', schema: { type: 'string' } }, + { name: 'severity', in: 'query', schema: { type: 'string' } }, + { name: 'category', in: 'query', schema: { type: 'string' } }, + ...pagingParams, + ], + responses: { '200': ok }, + }, + post: { + tags: ['Complaints'], + summary: 'Create complaint', + requestBody: jsonBody('#/components/schemas/ComplaintCreate'), + responses: { '201': ok, '400': err4 }, + }, + }, + '/complaints/{id}': { + get: { + tags: ['Complaints'], + summary: 'Get complaint', + parameters: [idPath()], + responses: { '200': ok, '404': err404 }, + }, + patch: { + tags: ['Complaints'], + summary: 'Update complaint', + parameters: [idPath()], + requestBody: jsonBody('#/components/schemas/ComplaintUpdate'), + responses: { '200': ok }, + }, + delete: { + tags: ['Complaints'], + summary: 'Delete complaint', + parameters: [idPath()], + responses: { '200': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // ANALYTICS + // ════════════════════════════════════════════════════════════════ + '/analytics/summary': { + get: { tags: ['Analytics'], summary: 'Quick summary stats', responses: { '200': ok } }, + }, + '/analytics/dashboard': { + get: { tags: ['Analytics'], summary: 'Full dashboard analytics', responses: { '200': ok } }, + }, + '/analytics/sources': { + get: { tags: ['Analytics'], summary: 'Reservation source breakdown', responses: { '200': ok } }, + }, + '/analytics/report': { + get: { + tags: ['Analytics'], + summary: 'Detailed report (Manager+)', + parameters: [ + { name: 'startDate', in: 'query', schema: { type: 'string', format: 'date' } }, + { name: 'endDate', in: 'query', schema: { type: 'string', format: 'date' } }, + ], + responses: { '200': ok }, + }, + }, + + // ════════════════════════════════════════════════════════════════ + // NOTIFICATIONS + // ════════════════════════════════════════════════════════════════ + '/notifications/company': { + get: { tags: ['Notifications'], summary: 'Company notifications', responses: { '200': ok } }, + }, + '/notifications/unread-count': { + get: { tags: ['Notifications'], summary: 'Unread count', responses: { '200': ok } }, + }, + '/notifications/company/{id}/read': { + post: { tags: ['Notifications'], summary: 'Mark notification as read', parameters: [idPath()], responses: { '200': ok } }, + }, + '/notifications/company/read-all': { + post: { tags: ['Notifications'], summary: 'Mark all notifications as read', responses: { '200': ok } }, + }, + '/notifications/company/preferences': { + get: { tags: ['Notifications'], summary: 'Get notification preferences', responses: { '200': ok } }, + patch: { tags: ['Notifications'], summary: 'Update notification preferences', responses: { '200': ok } }, + }, + '/notifications/history': { + get: { tags: ['Notifications'], summary: 'Notification history', responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // COMPANIES + // ════════════════════════════════════════════════════════════════ + '/companies/me': { + get: { tags: ['Companies'], summary: 'Current company profile', responses: { '200': ok } }, + patch: { tags: ['Companies'], summary: 'Update company profile (Owner)', responses: { '200': ok } }, + }, + '/companies/me/brand': { + get: { tags: ['Companies'], summary: 'Brand settings', responses: { '200': ok } }, + patch: { tags: ['Companies'], summary: 'Update brand settings (Owner)', responses: { '200': ok } }, + }, + '/companies/me/brand/logo': { + post: { tags: ['Companies'], summary: 'Upload brand logo', requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } } } }, responses: { '200': ok } }, + }, + '/companies/me/brand/hero': { + post: { tags: ['Companies'], summary: 'Upload brand hero image', requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } } } }, responses: { '200': ok } }, + }, + '/companies/me/brand/subdomain/check': { + post: { tags: ['Companies'], summary: 'Check subdomain availability', responses: { '200': ok } }, + }, + '/companies/me/brand/custom-domain': { + post: { tags: ['Companies'], summary: 'Set custom domain', responses: { '200': ok } }, + delete: { tags: ['Companies'], summary: 'Remove custom domain', responses: { '200': ok } }, + }, + '/companies/me/brand/custom-domain/status': { + get: { tags: ['Companies'], summary: 'Custom domain DNS status', responses: { '200': ok } }, + }, + '/companies/me/contract-settings': { + get: { tags: ['Companies'], summary: 'Contract settings', responses: { '200': ok } }, + patch: { tags: ['Companies'], summary: 'Update contract settings', responses: { '200': ok } }, + }, + '/companies/me/insurance-policies': { + get: { tags: ['Companies'], summary: 'List insurance policies', responses: { '200': ok } }, + post: { tags: ['Companies'], summary: 'Create insurance policy', responses: { '201': ok } }, + }, + '/companies/me/insurance-policies/{id}': { + patch: { tags: ['Companies'], summary: 'Update insurance policy', parameters: [idPath()], responses: { '200': ok } }, + delete: { tags: ['Companies'], summary: 'Delete insurance policy', parameters: [idPath()], responses: { '200': ok } }, + }, + '/companies/me/pricing-rules': { + get: { tags: ['Companies'], summary: 'List pricing rules', responses: { '200': ok } }, + post: { tags: ['Companies'], summary: 'Create pricing rule', responses: { '201': ok } }, + }, + '/companies/me/pricing-rules/{id}': { + patch: { tags: ['Companies'], summary: 'Update pricing rule', parameters: [idPath()], responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // TEAM + // ════════════════════════════════════════════════════════════════ + '/team': { + get: { tags: ['Team'], summary: 'List team members', responses: { '200': ok } }, + }, + '/team/stats': { + get: { tags: ['Team'], summary: 'Team statistics', responses: { '200': ok } }, + }, + '/team/invite': { + post: { tags: ['Team'], summary: 'Invite member (Owner)', responses: { '201': ok } }, + }, + '/team/{id}/role': { + patch: { tags: ['Team'], summary: 'Update member role (Owner)', parameters: [idPath()], responses: { '200': ok } }, + }, + '/team/{id}/deactivate': { + post: { tags: ['Team'], summary: 'Deactivate member (Owner)', parameters: [idPath()], responses: { '200': ok } }, + }, + '/team/{id}/reactivate': { + post: { tags: ['Team'], summary: 'Reactivate member (Owner)', parameters: [idPath()], responses: { '200': ok } }, + }, + '/team/{id}': { + delete: { tags: ['Team'], summary: 'Remove member (Owner)', parameters: [idPath()], responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // SUBSCRIPTIONS + // ════════════════════════════════════════════════════════════════ + '/subscriptions/plans': { + get: { tags: ['Subscriptions'], summary: 'List available plans', security: [], responses: { '200': ok } }, + }, + '/subscriptions/features': { + get: { tags: ['Subscriptions'], summary: 'Plan feature matrix', security: [], responses: { '200': ok } }, + }, + '/subscriptions/me': { + get: { tags: ['Subscriptions'], summary: 'Current subscription', responses: { '200': ok } }, + }, + '/subscriptions/invoices': { + get: { tags: ['Subscriptions'], summary: 'Billing invoices', responses: { '200': ok } }, + }, + '/subscriptions/events': { + get: { tags: ['Subscriptions'], summary: 'Subscription events', responses: { '200': ok } }, + }, + '/subscriptions/entitlement': { + get: { tags: ['Subscriptions'], summary: 'Feature entitlements', responses: { '200': ok } }, + }, + '/subscriptions/trial': { + post: { tags: ['Subscriptions'], summary: 'Start trial (Owner)', responses: { '200': ok } }, + }, + '/subscriptions/checkout': { + post: { tags: ['Subscriptions'], summary: 'Checkout new subscription (Owner)', responses: { '200': ok } }, + }, + '/subscriptions/change-plan': { + post: { tags: ['Subscriptions'], summary: 'Change plan (Owner)', responses: { '200': ok } }, + }, + '/subscriptions/cancel': { + post: { tags: ['Subscriptions'], summary: 'Cancel subscription (Owner)', responses: { '200': ok } }, + }, + '/subscriptions/reactivate': { + post: { tags: ['Subscriptions'], summary: 'Reactivate subscription (Owner)', responses: { '200': ok } }, + }, + '/subscriptions/resume': { + post: { tags: ['Subscriptions'], summary: 'Resume subscription (Owner)', responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // MARKETPLACE (public — no auth required) + // ════════════════════════════════════════════════════════════════ + '/marketplace/cities': { + get: { tags: ['Marketplace'], summary: 'Cities with active companies', security: [], responses: { '200': ok } }, + }, + '/marketplace/companies': { + get: { tags: ['Marketplace'], summary: 'List companies', security: [], responses: { '200': ok } }, + }, + '/marketplace/offers': { + get: { tags: ['Marketplace'], summary: 'Public offers', security: [], responses: { '200': ok } }, + }, + '/marketplace/search': { + get: { + tags: ['Marketplace'], + summary: 'Search available vehicles', + security: [], + parameters: [ + { name: 'city', in: 'query', schema: { type: 'string' } }, + { name: 'startDate', in: 'query', schema: { type: 'string', format: 'date-time' } }, + { name: 'endDate', in: 'query', schema: { type: 'string', format: 'date-time' } }, + { name: 'category', in: 'query', schema: { type: 'string' } }, + ], + responses: { '200': ok }, + }, + }, + '/marketplace/reservations': { + post: { tags: ['Marketplace'], summary: 'Create marketplace booking', security: [], responses: { '201': ok } }, + }, + '/marketplace/offers/{code}/validate': { + post: { tags: ['Marketplace'], summary: 'Validate promo code', security: [], parameters: [{ name: 'code', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/marketplace/review/{token}': { + get: { tags: ['Marketplace'], summary: 'Get review form by token', security: [], parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + post: { tags: ['Marketplace'], summary: 'Submit review', security: [], parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/marketplace/{slug}': { + get: { tags: ['Marketplace'], summary: 'Company public page', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/marketplace/{slug}/reviews': { + get: { tags: ['Marketplace'], summary: 'Company reviews', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/marketplace/{slug}/vehicles': { + get: { tags: ['Marketplace'], summary: 'Company vehicles', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/marketplace/{slug}/vehicles/{id}': { + get: { tags: ['Marketplace'], summary: 'Vehicle details', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } }, + }, + '/marketplace/{slug}/offers': { + get: { tags: ['Marketplace'], summary: 'Company offers', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // SITE (white-label) + // ════════════════════════════════════════════════════════════════ + '/site/platform/homepage': { + get: { tags: ['Site'], summary: 'Platform homepage content', security: [], responses: { '200': ok } }, + }, + '/site/platform/pricing': { + get: { tags: ['Site'], summary: 'Platform pricing page content', security: [], responses: { '200': ok } }, + }, + '/site/{slug}/brand': { + get: { tags: ['Site'], summary: 'Brand config for white-label site', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/site/{slug}/vehicles': { + get: { tags: ['Site'], summary: 'Vehicles for white-label site', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/site/{slug}/vehicles/{id}': { + get: { tags: ['Site'], summary: 'Vehicle detail', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } }, + }, + '/site/{slug}/offers': { + get: { tags: ['Site'], summary: 'Offers for white-label site', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/site/{slug}/booking-options': { + get: { tags: ['Site'], summary: 'Booking options', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/site/{slug}/availability': { + post: { tags: ['Site'], summary: 'Check availability', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/site/{slug}/book/validate-code': { + post: { tags: ['Site'], summary: 'Validate promo code', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + '/site/{slug}/book': { + post: { tags: ['Site'], summary: 'Create booking', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '201': ok } }, + }, + '/site/{slug}/booking/{id}': { + get: { tags: ['Site'], summary: 'Get booking status', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } }, + }, + '/site/{slug}/booking/{id}/pay': { + post: { tags: ['Site'], summary: 'Pay booking', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } }, + }, + '/site/{slug}/contact': { + post: { tags: ['Site'], summary: 'Contact form', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } }, + }, + + // ════════════════════════════════════════════════════════════════ + // ADMIN + // ════════════════════════════════════════════════════════════════ + '/admin/auth/login': { + post: { tags: ['Admin'], summary: 'Admin login', security: [], responses: { '200': ok } }, + }, + '/admin/auth/forgot-password': { + post: { tags: ['Admin'], summary: 'Admin forgot password', security: [], responses: { '200': ok } }, + }, + '/admin/auth/reset-password': { + post: { tags: ['Admin'], summary: 'Admin reset password', security: [], responses: { '200': ok } }, + }, + '/admin/auth/me': { + get: { tags: ['Admin'], summary: 'Current admin profile', responses: { '200': ok } }, + }, + '/admin/auth/2fa/setup': { + post: { tags: ['Admin'], summary: 'Setup 2FA', responses: { '200': ok } }, + }, + '/admin/auth/2fa/verify': { + post: { tags: ['Admin'], summary: 'Verify 2FA code', responses: { '200': ok } }, + }, + '/admin/companies': { + get: { tags: ['Admin'], summary: 'List all companies', responses: { '200': ok } }, + }, + '/admin/companies/{id}': { + get: { tags: ['Admin'], summary: 'Get company', parameters: [idPath()], responses: { '200': ok } }, + patch: { tags: ['Admin'], summary: 'Update company', parameters: [idPath()], responses: { '200': ok } }, + delete: { tags: ['Admin'], summary: 'Delete company (Admin)', parameters: [idPath()], responses: { '200': ok } }, + }, + '/admin/companies/{id}/status': { + patch: { tags: ['Admin'], summary: 'Update company status', parameters: [idPath()], responses: { '200': ok } }, + }, + '/admin/companies/{id}/impersonate': { + post: { tags: ['Admin'], summary: 'Impersonate company (Admin)', parameters: [idPath()], responses: { '200': ok } }, + }, + '/admin/renters': { + get: { tags: ['Admin'], summary: 'List renters', responses: { '200': ok } }, + }, + '/admin/renters/{id}/block': { + post: { tags: ['Admin'], summary: 'Block renter', parameters: [idPath()], responses: { '200': ok } }, + }, + '/admin/renters/{id}/unblock': { + post: { tags: ['Admin'], summary: 'Unblock renter', parameters: [idPath()], responses: { '200': ok } }, + }, + '/admin/metrics': { + get: { tags: ['Admin'], summary: 'Platform metrics (Finance)', responses: { '200': ok } }, + }, + '/admin/notifications': { + get: { tags: ['Admin'], summary: 'Admin notifications', responses: { '200': ok } }, + }, + '/admin/audit-logs': { + get: { tags: ['Admin'], summary: 'Audit logs (Admin)', responses: { '200': ok } }, + }, + '/admin/admins': { + get: { tags: ['Admin'], summary: 'List admins (Super Admin)', responses: { '200': ok } }, + post: { tags: ['Admin'], summary: 'Create admin (Super Admin)', responses: { '201': ok } }, + }, + }, +} diff --git a/apps/api/src/tests/integration/health.test.ts b/apps/api/src/tests/integration/health.test.ts index 9642048..4bc8a42 100644 --- a/apps/api/src/tests/integration/health.test.ts +++ b/apps/api/src/tests/integration/health.test.ts @@ -17,5 +17,14 @@ describe('GET /health', () => { expect(res.status).toBe(200) expect(res.body.name).toBe('rentaldrivego-api') expect(Array.isArray(res.body.routes)).toBe(true) + expect(res.body.swaggerUi).toBe('/docs') + expect(res.body.openApi).toBe('/api/v1/openapi.json') + }) + + it('GET /api/v1/openapi.json returns the OpenAPI document', async () => { + const res = await request(app).get('/api/v1/openapi.json') + expect(res.status).toBe(200) + expect(res.body.openapi).toBe('3.0.3') + expect(res.body.info?.title).toBe('RentalDriveGo API') }) }) diff --git a/package-lock.json b/package-lock.json index 76431b6..a718ee0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,8 +72,10 @@ "react-dom": "^18.3.1", "resend": "^3.2.0", "socket.io": "^4.7.5", + "swagger-ui-express": "^5.0.1", "twilio": "^5.1.0", - "zod": "^3.23.0" + "zod": "^3.23.0", + "zod-to-json-schema": "^3.25.2" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", @@ -89,6 +91,7 @@ "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@types/supertest": "^6.0.2", + "@types/swagger-ui-express": "^4.1.8", "supertest": "^7.0.0", "ts-node-dev": "^2.0.0", "typescript": "^5.4.0", @@ -2692,6 +2695,13 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -3167,6 +3177,17 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -9495,6 +9516,30 @@ "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", "license": "ISC" }, + "node_modules/swagger-ui-dist": { + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -10688,6 +10733,15 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "packages/database": { "name": "@rentaldrivego/database", "version": "1.0.0",