This commit is contained in:
shafi54 2026-03-27 00:34:32 +05:30
parent 639428caba
commit 1b042819af
18 changed files with 303 additions and 691 deletions

View file

@ -1,13 +1,12 @@
import 'dotenv/config';
import express, { NextFunction, Request, Response } from "express";
import cors from "cors";
// import bodyParser from "body-parser";
import path from "path";
import fs from "fs";
import { getStaffUserById, getUserDetailsByUserId, isUserSuspended } from '@/src/dbService';
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 { createExpressMiddleware } from '@trpc/server/adapters/express';
import { appRouter } from '@/src/trpc/router';
import { TRPCError } from '@trpc/server';
import { jwtVerify } from 'jose'
@ -21,57 +20,28 @@ seed()
initFunc()
startAutomatedJobs()
const app = express();
const app = new Hono();
// CORS middleware
app.use(cors({
origin: 'http://localhost:5174'
origin: 'http://localhost:5174',
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization'],
credentials: true,
}));
signedUrlCache.loadFromDisk();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Logger middleware
app.use(logger());
// Middleware to log all request URLs
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.url}`);
next();
});
//cors middleware
export function corsMiddleware(req: Request, res: Response, next: NextFunction) {
// Allow requests from any origin (for production, replace * with your domain)
res.header('Access-Control-Allow-Origin', '*');
// Allow specific headers clients can send
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization'
);
// Allow specific HTTP methods
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
// Allow credentials if needed (optional)
// res.header('Access-Control-Allow-Credentials', 'true');
// Handle preflight (OPTIONS) requests quickly
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
}
app.use('/api/trpc', createExpressMiddleware({
// tRPC middleware
app.use('/api/trpc', trpcServer({
router: appRouter,
createContext: async ({ req, res }) => {
createContext: async ({ req }) => {
let user = null;
let staffUser = null;
const authHeader = req.headers.authorization;
const authHeader = req.headers.get('authorization');
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
@ -85,7 +55,7 @@ app.use('/api/trpc', createExpressMiddleware({
const staff = await getStaffUserById(decoded.staffId);
if (staff) {
user=staffUser
user = staffUser
staffUser = {
id: staff.id,
name: staff.name,
@ -110,71 +80,64 @@ app.use('/api/trpc', createExpressMiddleware({
// Invalid token, both user and staffUser remain null
}
}
return { req, res, user, staffUser };
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,
});
},
console.error('🚨 tRPC Error :', {
path,
type,
code: error.code,
message: error.message,
userId: ctx?.user?.userId,
stack: error.stack,
});
},
}));
app.use('/api', mainRouter)
const fallbackUiDirCandidates = [
path.resolve(__dirname, '../fallback-ui/dist'),
path.resolve(__dirname, '../../fallback-ui/dist'),
path.resolve(process.cwd(), '../fallback-ui/dist'),
path.resolve(process.cwd(), './apps/fallback-ui/dist')
]
const fallbackUiDir =
fallbackUiDirCandidates.find((candidate) => fs.existsSync(candidate)) ??
fallbackUiDirCandidates[0]
const fallbackUiIndex = path.join(fallbackUiDir, 'index.html')
// const fallbackUiMountPath = '/admin-web'
const fallbackUiMountPath = '/';
if (fs.existsSync(fallbackUiIndex)) {
app.use(fallbackUiMountPath, express.static(fallbackUiDir))
app.use('/mf'+fallbackUiMountPath, express.static(fallbackUiDir))
const fallbackUiRegex = new RegExp(
`^${fallbackUiMountPath.replace(/\//g, '\\/')}(?:\\/.*)?$`
)
app.get([fallbackUiMountPath, fallbackUiRegex], (req, res) => {
res.sendFile(fallbackUiIndex)
})
app.get(/.*/, (req,res) => {
res.sendFile(fallbackUiIndex)
})
} else {
console.warn(`Fallback UI build not found at ${fallbackUiIndex}`)
}
// Serve /assets/public folder at /assets route
const assetsPublicDir = path.resolve(__dirname, './assets/public');
if (fs.existsSync(assetsPublicDir)) {
app.use('/assets', express.static(assetsPublicDir));
console.log('Serving /assets from', assetsPublicDir);
} else {
console.warn('Assets public folder not found at', assetsPublicDir);
}
// Mount main router
app.route('/api', mainRouter);
// Global error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
app.onError((err, c) => {
console.error(err);
const status = err.statusCode || err.status || 500;
const message = err.message || 'Internal Server Error';
res.status(status).json({ message });
// 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);
});
app.listen(4000, () => {
console.log("Server is running on http://localhost:4000/api/mobile/");
serve({
fetch: app.fetch,
port: 4000,
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}/api/mobile/`);
});

View file

@ -20,23 +20,22 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.888.0",
"@aws-sdk/s3-request-presigner": "^3.888.0",
"@hono/node-server": "^1.19.11",
"@hono/trpc-server": "^0.4.2",
"@trpc/server": "^11.6.0",
"@turf/turf": "^7.2.0",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.19",
"axios": "^1.11.0",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dayjs": "^1.11.18",
"dotenv": "^17.2.1",
"expo-server-sdk": "^4.0.0",
"express": "^5.1.0",
"fuse.js": "^7.1.0",
"hono": "^4.12.9",
"jose": "^6.2.2",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.5.2",
"rimraf": "^6.1.2",
"ts-node-dev": "^2.0.0",

View file

@ -1,10 +1,10 @@
import { Router } from "express";
import { Hono } from 'hono';
import { authenticateStaff } from "@/src/middleware/staff-auth";
const router = Router();
const router = new Hono();
// Apply staff authentication to all admin routes
router.use(authenticateStaff);
router.use('*', authenticateStaff);
const avRouter = router;

View file

@ -1,4 +1,4 @@
import { Request, Response } from "express";
import { Context } from 'hono';
import {
checkProductExistsByName,
checkUnitExists,
@ -25,8 +25,9 @@ type CreateDeal = {
/**
* Create a new product
*/
export const createProduct = async (req: Request, res: Response) => {
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals, tagIds } = req.body;
export const createProduct = async (c: Context) => {
const body = await c.req.parseBody({ all: true });
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals, tagIds } = body;
// Validate required fields
if (!name || !unitId || !storeId || !price) {
@ -34,100 +35,111 @@ export const createProduct = async (req: Request, res: Response) => {
}
// Check for duplicate name
const existingProduct = await checkProductExistsByName(name.trim())
const existingProduct = await checkProductExistsByName((name as string).trim())
if (existingProduct) {
throw new ApiError("A product with this name already exists", 400);
}
// Check if unit exists
const unitExists = await checkUnitExists(unitId)
const unitExists = await checkUnitExists(parseInt(unitId as string))
if (!unitExists) {
throw new ApiError("Invalid unit ID", 400);
}
// Extract images from req.files
const images = (req.files as Express.Multer.File[])?.filter(item => item.fieldname === 'images');
// Extract images from body
const images = body.images;
let uploadedImageUrls: string[] = [];
if (images && Array.isArray(images)) {
const imageUploadPromises = images.map((file, index) => {
const key = `product-images/${Date.now()}-${index}`;
return imageUploadS3(file.buffer, file.mimetype, key);
});
if (images) {
const imageFiles = Array.isArray(images) ? images : [images];
const imageUploadPromises = imageFiles.map((file, index) => {
if (file instanceof File) {
const key = `product-images/${Date.now()}-${index}`;
return imageUploadS3(Buffer.from(file.stream() as any), file.type, key);
}
return null;
}).filter(Boolean);
uploadedImageUrls = await Promise.all(imageUploadPromises);
uploadedImageUrls = await Promise.all(imageUploadPromises as Promise<string>[]);
}
// Create product
const productData: any = {
name,
shortDescription,
longDescription,
unitId,
storeId,
price,
marketPrice,
incrementStep: incrementStep || 1,
productQuantity: productQuantity || 1,
isSuspended: isSuspended || false,
isFlashAvailable: isFlashAvailable || false,
name: name as string,
shortDescription: shortDescription as string | undefined,
longDescription: longDescription as string | undefined,
unitId: parseInt(unitId as string),
storeId: parseInt(storeId as string),
price: parseFloat(price as string),
marketPrice: marketPrice ? parseFloat(marketPrice as string) : null,
incrementStep: incrementStep ? parseInt(incrementStep as string) : 1,
productQuantity: productQuantity ? parseInt(productQuantity as string) : 1,
isSuspended: isSuspended === 'true',
isFlashAvailable: isFlashAvailable === 'true',
images: uploadedImageUrls,
};
if (flashPrice) {
productData.flashPrice = parseFloat(flashPrice);
productData.flashPrice = parseFloat(flashPrice as string);
}
const newProduct = await createProductRecord(productData)
// Handle deals if provided
let createdDeals: AdminSpecialDeal[] = []
if (deals && Array.isArray(deals)) {
createdDeals = await createSpecialDealsForProduct(newProduct.id, deals)
if (deals) {
const parsedDeals = typeof deals === 'string' ? JSON.parse(deals) : deals;
if (Array.isArray(parsedDeals)) {
createdDeals = await createSpecialDealsForProduct(newProduct.id, parsedDeals)
}
}
// Handle tag assignments if provided
if (tagIds && Array.isArray(tagIds)) {
await replaceProductTags(newProduct.id, tagIds)
if (tagIds) {
const parsedTagIds = typeof tagIds === 'string' ? JSON.parse(tagIds) : tagIds;
if (Array.isArray(parsedTagIds)) {
await replaceProductTags(newProduct.id, parsedTagIds)
}
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
// Send response first
res.status(201).json({
return c.json({
product: newProduct,
deals: createdDeals,
message: "Product created successfully",
});
}, 201);
};
/**
* Update a product
*/
export const updateProduct = async (req: Request, res: Response) => {
const id = req.params.id as string
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
export const updateProduct = async (c: Context) => {
const id = c.req.param('id')
const body = await c.req.parseBody({ all: true });
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = body;
const deals = dealsRaw ? JSON.parse(dealsRaw) : null;
const imagesToDelete = imagesToDeleteRaw ? JSON.parse(imagesToDeleteRaw) : [];
const deals = dealsRaw ? (typeof dealsRaw === 'string' ? JSON.parse(dealsRaw) : dealsRaw) : null;
const imagesToDelete = imagesToDeleteRaw ? (typeof imagesToDeleteRaw === 'string' ? JSON.parse(imagesToDeleteRaw) : imagesToDeleteRaw) : [];
if (!name || !unitId || !storeId || !price) {
throw new ApiError("Name, unitId, storeId, and price are required", 400);
}
// Check if unit exists
const unitExists = await checkUnitExists(unitId)
const unitExists = await checkUnitExists(parseInt(unitId as string))
if (!unitExists) {
throw new ApiError("Invalid unit ID", 400);
}
// Get current product to handle image updates
const currentImages = await getProductImagesById(parseInt(id))
const currentImages = await getProductImagesById(parseInt(id as string))
if (!currentImages) {
throw new ApiError("Product not found", 404);
@ -154,45 +166,49 @@ export const updateProduct = async (req: Request, res: Response) => {
updatedImages = updatedImages.filter(img => !imagesToRemoveFromDb.includes(img));
}
// Extract new images from req.files
const images = (req.files as Express.Multer.File[])?.filter(item => item.fieldname === 'images');
// Extract new images from body
const images = body.images;
let uploadedImageUrls: string[] = [];
if (images && Array.isArray(images)) {
const imageUploadPromises = images.map((file, index) => {
const key = `product-images/${Date.now()}-${index}`;
return imageUploadS3(file.buffer, file.mimetype, key);
});
if (images) {
const imageFiles = Array.isArray(images) ? images : [images];
const imageUploadPromises = imageFiles.map((file, index) => {
if (file instanceof File) {
const key = `product-images/${Date.now()}-${index}`;
return imageUploadS3(Buffer.from(file.stream() as any), file.type, key);
}
return null;
}).filter(Boolean);
uploadedImageUrls = await Promise.all(imageUploadPromises);
uploadedImageUrls = await Promise.all(imageUploadPromises as Promise<string>[]);
}
// Combine remaining current images with new uploaded images
const finalImages = [...updatedImages, ...uploadedImageUrls];
const updateData: any = {
name,
shortDescription,
longDescription,
unitId,
storeId,
price,
marketPrice,
incrementStep: incrementStep || 1,
productQuantity: productQuantity || 1,
isSuspended: isSuspended || false,
name: name as string,
shortDescription: shortDescription as string | undefined,
longDescription: longDescription as string | undefined,
unitId: parseInt(unitId as string),
storeId: parseInt(storeId as string),
price: parseFloat(price as string),
marketPrice: marketPrice ? parseFloat(marketPrice as string) : null,
incrementStep: incrementStep ? parseInt(incrementStep as string) : 1,
productQuantity: productQuantity ? parseInt(productQuantity as string) : 1,
isSuspended: isSuspended === 'true',
images: finalImages.length > 0 ? finalImages : undefined,
};
if (isFlashAvailable !== undefined) {
updateData.isFlashAvailable = isFlashAvailable;
updateData.isFlashAvailable = isFlashAvailable === 'true';
}
if (flashPrice !== undefined) {
updateData.flashPrice = flashPrice ? parseFloat(flashPrice) : null;
updateData.flashPrice = flashPrice ? parseFloat(flashPrice as string) : null;
}
const updatedProduct = await updateProductRecord(parseInt(id), updateData)
const updatedProduct = await updateProductRecord(parseInt(id as string), updateData)
if (!updatedProduct) {
throw new ApiError("Product not found", 404);
@ -200,22 +216,21 @@ export const updateProduct = async (req: Request, res: Response) => {
// Handle deals if provided
if (deals && Array.isArray(deals)) {
await updateProductDeals(parseInt(id), deals)
await updateProductDeals(parseInt(id as string), deals)
}
// Handle tag assignments if provided
// if (tagIds && Array.isArray(tagIds)) {
if (tagIds && Boolean(tagIds)) {
const tagIdsArray = Array.isArray(tagIds) ? tagIds : [+tagIds]
await replaceProductTags(parseInt(id), tagIdsArray)
if (tagIds) {
const parsedTagIds = typeof tagIds === 'string' ? [parseInt(tagIds)] : (Array.isArray(tagIds) ? tagIds.map((t: any) => parseInt(t)) : [parseInt(tagIds as any)])
await replaceProductTags(parseInt(id as string), parsedTagIds)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
// Send response first
res.status(200).json({
return c.json({
product: updatedProduct,
message: "Product updated successfully",
});
}, 200);
};

View file

@ -1,4 +1,4 @@
import { Request, Response } from "express";
import { Context } from 'hono';
import { scaffoldAssetUrl } from "@/src/lib/s3-client"
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common"
import {
@ -9,19 +9,19 @@ import {
/**
* Get all products summary for dropdown
*/
export const getAllProductsSummary = async (req: Request, res: Response) => {
export const getAllProductsSummary = async (c: Context) => {
try {
const { tagId } = req.query;
const tagIdNum = tagId ? parseInt(tagId as string) : undefined;
const tagId = c.req.query('tagId');
const tagIdNum = tagId ? parseInt(tagId) : undefined;
// If tagId is provided but no products found, return empty array
if (tagIdNum) {
const products = await getAllProductsWithUnits(tagIdNum);
if (products.length === 0) {
return res.status(200).json({
return c.json({
products: [],
count: 0,
});
}, 200);
}
}
@ -46,13 +46,13 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
})
);
return res.status(200).json({
return c.json({
products: formattedProducts,
count: formattedProducts.length,
});
}, 200);
} catch (error) {
console.error("Get products summary error:", error);
return res.status(500).json({ error: "Failed to fetch products summary" });
return c.json({ error: "Failed to fetch products summary" }, 500);
}
};

View file

@ -1,10 +1,10 @@
import { Router } from "express";
import { Hono } from 'hono';
import { getAllProductsSummary } from "@/src/apis/common-apis/apis/common-product.controller"
const router = Router();
const router = new Hono();
router.get("/summary", getAllProductsSummary);
const commonProductsRouter= router;
export default commonProductsRouter;
export default commonProductsRouter;

View file

@ -1,10 +1,10 @@
import { Router } from "express";
import { Hono } from 'hono';
import commonProductsRouter from "@/src/apis/common-apis/apis/common-product.router"
const router = Router();
const router = new Hono();
router.use('/products', commonProductsRouter)
router.route('/products', commonProductsRouter)
const commonRouter = router;
export default commonRouter;
export default commonRouter;

View file

@ -1,6 +1,11 @@
import express from 'express';
const catchAsync =
(fn: express.RequestHandler) =>
(req: express.Request, res: express.Response, next: express.NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
export default catchAsync;
// catchAsync is no longer needed with Hono
// Hono handles async errors automatically
// This file is kept for backward compatibility but should be removed in the future
import { Context } from 'hono';
const catchAsync = (fn: (c: Context) => Promise<Response>) => {
return fn;
};
export default catchAsync;

View file

@ -1,56 +1,36 @@
import { Router, Request, Response, NextFunction } from "express";
import { Hono } from 'hono';
import avRouter from "@/src/apis/admin-apis/apis/av-router"
import { ApiError } from "@/src/lib/api-error"
import v1Router from "@/src/v1-router"
import testController from "@/src/test-controller"
import { authenticateUser } from "@/src/middleware/auth.middleware"
const router = Router();
const router = new Hono();
// Health check endpoints (no auth required)
router.get('/health', (req: Request, res: Response) => {
res.status(200).json({
router.get('/health', (c) => {
return c.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
message: 'Hello world'
});
});
router.get('/seed', (req:Request, res: Response) => {
res.status(200).json({
router.get('/seed', (c) => {
return c.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
})
});
// Apply authentication middleware to all subsequent routes
router.use(authenticateUser);
router.use('*', authenticateUser);
router.use('/v1', v1Router);
// router.use('/av', avRouter);
router.use('/test', testController);
// Global error handling middleware
router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Error:', err);
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
error: err.message,
details: err.details,
statusCode: err.statusCode
});
}
// Handle unknown errors
return res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
statusCode: 500
});
});
router.route('/v1', v1Router);
// router.route('/av', avRouter);
router.route('/test', testController);
const mainRouter = router;

View file

@ -1,32 +1,31 @@
import { Request, Response, NextFunction } from 'express';
import { Context, Next } from 'hono';
import { jwtVerify } from 'jose';
import { getStaffUserById, isUserSuspended } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error';
import { encodedJwtSecret } from '@/src/lib/env-exporter';
interface AuthenticatedRequest extends Request {
user?: {
userId: number;
name?: string;
email?: string;
mobile?: string;
};
staffUser?: {
id: number;
name: string;
};
interface UserContext {
userId: number;
name?: string;
email?: string;
mobile?: string;
}
export const authenticateUser = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
interface StaffContext {
id: number;
name: string;
}
export const authenticateUser = async (c: Context, next: Next) => {
try {
const authHeader = req.headers.authorization;
const authHeader = c.req.header('authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new ApiError('Authorization token required', 401);
}
const token = authHeader.substring(7);
console.log(req.headers)
console.log(c.req.header)
const { payload } = await jwtVerify(token, encodedJwtSecret);
const decoded = payload as any;
@ -51,13 +50,13 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
throw new ApiError('Invalid staff token', 401);
}
req.staffUser = {
c.set('staffUser', {
id: staff.id,
name: staff.name,
};
});
} else {
// This is a regular user token
req.user = decoded;
c.set('user', decoded);
/*
// Old implementation - direct DB queries:
@ -82,8 +81,8 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
}
}
next();
await next();
} catch (error) {
next(error);
throw error;
}
};

View file

@ -1,21 +1,12 @@
import { Request, Response, NextFunction } from 'express';
import { Context, Next } from 'hono';
import { jwtVerify, errors } from 'jose';
import { ApiError } from '@/src/lib/api-error'
import { encodedJwtSecret } from '@/src/lib/env-exporter';
// Extend the Request interface to include user property
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export const verifyToken = async (req: Request, res: Response, next: NextFunction) => {
export const verifyToken = async (c: Context, next: Next) => {
try {
// Get token from Authorization header
const authHeader = req.headers.authorization;
const authHeader = c.req.header('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
@ -32,37 +23,38 @@ export const verifyToken = async (req: Request, res: Response, next: NextFunctio
const { payload } = await jwtVerify(token, encodedJwtSecret);
// Add user info to request
req.user = payload;
// Add user info to context
c.set('user', payload);
next();
await next();
} catch (error) {
if (error instanceof errors.JOSEError) {
next(new ApiError('Invalid Auth Credentials', 401));
throw new ApiError('Invalid Auth Credentials', 401);
} else {
next(error);
throw error;
}
}
};
export const requireRole = (roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
return async (c: Context, next: Next) => {
try {
if (!req.user) {
const user = c.get('user');
if (!user) {
throw new ApiError('Authentication required', 401);
}
// Check if user has any of the required roles
const userRoles = req.user.roles || [];
const userRoles = user.roles || [];
const hasPermission = roles.some(role => userRoles.includes(role));
if (!hasPermission) {
throw new ApiError('Access denied. Insufficient permissions', 403);
}
next();
await next();
} catch (error) {
next(error);
throw error;
}
};
};

View file

@ -1,21 +1,9 @@
import { Request, Response, NextFunction } from 'express';
import { jwtVerify, errors } from 'jose';
import { Context, Next } from 'hono';
import { jwtVerify } from 'jose';
import { getStaffUserById } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error';
import { encodedJwtSecret } from '@/src/lib/env-exporter';
// Extend Request interface to include staffUser
declare global {
namespace Express {
interface Request {
staffUser?: {
id: number;
name: string;
};
}
}
}
/**
* Verify JWT token and extract payload
*/
@ -29,12 +17,12 @@ const verifyStaffToken = async (token: string) => {
};
/**
* Middleware to authenticate staff users and attach staffUser to request
* Middleware to authenticate staff users and attach staffUser to context
*/
export const authenticateStaff = async (req: Request, res: Response, next: NextFunction) => {
export const authenticateStaff = async (c: Context, next: Next) => {
try {
// Extract token from Authorization header
const authHeader = req.headers.authorization;
const authHeader = c.req.header('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new ApiError('Staff authentication required', 401);
@ -72,14 +60,14 @@ export const authenticateStaff = async (req: Request, res: Response, next: NextF
throw new ApiError('Staff user not found', 401);
}
// Attach staff user to request
req.staffUser = {
// Attach staff user to context
c.set('staffUser', {
id: staff.id,
name: staff.name,
};
});
next();
await next();
} catch (error) {
next(error);
throw error;
}
};

View file

@ -1,13 +1,13 @@
import { Router, Request, Response } from 'express';
import { Hono } from 'hono';
const router = Router();
const router = new Hono();
router.get('/', (req: Request, res: Response) => {
res.json({
router.get('/', (c) => {
return c.json({
status: 'ok',
message: 'Health check passed',
timestamp: new Date().toISOString(),
});
});
export default router;
export default router;

View file

@ -357,7 +357,8 @@ export const authRouter = router({
? await generateSignedUrlFromS3Url(userDetail.profileImage)
: null
const token = ctx.req.headers.authorization?.replace('Bearer ', '') || ''
const authHeader = ctx.req.header('authorization');
const token = authHeader?.replace('Bearer ', '') || ''
const response: UserAuthResponse = {
token,

View file

@ -1,9 +1,8 @@
import { initTRPC, TRPCError } from '@trpc/server';
import { type CreateExpressContextOptions } from '@trpc/server/adapters/express';
import type { Context as HonoContext } from 'hono';
export interface Context {
req: CreateExpressContextOptions['req'];
res: CreateExpressContextOptions['res'];
req: HonoContext['req'];
user?: any;
staffUser?: {
id: number;
@ -70,4 +69,4 @@ export const protectedProcedure = t.procedure.use(errorLoggerMiddleware).use(
);
export const createCallerFactory = t.createCallerFactory;
export const createTRPCRouter = t.router;
export const createTRPCRouter = t.router;

13
apps/backend/src/types/hono.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
import { Context as HonoContext } from 'hono';
declare module 'hono' {
interface ContextVariableMap {
user?: any;
staffUser?: {
id: number;
name: string;
};
}
}
export {};

View file

@ -1,12 +1,11 @@
import { Router } from "express";
import { Hono } from 'hono';
import avRouter from "@/src/apis/admin-apis/apis/av-router"
import commonRouter from "@/src/apis/common-apis/apis/common.router"
const router = Router();
router.use('/av', avRouter);
router.use('/cm', commonRouter);
const router = new Hono();
router.route('/av', avRouter);
router.route('/cm', commonRouter);
const v1Router = router;

415
package-lock.json generated
View file

@ -127,23 +127,22 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.888.0",
"@aws-sdk/s3-request-presigner": "^3.888.0",
"@hono/node-server": "^1.19.11",
"@hono/trpc-server": "^0.4.2",
"@trpc/server": "^11.6.0",
"@turf/turf": "^7.2.0",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.19",
"axios": "^1.11.0",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dayjs": "^1.11.18",
"dotenv": "^17.2.1",
"expo-server-sdk": "^4.0.0",
"express": "^5.1.0",
"fuse.js": "^7.1.0",
"hono": "^4.12.9",
"jose": "^6.2.2",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.5.2",
"rimraf": "^6.1.2",
"ts-node-dev": "^2.0.0",
@ -3720,6 +3719,31 @@
"excpretty": "build/cli.js"
}
},
"node_modules/@hono/node-server": {
"version": "1.19.11",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@hono/trpc-server": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@hono/trpc-server/-/trpc-server-0.4.2.tgz",
"integrity": "sha512-3TDrc42CZLgcTFkXQba+y7JlRWRiyw1AqhLqztWyNS2IFT+3bHld0lxKdGBttCtGKHYx0505dM67RMazjhdZqw==",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@trpc/server": "^10.10.0 || >11.0.0-rc",
"hono": ">=4.0.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"dev": true,
@ -7600,35 +7624,11 @@
"version": "2.4.6",
"license": "MIT"
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/canvas-confetti": {
"version": "1.9.0",
"dev": true,
"license": "MIT"
},
"node_modules/@types/connect": {
"version": "3.4.38",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/cors": {
"version": "2.8.19",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/d3-voronoi": {
"version": "1.1.12",
"license": "MIT"
@ -7642,27 +7642,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/express": {
"version": "5.0.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "^2"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"license": "MIT"
@ -7706,11 +7685,6 @@
"@types/react": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.5",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"license": "MIT"
@ -7781,16 +7755,6 @@
"pg-types": "^2.2.0"
}
},
"node_modules/@types/qs": {
"version": "6.15.0",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.0.14",
"license": "MIT",
@ -7821,23 +7785,6 @@
"@types/react": "*"
}
},
"node_modules/@types/send": {
"version": "1.2.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "2.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*"
}
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"license": "MIT"
@ -8232,17 +8179,6 @@
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"license": "MIT",
@ -8915,28 +8851,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/body-parser": {
"version": "2.2.2",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.14.1",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"license": "ISC"
@ -9573,17 +9487,6 @@
"@babel/types": "^7.6.1"
}
},
"node_modules/content-disposition": {
"version": "1.0.1",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"license": "MIT",
@ -9606,13 +9509,6 @@
"version": "2.0.0",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/copy-anything": {
"version": "4.0.5",
"license": "MIT",
@ -9637,21 +9533,6 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cors": {
"version": "2.8.6",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cosmiconfig": {
"version": "5.2.1",
"license": "MIT",
@ -11587,47 +11468,6 @@
"version": "3.1.3",
"license": "Apache-2.0"
},
"node_modules/express": {
"version": "5.2.1",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/fallback-ui": {
"resolved": "apps/fallback-ui",
"link": true
@ -11809,25 +11649,6 @@
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": {
"version": "2.1.1",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"license": "MIT",
@ -11996,13 +11817,6 @@
"node": ">=8"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"license": "ISC"
@ -12439,6 +12253,15 @@
"version": "16.13.1",
"license": "MIT"
},
"node_modules/hono": {
"version": "4.12.9",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
"integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/hosted-git-info": {
"version": "7.0.2",
"license": "ISC",
@ -12486,20 +12309,6 @@
"version": "1.1.0",
"license": "BSD-3-Clause"
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"funding": [
@ -12940,10 +12749,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"license": "MIT",
@ -13773,27 +13578,10 @@
"version": "2.0.14",
"license": "CC0-1.0"
},
"node_modules/media-typer": {
"version": "1.1.0",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"license": "MIT"
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"license": "MIT"
@ -14175,27 +13963,6 @@
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/mimic-fn": {
"version": "1.2.0",
"license": "MIT",
@ -14311,13 +14078,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nested-error-stacks": {
"version": "2.0.1",
"license": "MIT"
@ -15443,19 +15203,6 @@
"qrcode-terminal": "bin/qrcode-terminal.js"
}
},
"node_modules/qs": {
"version": "6.15.0",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/query-string": {
"version": "7.1.3",
"license": "MIT",
@ -15516,19 +15263,6 @@
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "3.0.2",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.7.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/rbush": {
"version": "3.0.1",
"license": "MIT",
@ -16394,28 +16128,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/router": {
"version": "2.2.0",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/router/node_modules/path-to-regexp": {
"version": "8.3.0",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"funding": [
@ -16529,30 +16241,6 @@
"semver": "bin/semver.js"
}
},
"node_modules/send": {
"version": "1.2.1",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/serialize-error": {
"version": "2.1.0",
"license": "MIT",
@ -16577,23 +16265,6 @@
"seroval": "^1.0"
}
},
"node_modules/serve-static": {
"version": "2.2.1",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/server-only": {
"version": "0.0.1",
"license": "MIT"
@ -17944,18 +17615,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"dev": true,