refractor code,
This commit is contained in:
+4
-194
@@ -1,7 +1,3 @@
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
import morgan from 'morgan'
|
||||
import http from 'http'
|
||||
import { Server as SocketIOServer } from 'socket.io'
|
||||
import cron from 'node-cron'
|
||||
@@ -9,72 +5,12 @@ import jwt from 'jsonwebtoken'
|
||||
import { z } from 'zod'
|
||||
import { redis } from './lib/redis'
|
||||
import { prisma } from './lib/prisma'
|
||||
import { getStorageRoot } from './lib/storage'
|
||||
import { authLimiter, apiLimiter, publicLimiter, adminLimiter } from './middleware/rateLimiter'
|
||||
import { assertStorageConfiguration } from './lib/storage'
|
||||
import { createApp, corsOrigins } from './app'
|
||||
|
||||
// ─── Routes ───────────────────────────────────────────────────
|
||||
import webhookRouter from './routes/webhooks'
|
||||
import companyAuthRouter from './routes/auth.company'
|
||||
import employeeAuthRouter from './routes/auth.employee'
|
||||
import renterAuthRouter from './routes/auth.renter'
|
||||
import vehiclesRouter from './routes/vehicles'
|
||||
import reservationsRouter from './routes/reservations'
|
||||
import teamRouter from './routes/team'
|
||||
import customersRouter from './routes/customers'
|
||||
import offersRouter from './routes/offers'
|
||||
import analyticsRouter from './routes/analytics'
|
||||
import notificationsRouter from './routes/notifications'
|
||||
import marketplaceRouter from './routes/marketplace'
|
||||
import adminRouter from './routes/admin'
|
||||
import companiesRouter from './routes/companies'
|
||||
import subscriptionsRouter from './routes/subscriptions'
|
||||
import siteRouter from './routes/site'
|
||||
import paymentsRouter from './routes/payments'
|
||||
|
||||
const app = express()
|
||||
const app = createApp()
|
||||
const server = http.createServer(app)
|
||||
|
||||
// Trust the first hop from a reverse proxy so req.ip and rate-limiting
|
||||
// use the real client IP (from X-Forwarded-For) rather than the proxy's IP.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.set('trust proxy', 1)
|
||||
}
|
||||
const v1 = '/api/v1'
|
||||
const defaultCorsOrigins = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3001',
|
||||
'http://localhost:3002',
|
||||
'http://localhost:3003',
|
||||
'http://127.0.0.1:3000',
|
||||
'http://127.0.0.1:3001',
|
||||
'http://127.0.0.1:3002',
|
||||
'http://127.0.0.1:3003',
|
||||
]
|
||||
const corsOrigins = process.env.CORS_ORIGINS
|
||||
? process.env.CORS_ORIGINS.split(',').map((origin) => origin.trim()).filter(Boolean)
|
||||
: defaultCorsOrigins
|
||||
|
||||
const routeDocs = [
|
||||
{ method: 'GET', path: '/health', description: 'Health check' },
|
||||
{ method: 'GET', path: `${v1}/docs`, description: 'Machine-readable API index' },
|
||||
{ method: 'GET', path: `${v1}/auth/renter/me`, description: 'Current renter profile' },
|
||||
{ method: 'GET', path: `${v1}/vehicles`, description: 'List company vehicles' },
|
||||
{ method: 'POST', path: `${v1}/vehicles`, description: 'Create vehicle' },
|
||||
{ method: 'GET', path: `${v1}/reservations`, description: 'List reservations' },
|
||||
{ method: 'POST', path: `${v1}/reservations`, description: 'Create reservation' },
|
||||
{ method: 'GET', path: `${v1}/customers`, description: 'List customers' },
|
||||
{ method: 'POST', path: `${v1}/customers`, description: 'Create customer' },
|
||||
{ method: 'GET', path: `${v1}/offers`, description: 'List offers' },
|
||||
{ method: 'GET', path: `${v1}/analytics/dashboard`, description: 'Dashboard analytics' },
|
||||
{ method: 'GET', path: `${v1}/analytics/report`, description: 'Analytics report' },
|
||||
{ method: 'GET', path: `${v1}/notifications/company`, description: 'Company notifications' },
|
||||
{ method: 'GET', path: `${v1}/marketplace/cities`, description: 'Marketplace cities' },
|
||||
{ method: 'GET', path: `${v1}/marketplace/search`, description: 'Marketplace search' },
|
||||
{ method: 'GET', path: `${v1}/site/:slug/brand`, description: 'Public site brand config' },
|
||||
{ method: 'GET', path: `${v1}/companies/me`, description: 'Current company profile' },
|
||||
{ method: 'GET', path: `${v1}/subscriptions/plans`, description: 'Subscription plans' },
|
||||
{ method: 'POST', path: `${v1}/admin/auth/login`, description: 'Admin login' },
|
||||
]
|
||||
assertStorageConfiguration()
|
||||
|
||||
// ─── Socket.io ────────────────────────────────────────────────
|
||||
const io = new SocketIOServer(server, {
|
||||
@@ -122,132 +58,6 @@ subscriber.on('pmessage', (_pattern, channel, message) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ─── Middleware ────────────────────────────────────────────────
|
||||
// Serve uploaded assets from the configured storage root.
|
||||
app.use('/storage', express.static(getStorageRoot()))
|
||||
|
||||
// Webhook must use raw body BEFORE express.json()
|
||||
app.use('/api/v1/webhooks', express.raw({ type: 'application/json' }), webhookRouter)
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({ origin: corsOrigins, credentials: true }))
|
||||
app.use(morgan('combined'))
|
||||
app.use(express.json({ limit: '10mb' }))
|
||||
|
||||
// ─── API Routes ───────────────────────────────────────────────
|
||||
// Auth routes: strict brute-force protection
|
||||
app.use(`${v1}/auth/renter`, authLimiter, renterAuthRouter)
|
||||
app.use(`${v1}/auth/company`, authLimiter, companyAuthRouter)
|
||||
app.use(`${v1}/auth/employee`, authLimiter, employeeAuthRouter)
|
||||
|
||||
// Admin routes: dedicated limit
|
||||
app.use(`${v1}/admin`, adminLimiter, adminRouter)
|
||||
|
||||
// Public unauthenticated routes
|
||||
app.use(`${v1}/marketplace`, publicLimiter, marketplaceRouter)
|
||||
app.use(`${v1}/site`, publicLimiter, siteRouter)
|
||||
|
||||
// Authenticated company/renter routes
|
||||
app.use(`${v1}/vehicles`, apiLimiter, vehiclesRouter)
|
||||
app.use(`${v1}/reservations`, apiLimiter, reservationsRouter)
|
||||
app.use(`${v1}/team`, apiLimiter, teamRouter)
|
||||
app.use(`${v1}/customers`, apiLimiter, customersRouter)
|
||||
app.use(`${v1}/offers`, apiLimiter, offersRouter)
|
||||
app.use(`${v1}/analytics`, apiLimiter, analyticsRouter)
|
||||
app.use(`${v1}/notifications`, apiLimiter, notificationsRouter)
|
||||
app.use(`${v1}/companies`, apiLimiter, companiesRouter)
|
||||
app.use(`${v1}/subscriptions`, apiLimiter, subscriptionsRouter)
|
||||
app.use(`${v1}/payments`, apiLimiter, paymentsRouter)
|
||||
|
||||
// ─── Health check ─────────────────────────────────────────────
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', version: '1.0.0', timestamp: new Date().toISOString() })
|
||||
})
|
||||
|
||||
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(
|
||||
(route) => `
|
||||
<tr>
|
||||
<td>${route.method}</td>
|
||||
<td><code>${route.path}</code></td>
|
||||
<td>${route.description}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('')
|
||||
|
||||
res.type('html').send(`<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>RentalDriveGo API Docs</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; color: #0f172a; background: #f8fafc; }
|
||||
h1 { margin-bottom: 8px; }
|
||||
p { color: #475569; }
|
||||
.meta { margin-bottom: 24px; }
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 12px; overflow: hidden; }
|
||||
th, td { text-align: left; padding: 12px 16px; border-bottom: 1px solid #e2e8f0; }
|
||||
th { background: #e2e8f0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
code { background: #f1f5f9; padding: 2px 6px; border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>RentalDriveGo API</h1>
|
||||
<p class="meta">Base URL: <code>${v1}</code> · JSON index: <code>${v1}/docs</code></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Path</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
})
|
||||
|
||||
// ─── Error handler ────────────────────────────────────────────
|
||||
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
const statusCode = err.statusCode ?? 500
|
||||
const message = err.message ?? 'Internal server error'
|
||||
const code = err.code ?? 'internal_error'
|
||||
|
||||
if (statusCode >= 500) {
|
||||
console.error('[API Error]', err)
|
||||
}
|
||||
|
||||
// Zod validation error
|
||||
if (err.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'validation_error', message: 'Invalid request body', issues: err.issues, statusCode: 400 })
|
||||
}
|
||||
|
||||
// Prisma not found
|
||||
if (err.code === 'P2025') {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Resource not found', statusCode: 404 })
|
||||
}
|
||||
|
||||
// Prisma unique constraint
|
||||
if (err.code === 'P2002') {
|
||||
return res.status(409).json({ error: 'conflict', message: 'A resource with this value already exists', statusCode: 409 })
|
||||
}
|
||||
|
||||
res.status(statusCode).json({ error: code, message, statusCode })
|
||||
})
|
||||
|
||||
// ─── Scheduled jobs ───────────────────────────────────────────
|
||||
|
||||
// Daily: flag expiring/expired licenses
|
||||
|
||||
Reference in New Issue
Block a user