Files
carmanagement/apps/api/src/index.ts
T
root 8fc88ffc14
Build & Push / Pipeline Tests (push) Failing after 59s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 51s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
fix production issues
2026-08-12 16:48:41 -04:00

131 lines
3.7 KiB
TypeScript

import http from 'http'
import { Server as SocketIOServer } from 'socket.io'
import type { Socket } from 'socket.io'
import { redis } from './lib/redis'
import { prisma } from './lib/prisma'
import { assertStorageConfiguration } from './lib/storage'
import { createApp, corsOrigins } from './app'
import { verifyAnyActorToken } from './security/tokens'
import { getSessionCookieName } from './security/sessionCookies'
import { startOutboxWorker, startScheduledJobs } from './workers/jobs'
const app = createApp()
const server = http.createServer(app)
assertStorageConfiguration()
const io = new SocketIOServer(server, {
cors: { origin: corsOrigins, credentials: true, methods: ['GET', 'POST'] },
})
function readCookieFromHeader(cookieHeader: string | undefined, name: string): string | null {
if (!cookieHeader) return null
for (const chunk of cookieHeader.split(';')) {
const [rawName, ...rawValue] = chunk.trim().split('=')
if (rawName === name) return decodeURIComponent(rawValue.join('='))
}
return null
}
function getSocketSessionToken(socket: Socket): string | undefined {
const explicitToken = socket.handshake.auth?.token
if (typeof explicitToken === 'string' && explicitToken.trim()) return explicitToken.trim()
const cookieHeader = socket.request.headers.cookie
return (
readCookieFromHeader(cookieHeader, getSessionCookieName('employee')) ??
readCookieFromHeader(cookieHeader, getSessionCookieName('admin')) ??
readCookieFromHeader(cookieHeader, getSessionCookieName('renter')) ??
undefined
)
}
io.use((socket, next) => {
const token = getSocketSessionToken(socket)
if (!token) return next()
try {
const payload = verifyAnyActorToken(token)
;(socket as any).authenticatedUserId = payload.sub
next()
} catch {
next(new Error('invalid_token'))
}
})
io.on('connection', (socket) => {
const userId = (socket as any).authenticatedUserId as string | undefined
if (userId) socket.join(`user:${userId}`)
})
const subscriber = redis.duplicate()
subscriber.psubscribe('notifications:*', (err) => {
if (err) console.error('[Redis] Subscribe error:', err)
})
subscriber.on('pmessage', (_pattern, channel, message) => {
try {
const parsed = JSON.parse(message)
const userId = channel.replace('notifications:', '')
io.to(`user:${userId}`).emit('notification', parsed)
} catch (err) {
console.error('[Redis] Invalid notification message:', err)
}
})
// Embedded jobs only when explicitly enabled (single-process local/dev).
// Production should run `npm run worker` / Compose `api-worker` instead.
if (process.env.ENABLE_EMBEDDED_JOBS === 'true') {
console.warn('[API] ENABLE_EMBEDDED_JOBS=true — running outbox/cron inside the API process')
startOutboxWorker()
startScheduledJobs()
}
const PORT = Number(process.env.API_PORT ?? 4000)
const HOST = process.env.API_HOST ?? '0.0.0.0'
server.listen(PORT, HOST, () => {
console.log(`[API] Server running on ${HOST}:${PORT}`)
})
let shuttingDown = false
async function shutdown(signal: string) {
if (shuttingDown) return
shuttingDown = true
console.log(`[API] ${signal} received, draining`)
server.close((err) => {
if (err) console.error('[API] HTTP close error:', err.message)
})
try {
io.close()
} catch (err: any) {
console.error('[API] Socket.IO close error:', err?.message ?? err)
}
try {
await subscriber.quit()
} catch {
subscriber.disconnect()
}
try {
await redis.quit()
} catch {
redis.disconnect()
}
try {
await prisma.$disconnect()
} catch (err: any) {
console.error('[API] Prisma disconnect error:', err?.message ?? err)
}
process.exit(0)
}
process.on('SIGTERM', () => void shutdown('SIGTERM'))
process.on('SIGINT', () => void shutdown('SIGINT'))
export { app, io }