import 'dotenv/config'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { serve } from '@hono/node-server'; import { trpcServer } from '@hono/trpc-server'; import { getStaffUserById, isUserSuspended } from '@/src/dbService'; import mainRouter from '@/src/main-router'; import initFunc from '@/src/lib/init'; import { appRouter } from '@/src/trpc/router'; import { TRPCError } from '@trpc/server'; import { jwtVerify } from 'jose' import { encodedJwtSecret } from '@/src/lib/env-exporter'; import signedUrlCache from '@/src/lib/signed-url-cache'; import { seed } from '@/src/lib/seed'; import '@/src/jobs/jobs-index'; import { startAutomatedJobs } from '@/src/lib/automatedJobs'; seed() initFunc() startAutomatedJobs() const app = new Hono(); // CORS middleware app.use(cors({ origin: 'http://localhost:5174', allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization'], credentials: true, })); signedUrlCache.loadFromDisk(); // 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, encodedJwtSecret); 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 = { '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); }); serve({ fetch: app.fetch, port: 4000, }, (info) => { console.log(`Server is running on http://localhost:${info.port}/api/mobile/`); });