143 lines
3.9 KiB
TypeScript
Executable file
143 lines
3.9 KiB
TypeScript
Executable file
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<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);
|
|
});
|
|
|
|
serve({
|
|
fetch: app.fetch,
|
|
port: 4000,
|
|
}, (info) => {
|
|
console.log(`Server is running on http://localhost:${info.port}/api/mobile/`);
|
|
});
|