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
131 lines
3.7 KiB
TypeScript
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 }
|