167 lines
4.3 KiB
TypeScript
167 lines
4.3 KiB
TypeScript
import { Hono } from 'hono'
|
|
import { cors } from 'hono/cors'
|
|
import { logger } from 'hono/logger'
|
|
import { serve } from 'bun'
|
|
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
|
import { appRouter } from '@/src/trpc/router'
|
|
import { verifyToken, UserJWTPayload, StaffJWTPayload } from '@/src/lib/jwt-utils'
|
|
import { db } from '@/src/db/db_index'
|
|
import { staffUsers, userDetails } from '@/src/db/schema'
|
|
import { eq } from 'drizzle-orm'
|
|
import { TRPCError } from '@trpc/server'
|
|
import signedUrlCache from '@/src/lib/signed-url-cache'
|
|
import { seed } from '@/src/db/seed'
|
|
import initFunc from '@/src/lib/init'
|
|
import '@/src/jobs/jobs-index'
|
|
import { startAutomatedJobs } from '@/src/lib/automatedJobs'
|
|
|
|
// Initialize
|
|
seed()
|
|
initFunc()
|
|
startAutomatedJobs()
|
|
|
|
const app = new Hono()
|
|
|
|
// CORS middleware
|
|
app.use('*', cors({
|
|
origin: ['http://localhost:5174', 'http://localhost:5173'],
|
|
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
allowHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization'],
|
|
credentials: true
|
|
}))
|
|
|
|
// Request logging
|
|
app.use('*', logger())
|
|
|
|
// Health check
|
|
app.get('/health', (c) => {
|
|
return c.json({
|
|
status: 'OK',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
message: 'Hello world'
|
|
})
|
|
})
|
|
|
|
// tRPC handler with context
|
|
app.use('/api/trpc/*', async (c) => {
|
|
const response = await fetchRequestHandler({
|
|
endpoint: '/api/trpc',
|
|
req: c.req.raw,
|
|
router: appRouter,
|
|
createContext: async ({ req }) => {
|
|
let user = null
|
|
let staffUser = null
|
|
const authHeader = req.headers.get('authorization')
|
|
|
|
if (authHeader?.startsWith('Bearer ')) {
|
|
const token = authHeader.substring(7)
|
|
try {
|
|
const decoded = await verifyToken(token)
|
|
|
|
if ('staffId' in decoded) {
|
|
const staffPayload = decoded as StaffJWTPayload
|
|
const staff = await db.query.staffUsers.findFirst({
|
|
where: eq(staffUsers.id, staffPayload.staffId)
|
|
})
|
|
if (staff) {
|
|
staffUser = { id: staff.id, name: staff.name }
|
|
}
|
|
} else {
|
|
const userPayload = decoded as UserJWTPayload
|
|
user = {
|
|
userId: userPayload.userId,
|
|
name: userPayload.name,
|
|
email: userPayload.email,
|
|
mobile: userPayload.mobile
|
|
}
|
|
|
|
const details = await db.query.userDetails.findFirst({
|
|
where: eq(userDetails.userId, userPayload.userId)
|
|
})
|
|
|
|
if (details?.isSuspended) {
|
|
throw new TRPCError({
|
|
code: 'FORBIDDEN',
|
|
message: 'Account suspended'
|
|
})
|
|
}
|
|
}
|
|
} catch {
|
|
// Invalid token
|
|
}
|
|
}
|
|
|
|
return { req, res: c.res, user, staffUser }
|
|
},
|
|
onError: ({ error, path, ctx }) => {
|
|
console.error('🚨 tRPC Error:', {
|
|
path,
|
|
code: error.code,
|
|
message: error.message,
|
|
userId: ctx?.user?.userId
|
|
})
|
|
}
|
|
})
|
|
|
|
return response
|
|
})
|
|
|
|
// Static files - Fallback UI
|
|
app.use('/*', async (c) => {
|
|
const url = new URL(c.req.url)
|
|
let filePath = url.pathname
|
|
|
|
// Default to index.html for root
|
|
if (filePath === '/') {
|
|
filePath = '/index.html'
|
|
}
|
|
|
|
// Try to serve the file
|
|
const file = Bun.file(`./fallback-ui/dist${filePath}`)
|
|
if (await file.exists()) {
|
|
return new Response(file)
|
|
}
|
|
|
|
// SPA fallback - serve index.html for any unmatched routes
|
|
const indexFile = Bun.file('./fallback-ui/dist/index.html')
|
|
if (await indexFile.exists()) {
|
|
return new Response(indexFile)
|
|
}
|
|
|
|
return c.notFound()
|
|
})
|
|
|
|
// Static files - Assets
|
|
app.use('/assets/*', async (c) => {
|
|
const path = c.req.path.replace('/assets/', '')
|
|
const file = Bun.file(`./assets/public/${path}`)
|
|
|
|
if (await file.exists()) {
|
|
return new Response(file)
|
|
}
|
|
|
|
return c.notFound()
|
|
})
|
|
|
|
// Global error handler
|
|
app.onError((err, c) => {
|
|
console.error('Error:', err)
|
|
|
|
const status = err instanceof TRPCError
|
|
? (err.code === 'UNAUTHORIZED' ? 401 : 500)
|
|
: 500
|
|
|
|
const message = err.message || 'Internal Server Error'
|
|
|
|
return c.json({ message }, status)
|
|
})
|
|
|
|
// Start server
|
|
serve({
|
|
fetch: app.fetch,
|
|
port: 4000,
|
|
hostname: '0.0.0.0'
|
|
})
|
|
|
|
console.log('🚀 Server running on http://localhost:4000')
|