126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
import { Hono } from 'hono'
|
|
import { cors } from 'hono/cors'
|
|
import { logger } from 'hono/logger'
|
|
import { trpcServer } from '@hono/trpc-server'
|
|
import { getStaffUserById, isUserSuspended } from '@/src/dbService'
|
|
import mainRouter from '@/src/main-router'
|
|
import { appRouter } from '@/src/trpc/router'
|
|
import { TRPCError } from '@trpc/server'
|
|
import { jwtVerify } from 'jose'
|
|
import { getEncodedJwtSecret } from '@/src/lib/env-exporter'
|
|
|
|
export const createApp = () => {
|
|
const app = new Hono()
|
|
|
|
// CORS middleware
|
|
app.use(cors({
|
|
origin: ['http://localhost:5174', 'https://ui.freshyo.in'],
|
|
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
allowHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization'],
|
|
credentials: true,
|
|
}))
|
|
|
|
// Logger middleware
|
|
app.use(logger())
|
|
|
|
// tRPC middleware
|
|
app.use('/api/trpc/*', trpcServer({
|
|
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 { payload } = await jwtVerify(token, getEncodedJwtSecret())
|
|
const decoded = payload as any
|
|
|
|
// Check if this is a staff token (has staffId)
|
|
if (decoded.staffId) {
|
|
// This is a staff token, verify staff exists
|
|
const staff = await getStaffUserById(decoded.staffId)
|
|
|
|
if (staff) {
|
|
user = staffUser
|
|
staffUser = {
|
|
id: staff.id,
|
|
name: staff.name,
|
|
}
|
|
}
|
|
} else {
|
|
// This is a regular user token
|
|
user = decoded
|
|
|
|
// Check if user is suspended
|
|
const suspended = await isUserSuspended(user.userId)
|
|
|
|
if (suspended) {
|
|
throw new TRPCError({
|
|
code: 'FORBIDDEN',
|
|
message: 'Account suspended',
|
|
})
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Invalid token, both user and staffUser remain null
|
|
}
|
|
}
|
|
return { req, user, staffUser }
|
|
},
|
|
onError({ error, path, type, ctx }) {
|
|
console.error('🚨 tRPC Error :', {
|
|
path,
|
|
type,
|
|
code: error.code,
|
|
message: error.message,
|
|
userId: ctx?.user?.userId,
|
|
stack: error.stack,
|
|
})
|
|
},
|
|
}))
|
|
|
|
// Mount main router
|
|
app.route('/api', mainRouter)
|
|
|
|
// Global error handler
|
|
app.onError((err, c) => {
|
|
console.error(err)
|
|
// Handle different error types
|
|
let status = 500
|
|
let message = 'Internal Server Error'
|
|
|
|
if (err instanceof TRPCError) {
|
|
// Map TRPC error codes to HTTP status codes
|
|
const trpcStatusMap: Record<string, number> = {
|
|
BAD_REQUEST: 400,
|
|
UNAUTHORIZED: 401,
|
|
FORBIDDEN: 403,
|
|
NOT_FOUND: 404,
|
|
TIMEOUT: 408,
|
|
CONFLICT: 409,
|
|
PRECONDITION_FAILED: 412,
|
|
PAYLOAD_TOO_LARGE: 413,
|
|
METHOD_NOT_SUPPORTED: 405,
|
|
UNPROCESSABLE_CONTENT: 422,
|
|
TOO_MANY_REQUESTS: 429,
|
|
INTERNAL_SERVER_ERROR: 500,
|
|
}
|
|
status = trpcStatusMap[err.code] || 500
|
|
message = err.message
|
|
} else if ((err as any).statusCode) {
|
|
status = (err as any).statusCode
|
|
message = err.message
|
|
} else if ((err as any).status) {
|
|
status = (err as any).status
|
|
message = err.message
|
|
} else if (err.message) {
|
|
message = err.message
|
|
}
|
|
|
|
return c.json({ message }, status as any)
|
|
})
|
|
|
|
return app
|
|
}
|