freshyo/apps/backend/index.ts
2026-03-22 16:52:25 +05:30

168 lines
4.4 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()
signedUrlCache.loadFromDisk()
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')