This commit is contained in:
shafi54 2026-03-26 18:59:58 +05:30
parent 68103010c6
commit 639428caba
21 changed files with 19950 additions and 403 deletions

View file

@ -10,7 +10,8 @@ import initFunc from '@/src/lib/init';
import { createExpressMiddleware } from '@trpc/server/adapters/express'; import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { appRouter } from '@/src/trpc/router'; import { appRouter } from '@/src/trpc/router';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import jwt from 'jsonwebtoken' import { jwtVerify } from 'jose'
import { encodedJwtSecret } from '@/src/lib/env-exporter';
import signedUrlCache from '@/src/lib/signed-url-cache'; import signedUrlCache from '@/src/lib/signed-url-cache';
import { seed } from '@/src/lib/seed'; import { seed } from '@/src/lib/seed';
import '@/src/jobs/jobs-index'; import '@/src/jobs/jobs-index';
@ -75,7 +76,8 @@ app.use('/api/trpc', createExpressMiddleware({
if (authHeader?.startsWith('Bearer ')) { if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7); const token = authHeader.substring(7);
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key') as any; const { payload } = await jwtVerify(token, encodedJwtSecret);
const decoded = payload as any;
// Check if this is a staff token (has staffId) // Check if this is a staff token (has staffId)
if (decoded.staffId) { if (decoded.staffId) {

View file

@ -24,19 +24,15 @@
"@turf/turf": "^7.2.0", "@turf/turf": "^7.2.0",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/jsonwebtoken": "^9.0.10",
"axios": "^1.11.0", "axios": "^1.11.0",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"bullmq": "^5.63.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dayjs": "^1.11.18", "dayjs": "^1.11.18",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"expo-server-sdk": "^4.0.0", "expo-server-sdk": "^4.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"jsonwebtoken": "^9.0.2", "jose": "^6.2.2",
"node-cron": "^4.2.1",
"redis": "^5.9.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
@ -44,8 +40,8 @@
"@types/node": "^24.5.2", "@types/node": "^24.5.2",
"rimraf": "^6.1.2", "rimraf": "^6.1.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsx": "^4.20.5",
"tsc-alias": "^1.8.16", "tsc-alias": "^1.8.16",
"tsx": "^4.20.5",
"typescript": "^5.9.2" "typescript": "^5.9.2"
} }
} }

View file

@ -1,4 +1,3 @@
import * as cron from 'node-cron';
import { checkPendingPayments, checkRefundStatuses } from '@/src/jobs/payment-status-checker' import { checkPendingPayments, checkRefundStatuses } from '@/src/jobs/payment-status-checker'
const runCombinedJob = async () => { const runCombinedJob = async () => {
@ -25,4 +24,4 @@ const runCombinedJob = async () => {
runCombinedJob(); runCombinedJob();
// Schedule combined cron job every 10 minutes // Schedule combined cron job every 10 minutes
cron.schedule('*/10 * * * *', runCombinedJob); // cron.schedule('*/10 * * * *', runCombinedJob);

View file

@ -1,4 +1,3 @@
import * as cron from 'node-cron';
interface PendingPaymentRecord { interface PendingPaymentRecord {
payment: any; payment: any;

View file

@ -1,4 +1,5 @@
import * as cron from 'node-cron'; // import * as cron from 'node-cron';
const cron:any = {}
import { toggleFlashDeliveryForItems, toggleKeyVal } from '@/src/dbService'; import { toggleFlashDeliveryForItems, toggleKeyVal } from '@/src/dbService';
import { CONST_KEYS } from '@/src/lib/const-keys' import { CONST_KEYS } from '@/src/lib/const-keys'
import { computeConstants } from '@/src/lib/const-store' import { computeConstants } from '@/src/lib/const-store'

View file

@ -1,8 +1,8 @@
import { getAllKeyValStore } from '@/src/dbService' import { getAllKeyValStore } from '@/src/dbService'
import redisClient from '@/src/lib/redis-client' // import redisClient from '@/src/lib/redis-client'
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys' import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys'
const CONST_REDIS_PREFIX = 'const:'; // const CONST_REDIS_PREFIX = 'const:';
export const computeConstants = async (): Promise<void> => { export const computeConstants = async (): Promise<void> => {
try { try {
@ -18,15 +18,13 @@ export const computeConstants = async (): Promise<void> => {
const constants = await getAllKeyValStore(); const constants = await getAllKeyValStore();
for (const constant of constants) { // for (const constant of constants) {
const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`; // const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`;
const value = JSON.stringify(constant.value); // const value = JSON.stringify(constant.value);
// console.log({redisKey, value}) // await redisClient.set(redisKey, value);
// }
await redisClient.set(redisKey, value); console.log(`Computed ${constants.length} constants from DB`);
}
console.log(`Computed and stored ${constants.length} constants in Redis`);
} catch (error) { } catch (error) {
console.error('Failed to compute constants:', error); console.error('Failed to compute constants:', error);
throw error; throw error;
@ -34,46 +32,76 @@ export const computeConstants = async (): Promise<void> => {
}; };
export const getConstant = async <T = any>(key: string): Promise<T | null> => { export const getConstant = async <T = any>(key: string): Promise<T | null> => {
const redisKey = `${CONST_REDIS_PREFIX}${key}`; // const redisKey = `${CONST_REDIS_PREFIX}${key}`;
const value = await redisClient.get(redisKey); // const value = await redisClient.get(redisKey);
//
// if (!value) {
// return null;
// }
//
// try {
// return JSON.parse(value) as T;
// } catch {
// return value as unknown as T;
// }
if (!value) { const constants = await getAllKeyValStore();
const entry = constants.find(c => c.key === key);
if (!entry) {
return null; return null;
} }
try { return entry.value as T;
return JSON.parse(value) as T;
} catch {
return value as unknown as T;
}
}; };
export const getConstants = async <T = any>(keys: string[]): Promise<Record<string, T | null>> => { export const getConstants = async <T = any>(keys: string[]): Promise<Record<string, T | null>> => {
const redisKeys = keys.map(key => `${CONST_REDIS_PREFIX}${key}`); // const redisKeys = keys.map(key => `${CONST_REDIS_PREFIX}${key}`);
const values = await redisClient.MGET(redisKeys); // const values = await redisClient.MGET(redisKeys);
//
// const result: Record<string, T | null> = {};
// keys.forEach((key, index) => {
// const value = values[index];
// if (!value) {
// result[key] = null;
// } else {
// try {
// result[key] = JSON.parse(value) as T;
// } catch {
// result[key] = value as unknown as T;
// }
// }
// });
//
// return result;
const constants = await getAllKeyValStore();
const constantsMap = new Map(constants.map(c => [c.key, c.value]));
const result: Record<string, T | null> = {}; const result: Record<string, T | null> = {};
keys.forEach((key, index) => { for (const key of keys) {
const value = values[index]; const value = constantsMap.get(key);
if (!value) { result[key] = (value !== undefined ? value : null) as T | null;
result[key] = null; }
} else {
try {
result[key] = JSON.parse(value) as T;
} catch {
result[key] = value as unknown as T;
}
}
});
return result; return result;
}; };
export const getAllConstValues = async (): Promise<Record<ConstKey, any>> => { export const getAllConstValues = async (): Promise<Record<ConstKey, any>> => {
const result: Record<string, any> = {}; // const result: Record<string, any> = {};
//
// for (const key of CONST_KEYS_ARRAY) {
// result[key] = await getConstant(key);
// }
//
// return result as Record<ConstKey, any>;
const constants = await getAllKeyValStore();
const constantsMap = new Map(constants.map(c => [c.key, c.value]));
const result: Record<string, any> = {};
for (const key of CONST_KEYS_ARRAY) { for (const key of CONST_KEYS_ARRAY) {
result[key] = await getConstant(key); result[key] = constantsMap.get(key) ?? null;
} }
return result as Record<ConstKey, any>; return result as Record<ConstKey, any>;

View file

@ -1,10 +1,11 @@
import redisClient from '@/src/lib/redis-client' // import redisClient from '@/src/lib/redis-client'
export async function enqueue(queueName: string, eventData: any): Promise<boolean> { export async function enqueue(queueName: string, eventData: any): Promise<boolean> {
try { try {
const jsonData = JSON.stringify(eventData); const jsonData = JSON.stringify(eventData);
const result = await redisClient.lPush(queueName, jsonData); // const result = await redisClient.lPush(queueName, jsonData);
return result > 0; // return result > 0;
return false;
} catch (error) { } catch (error) {
console.error('Event enqueue error:', error); console.error('Event enqueue error:', error);
return false; return false;

View file

@ -1,20 +1,26 @@
import { createClient, RedisClientType } from 'redis'; // import { createClient, RedisClientType } from 'redis';
import { redisUrl } from '@/src/lib/env-exporter' import { redisUrl } from '@/src/lib/env-exporter'
const createClient = (args:any) => {}
class RedisClient { class RedisClient {
private client: RedisClientType; // private client: RedisClientType;
private subscriberClient: RedisClientType | null = null; // private subscriberClient: RedisClientType | null = null;
private isConnected: boolean = false; // private isConnected: boolean = false;
//
private client: any;
private subscriberrlient: any;
private isConnected: any = false;
constructor() { constructor() {
this.client = createClient({ this.client = createClient({
url: redisUrl, url: redisUrl,
}); });
this.client.on('error', (err) => { // this.client.on('error', (err) => {
console.error('Redis Client Error:', err); // console.error('Redis Client Error:', err);
}); // });
//
this.client.on('connect', () => { this.client.on('connect', () => {
console.log('Redis Client Connected'); console.log('Redis Client Connected');
this.isConnected = true; this.isConnected = true;
@ -34,9 +40,9 @@ class RedisClient {
}); });
// Connect immediately (fire and forget) // Connect immediately (fire and forget)
this.client.connect().catch((err) => { // this.client.connect().catch((err) => {
console.error('Failed to connect Redis:', err); // console.error('Failed to connect Redis:', err);
}); // });
} }
async set(key: string, value: string, ttlSeconds?: number): Promise<string | null> { async set(key: string, value: string, ttlSeconds?: number): Promise<string | null> {
@ -79,41 +85,41 @@ class RedisClient {
// Subscribe to a channel with callback // Subscribe to a channel with callback
async subscribe(channel: string, callback: (message: string) => void): Promise<void> { async subscribe(channel: string, callback: (message: string) => void): Promise<void> {
if (!this.subscriberClient) { // if (!this.subscriberClient) {
this.subscriberClient = createClient({ // this.subscriberClient = createClient({
url: redisUrl, // url: redisUrl,
}); // });
//
this.subscriberClient.on('error', (err) => { // this.subscriberClient.on('error', (err) => {
console.error('Redis Subscriber Error:', err); // console.error('Redis Subscriber Error:', err);
}); // });
//
this.subscriberClient.on('connect', () => { // this.subscriberClient.on('connect', () => {
console.log('Redis Subscriber Connected'); // console.log('Redis Subscriber Connected');
}); // });
//
await this.subscriberClient.connect(); // await this.subscriberClient.connect();
} // }
//
await this.subscriberClient.subscribe(channel, callback); // await this.subscriberClient.subscribe(channel, callback);
console.log(`Subscribed to channel: ${channel}`); console.log(`Subscribed to channel: ${channel}`);
} }
// Unsubscribe from a channel // Unsubscribe from a channel
async unsubscribe(channel: string): Promise<void> { async unsubscribe(channel: string): Promise<void> {
if (this.subscriberClient) { // if (this.subscriberClient) {
await this.subscriberClient.unsubscribe(channel); // await this.subscriberClient.unsubscribe(channel);
console.log(`Unsubscribed from channel: ${channel}`); // console.log(`Unsubscribed from channel: ${channel}`);
} // }
} }
disconnect(): void { disconnect(): void {
if (this.isConnected) { // if (this.isConnected) {
this.client.disconnect(); // this.client.disconnect();
} // }
if (this.subscriberClient) { // if (this.subscriberClient) {
this.subscriberClient.disconnect(); // this.subscriberClient.disconnect();
} // }
} }
get isClientConnected(): boolean { get isClientConnected(): boolean {

View file

@ -1,7 +1,8 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import { jwtVerify } from 'jose';
import { getStaffUserById, isUserSuspended } from '@/src/dbService'; import { getStaffUserById, isUserSuspended } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { encodedJwtSecret } from '@/src/lib/env-exporter';
interface AuthenticatedRequest extends Request { interface AuthenticatedRequest extends Request {
user?: { user?: {
@ -27,7 +28,8 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
const token = authHeader.substring(7); const token = authHeader.substring(7);
console.log(req.headers) console.log(req.headers)
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key') as any; const { payload } = await jwtVerify(token, encodedJwtSecret);
const decoded = payload as any;
// Check if this is a staff token (has staffId) // Check if this is a staff token (has staffId)
if (decoded.staffId) { if (decoded.staffId) {

View file

@ -1,6 +1,7 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import { jwtVerify, errors } from 'jose';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { encodedJwtSecret } from '@/src/lib/env-exporter';
// Extend the Request interface to include user property // Extend the Request interface to include user property
declare global { declare global {
@ -11,7 +12,7 @@ declare global {
} }
} }
export const verifyToken = (req: Request, res: Response, next: NextFunction) => { export const verifyToken = async (req: Request, res: Response, next: NextFunction) => {
try { try {
// Get token from Authorization header // Get token from Authorization header
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
@ -28,15 +29,15 @@ export const verifyToken = (req: Request, res: Response, next: NextFunction) =>
} }
// Verify token // Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key'); const { payload } = await jwtVerify(token, encodedJwtSecret);
// Add user info to request // Add user info to request
req.user = decoded; req.user = payload;
next(); next();
} catch (error) { } catch (error) {
if (error instanceof jwt.JsonWebTokenError) { if (error instanceof errors.JOSEError) {
next(new ApiError('Invalid Auth Credentials', 401)); next(new ApiError('Invalid Auth Credentials', 401));
} else { } else {
next(error); next(error);

View file

@ -1,7 +1,8 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import { jwtVerify, errors } from 'jose';
import { getStaffUserById } from '@/src/dbService'; import { getStaffUserById } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { encodedJwtSecret } from '@/src/lib/env-exporter';
// Extend Request interface to include staffUser // Extend Request interface to include staffUser
declare global { declare global {
@ -18,9 +19,10 @@ declare global {
/** /**
* Verify JWT token and extract payload * Verify JWT token and extract payload
*/ */
const verifyStaffToken = (token: string) => { const verifyStaffToken = async (token: string) => {
try { try {
return jwt.verify(token, process.env.JWT_SECRET || 'default-secret'); const { payload } = await jwtVerify(token, encodedJwtSecret);
return payload;
} catch (error) { } catch (error) {
throw new ApiError('Access denied. Invalid auth credentials', 401); throw new ApiError('Access denied. Invalid auth credentials', 401);
} }
@ -45,7 +47,7 @@ export const authenticateStaff = async (req: Request, res: Response, next: NextF
} }
// Verify token and extract payload // Verify token and extract payload
const decoded = verifyStaffToken(token) as any; const decoded = await verifyStaffToken(token) as any;
// Verify staffId exists in token // Verify staffId exists in token
if (!decoded.staffId) { if (!decoded.staffId) {

View file

@ -1,6 +1,8 @@
import redisClient from '@/src/lib/redis-client' // import redisClient from '@/src/lib/redis-client'
import { import {
getAllBannersForCache, getAllBannersForCache,
getBanners,
getBannerById as getBannerByIdFromDb,
type BannerData, type BannerData,
} from '@/src/dbService' } from '@/src/dbService'
import { scaffoldAssetUrl } from '@/src/lib/s3-client' import { scaffoldAssetUrl } from '@/src/lib/s3-client'
@ -34,22 +36,22 @@ export async function initializeBannerStore(): Promise<void> {
*/ */
// Store each banner in Redis // Store each banner in Redis
for (const banner of banners) { // for (const banner of banners) {
const signedImageUrl = banner.imageUrl // const signedImageUrl = banner.imageUrl
? scaffoldAssetUrl(banner.imageUrl) // ? scaffoldAssetUrl(banner.imageUrl)
: banner.imageUrl // : banner.imageUrl
//
const bannerObj: Banner = { // const bannerObj: Banner = {
id: banner.id, // id: banner.id,
name: banner.name, // name: banner.name,
imageUrl: signedImageUrl, // imageUrl: signedImageUrl,
serialNum: banner.serialNum, // serialNum: banner.serialNum,
productIds: banner.productIds, // productIds: banner.productIds,
createdAt: banner.createdAt, // createdAt: banner.createdAt,
} // }
//
await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj)) // await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj))
} // }
console.log('Banner store initialized successfully') console.log('Banner store initialized successfully')
} catch (error) { } catch (error) {
@ -59,10 +61,26 @@ export async function initializeBannerStore(): Promise<void> {
export async function getBannerById(id: number): Promise<Banner | null> { export async function getBannerById(id: number): Promise<Banner | null> {
try { try {
const key = `banner:${id}` // const key = `banner:${id}`
const data = await redisClient.get(key) // const data = await redisClient.get(key)
if (!data) return null // if (!data) return null
return JSON.parse(data) as Banner // return JSON.parse(data) as Banner
const banner = await getBannerByIdFromDb(id)
if (!banner) return null
const signedImageUrl = banner.imageUrl
? scaffoldAssetUrl(banner.imageUrl)
: banner.imageUrl
return {
id: banner.id,
name: banner.name,
imageUrl: signedImageUrl,
serialNum: banner.serialNum,
productIds: banner.productIds,
createdAt: banner.createdAt,
}
} catch (error) { } catch (error) {
console.error(`Error getting banner ${id}:`, error) console.error(`Error getting banner ${id}:`, error)
return null return null
@ -72,24 +90,41 @@ export async function getBannerById(id: number): Promise<Banner | null> {
export async function getAllBanners(): Promise<Banner[]> { export async function getAllBanners(): Promise<Banner[]> {
try { try {
// Get all keys matching the pattern "banner:*" // Get all keys matching the pattern "banner:*"
const keys = await redisClient.KEYS('banner:*') // const keys = await redisClient.KEYS('banner:*')
//
// if (keys.length === 0) return []
//
// // Get all banners using MGET for better performance
// const bannersData = await redisClient.MGET(keys)
//
// const banners: Banner[] = []
// for (const bannerData of bannersData) {
// if (bannerData) {
// banners.push(JSON.parse(bannerData) as Banner)
// }
// }
//
// // Sort by serialNum to maintain the same order as the original query
// banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0))
//
// return banners
if (keys.length === 0) return [] const banners = await getBanners()
// Get all banners using MGET for better performance return banners.map((banner) => {
const bannersData = await redisClient.MGET(keys) const signedImageUrl = banner.imageUrl
? scaffoldAssetUrl(banner.imageUrl)
: banner.imageUrl
const banners: Banner[] = [] return {
for (const bannerData of bannersData) { id: banner.id,
if (bannerData) { name: banner.name,
banners.push(JSON.parse(bannerData) as Banner) imageUrl: signedImageUrl,
serialNum: banner.serialNum,
productIds: banner.productIds,
createdAt: banner.createdAt,
} }
} })
// Sort by serialNum to maintain the same order as the original query
banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0))
return banners
} catch (error) { } catch (error) {
console.error('Error getting all banners:', error) console.error('Error getting all banners:', error)
return [] return []

View file

@ -1,10 +1,11 @@
import redisClient from '@/src/lib/redis-client' // import redisClient from '@/src/lib/redis-client'
import { import {
getAllProductsForCache, getAllProductsForCache,
getAllStoresForCache, getAllStoresForCache,
getAllDeliverySlotsForCache, getAllDeliverySlotsForCache,
getAllSpecialDealsForCache, getAllSpecialDealsForCache,
getAllProductTagsForCache, getAllProductTagsForCache,
getProductById as getProductByIdFromDb,
type ProductBasicData, type ProductBasicData,
type StoreBasicData, type StoreBasicData,
type DeliverySlotData, type DeliverySlotData,
@ -99,6 +100,178 @@ export async function initializeProducts(): Promise<void> {
} }
// Store each product in Redis // Store each product in Redis
// for (const product of productsData) {
// const signedImages = scaffoldAssetUrl(
// (product.images as string[]) || []
// )
// const store = product.storeId
// ? storeMap.get(product.storeId) || null
// : null
// const deliverySlots = deliverySlotsMap.get(product.id) || []
// const specialDeals = specialDealsMap.get(product.id) || []
// const productTags = productTagsMap.get(product.id) || []
//
// const productObj: Product = {
// id: product.id,
// name: product.name,
// shortDescription: product.shortDescription,
// longDescription: product.longDescription,
// price: product.price.toString(),
// marketPrice: product.marketPrice?.toString() || null,
// unitNotation: product.unitShortNotation,
// images: signedImages,
// isOutOfStock: product.isOutOfStock,
// store: store
// ? { id: store.id, name: store.name, description: store.description }
// : null,
// incrementStep: product.incrementStep,
// productQuantity: product.productQuantity,
// isFlashAvailable: product.isFlashAvailable,
// flashPrice: product.flashPrice?.toString() || null,
// deliverySlots: deliverySlots.map((s) => ({
// id: s.id,
// deliveryTime: s.deliveryTime,
// freezeTime: s.freezeTime,
// isCapacityFull: s.isCapacityFull,
// })),
// specialDeals: specialDeals.map((d) => ({
// quantity: d.quantity.toString(),
// price: d.price.toString(),
// validTill: d.validTill,
// })),
// productTags: productTags,
// }
//
// await redisClient.set(`product:${product.id}`, JSON.stringify(productObj))
// }
console.log('Product store initialized successfully')
} catch (error) {
console.error('Error initializing product store:', error)
}
}
export async function getProductById(id: number): Promise<Product | null> {
try {
// const key = `product:${id}`
// const data = await redisClient.get(key)
// if (!data) return null
// return JSON.parse(data) as Product
const product = await getProductByIdFromDb(id)
if (!product) return null
const signedImages = scaffoldAssetUrl(
(product.images as string[]) || []
)
// Fetch store info
const allStores = await getAllStoresForCache()
const store = product.storeId
? allStores.find(s => s.id === product.storeId) || null
: null
// Fetch delivery slots for this product
const allDeliverySlots = await getAllDeliverySlotsForCache()
const productSlots = allDeliverySlots.filter(s => s.productId === id)
// Fetch special deals for this product
const allSpecialDeals = await getAllSpecialDealsForCache()
const productDeals = allSpecialDeals.filter(d => d.productId === id)
// Fetch product tags for this product
const allProductTags = await getAllProductTagsForCache()
const productTagNames = allProductTags
.filter(t => t.productId === id)
.map(t => t.tagName)
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unit.shortNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: store
? { id: store.id, name: store.name, description: store.description }
: null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: productSlots.map((s) => ({
id: s.id,
deliveryTime: s.deliveryTime,
freezeTime: s.freezeTime,
isCapacityFull: s.isCapacityFull,
})),
specialDeals: productDeals.map((d) => ({
quantity: d.quantity.toString(),
price: d.price.toString(),
validTill: d.validTill,
})),
productTags: productTagNames,
}
} catch (error) {
console.error(`Error getting product ${id}:`, error)
return null
}
}
export async function getAllProducts(): Promise<Product[]> {
try {
// Get all keys matching the pattern "product:*"
// const keys = await redisClient.KEYS('product:*')
//
// if (keys.length === 0) {
// return []
// }
//
// // Get all products using MGET for better performance
// const productsData = await redisClient.MGET(keys)
//
// const products: Product[] = []
// for (const productData of productsData) {
// if (productData) {
// products.push(JSON.parse(productData) as Product)
// }
// }
//
// return products
const productsData = await getAllProductsForCache()
const allStores = await getAllStoresForCache()
const storeMap = new Map(allStores.map((s) => [s.id, s]))
const allDeliverySlots = await getAllDeliverySlotsForCache()
const deliverySlotsMap = new Map<number, DeliverySlotData[]>()
for (const slot of allDeliverySlots) {
if (!deliverySlotsMap.has(slot.productId))
deliverySlotsMap.set(slot.productId, [])
deliverySlotsMap.get(slot.productId)!.push(slot)
}
const allSpecialDeals = await getAllSpecialDealsForCache()
const specialDealsMap = new Map<number, SpecialDealData[]>()
for (const deal of allSpecialDeals) {
if (!specialDealsMap.has(deal.productId))
specialDealsMap.set(deal.productId, [])
specialDealsMap.get(deal.productId)!.push(deal)
}
const allProductTags = await getAllProductTagsForCache()
const productTagsMap = new Map<number, string[]>()
for (const tag of allProductTags) {
if (!productTagsMap.has(tag.productId))
productTagsMap.set(tag.productId, [])
productTagsMap.get(tag.productId)!.push(tag.tagName)
}
const products: Product[] = []
for (const product of productsData) { for (const product of productsData) {
const signedImages = scaffoldAssetUrl( const signedImages = scaffoldAssetUrl(
(product.images as string[]) || [] (product.images as string[]) || []
@ -110,7 +283,7 @@ export async function initializeProducts(): Promise<void> {
const specialDeals = specialDealsMap.get(product.id) || [] const specialDeals = specialDealsMap.get(product.id) || []
const productTags = productTagsMap.get(product.id) || [] const productTags = productTagsMap.get(product.id) || []
const productObj: Product = { products.push({
id: product.id, id: product.id,
name: product.name, name: product.name,
shortDescription: product.shortDescription, shortDescription: product.shortDescription,
@ -139,46 +312,7 @@ export async function initializeProducts(): Promise<void> {
validTill: d.validTill, validTill: d.validTill,
})), })),
productTags: productTags, productTags: productTags,
} })
await redisClient.set(`product:${product.id}`, JSON.stringify(productObj))
}
console.log('Product store initialized successfully')
} catch (error) {
console.error('Error initializing product store:', error)
}
}
export async function getProductById(id: number): Promise<Product | null> {
try {
const key = `product:${id}`
const data = await redisClient.get(key)
if (!data) return null
return JSON.parse(data) as Product
} catch (error) {
console.error(`Error getting product ${id}:`, error)
return null
}
}
export async function getAllProducts(): Promise<Product[]> {
try {
// Get all keys matching the pattern "product:*"
const keys = await redisClient.KEYS('product:*')
if (keys.length === 0) {
return []
}
// Get all products using MGET for better performance
const productsData = await redisClient.MGET(keys)
const products: Product[] = []
for (const productData of productsData) {
if (productData) {
products.push(JSON.parse(productData) as Product)
}
} }
return products return products

View file

@ -1,7 +1,9 @@
import redisClient from '@/src/lib/redis-client' // import redisClient from '@/src/lib/redis-client'
import { import {
getAllTagsForCache, getAllTagsForCache,
getAllTagProductMappings, getAllTagProductMappings,
getAllProductTags,
getProductTagById as getProductTagByIdFromDb,
type TagBasicData, type TagBasicData,
type TagProductMapping, type TagProductMapping,
} from '@/src/dbService' } from '@/src/dbService'
@ -18,6 +20,30 @@ interface Tag {
productIds: number[] productIds: number[]
} }
async function transformTagToStoreTag(tag: {
id: number
tagName: string
tagDescription: string | null
imageUrl: string | null
isDashboardTag: boolean
relatedStores: unknown
products?: Array<{ productId: number }>
}): Promise<Tag> {
const signedImageUrl = tag.imageUrl
? await generateSignedUrlFromS3Url(tag.imageUrl)
: null
return {
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription,
imageUrl: signedImageUrl,
isDashboardTag: tag.isDashboardTag,
relatedStores: (tag.relatedStores as number[]) || [],
productIds: tag.products ? tag.products.map(p => p.productId) : [],
}
}
export async function initializeProductTagStore(): Promise<void> { export async function initializeProductTagStore(): Promise<void> {
try { try {
console.log('Initializing product tag store in Redis...') console.log('Initializing product tag store in Redis...')
@ -70,23 +96,23 @@ export async function initializeProductTagStore(): Promise<void> {
} }
// Store each tag in Redis // Store each tag in Redis
for (const tag of tagsData) { // for (const tag of tagsData) {
const signedImageUrl = tag.imageUrl // const signedImageUrl = tag.imageUrl
? await generateSignedUrlFromS3Url(tag.imageUrl) // ? await generateSignedUrlFromS3Url(tag.imageUrl)
: null // : null
//
const tagObj: Tag = { // const tagObj: Tag = {
id: tag.id, // id: tag.id,
tagName: tag.tagName, // tagName: tag.tagName,
tagDescription: tag.tagDescription, // tagDescription: tag.tagDescription,
imageUrl: signedImageUrl, // imageUrl: signedImageUrl,
isDashboardTag: tag.isDashboardTag, // isDashboardTag: tag.isDashboardTag,
relatedStores: (tag.relatedStores as number[]) || [], // relatedStores: (tag.relatedStores as number[]) || [],
productIds: productIdsByTag.get(tag.id) || [], // productIds: productIdsByTag.get(tag.id) || [],
} // }
//
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj)) // await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj))
} // }
console.log('Product tag store initialized successfully') console.log('Product tag store initialized successfully')
} catch (error) { } catch (error) {
@ -96,10 +122,15 @@ export async function initializeProductTagStore(): Promise<void> {
export async function getTagById(id: number): Promise<Tag | null> { export async function getTagById(id: number): Promise<Tag | null> {
try { try {
const key = `tag:${id}` // const key = `tag:${id}`
const data = await redisClient.get(key) // const data = await redisClient.get(key)
if (!data) return null // if (!data) return null
return JSON.parse(data) as Tag // return JSON.parse(data) as Tag
const tag = await getProductTagByIdFromDb(id)
if (!tag) return null
return transformTagToStoreTag(tag)
} catch (error) { } catch (error) {
console.error(`Error getting tag ${id}:`, error) console.error(`Error getting tag ${id}:`, error)
return null return null
@ -109,23 +140,31 @@ export async function getTagById(id: number): Promise<Tag | null> {
export async function getAllTags(): Promise<Tag[]> { export async function getAllTags(): Promise<Tag[]> {
try { try {
// Get all keys matching the pattern "tag:*" // Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*') // const keys = await redisClient.KEYS('tag:*')
//
// if (keys.length === 0) {
// return []
// }
//
// // Get all tags using MGET for better performance
// const tagsData = await redisClient.MGET(keys)
//
// const tags: Tag[] = []
// for (const tagData of tagsData) {
// if (tagData) {
// tags.push(JSON.parse(tagData) as Tag)
// }
// }
//
// return tags
if (keys.length === 0) { const tags = await getAllProductTags()
return []
const result: Tag[] = []
for (const tag of tags) {
result.push(await transformTagToStoreTag(tag))
} }
return result
// Get all tags using MGET for better performance
const tagsData = await redisClient.MGET(keys)
const tags: Tag[] = []
for (const tagData of tagsData) {
if (tagData) {
tags.push(JSON.parse(tagData) as Tag)
}
}
return tags
} catch (error) { } catch (error) {
console.error('Error getting all tags:', error) console.error('Error getting all tags:', error)
return [] return []
@ -135,26 +174,36 @@ export async function getAllTags(): Promise<Tag[]> {
export async function getDashboardTags(): Promise<Tag[]> { export async function getDashboardTags(): Promise<Tag[]> {
try { try {
// Get all keys matching the pattern "tag:*" // Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*') // const keys = await redisClient.KEYS('tag:*')
//
// if (keys.length === 0) {
// return []
// }
//
// // Get all tags using MGET for better performance
// const tagsData = await redisClient.MGET(keys)
//
// const dashboardTags: Tag[] = []
// for (const tagData of tagsData) {
// if (tagData) {
// const tag = JSON.parse(tagData) as Tag
// if (tag.isDashboardTag) {
// dashboardTags.push(tag)
// }
// }
// }
//
// return dashboardTags
if (keys.length === 0) { const tags = await getAllProductTags()
return []
}
// Get all tags using MGET for better performance const result: Tag[] = []
const tagsData = await redisClient.MGET(keys) for (const tag of tags) {
if (tag.isDashboardTag) {
const dashboardTags: Tag[] = [] result.push(await transformTagToStoreTag(tag))
for (const tagData of tagsData) {
if (tagData) {
const tag = JSON.parse(tagData) as Tag
if (tag.isDashboardTag) {
dashboardTags.push(tag)
}
} }
} }
return result
return dashboardTags
} catch (error) { } catch (error) {
console.error('Error getting dashboard tags:', error) console.error('Error getting dashboard tags:', error)
return [] return []
@ -164,26 +213,37 @@ export async function getDashboardTags(): Promise<Tag[]> {
export async function getTagsByStoreId(storeId: number): Promise<Tag[]> { export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
try { try {
// Get all keys matching the pattern "tag:*" // Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*') // const keys = await redisClient.KEYS('tag:*')
//
// if (keys.length === 0) {
// return []
// }
//
// // Get all tags using MGET for better performance
// const tagsData = await redisClient.MGET(keys)
//
// const storeTags: Tag[] = []
// for (const tagData of tagsData) {
// if (tagData) {
// const tag = JSON.parse(tagData) as Tag
// if (tag.relatedStores.includes(storeId)) {
// storeTags.push(tag)
// }
// }
// }
//
// return storeTags
if (keys.length === 0) { const tags = await getAllProductTags()
return []
}
// Get all tags using MGET for better performance const result: Tag[] = []
const tagsData = await redisClient.MGET(keys) for (const tag of tags) {
const relatedStores = (tag.relatedStores as number[]) || []
const storeTags: Tag[] = [] if (relatedStores.includes(storeId)) {
for (const tagData of tagsData) { result.push(await transformTagToStoreTag(tag))
if (tagData) {
const tag = JSON.parse(tagData) as Tag
if (tag.relatedStores.includes(storeId)) {
storeTags.push(tag)
}
} }
} }
return result
return storeTags
} catch (error) { } catch (error) {
console.error(`Error getting tags for store ${storeId}:`, error) console.error(`Error getting tags for store ${storeId}:`, error)
return [] return []

View file

@ -1,4 +1,4 @@
import redisClient from '@/src/lib/redis-client' // import redisClient from '@/src/lib/redis-client'
import { import {
getAllSlotsWithProductsForCache, getAllSlotsWithProductsForCache,
type SlotWithProductsData, type SlotWithProductsData,
@ -35,6 +35,45 @@ interface SlotInfo {
isCapacityFull: boolean isCapacityFull: boolean
} }
async function transformSlotToStoreSlot(slot: SlotWithProductsData): Promise<SlotWithProducts> {
return {
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isCapacityFull: slot.isCapacityFull,
products: slot.productSlots.map((productSlot) => ({
id: productSlot.product.id,
name: productSlot.product.name,
productQuantity: productSlot.product.productQuantity,
shortDescription: productSlot.product.shortDescription,
price: productSlot.product.price.toString(),
marketPrice: productSlot.product.marketPrice?.toString() || null,
unit: productSlot.product.unit?.shortNotation || null,
images: scaffoldAssetUrl(
(productSlot.product.images as string[]) || []
),
isOutOfStock: productSlot.product.isOutOfStock,
storeId: productSlot.product.storeId,
nextDeliveryDate: slot.deliveryTime,
})),
}
}
function extractSlotInfo(slot: SlotWithProductsData): SlotInfo {
return {
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isCapacityFull: slot.isCapacityFull,
}
}
async function fetchAllTransformedSlots(): Promise<SlotWithProducts[]> {
const slots = await getAllSlotsWithProductsForCache()
return Promise.all(slots.map(transformSlotToStoreSlot))
}
export async function initializeSlotStore(): Promise<void> { export async function initializeSlotStore(): Promise<void> {
try { try {
console.log('Initializing slot store in Redis...') console.log('Initializing slot store in Redis...')
@ -99,9 +138,9 @@ export async function initializeSlotStore(): Promise<void> {
) )
// Store each slot in Redis with key pattern "slot:{id}" // Store each slot in Redis with key pattern "slot:{id}"
for (const slot of slotsWithProducts) { // for (const slot of slotsWithProducts) {
await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot)) // await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot))
} // }
// Build and store product-slots map // Build and store product-slots map
// Group slots by productId // Group slots by productId
@ -122,12 +161,12 @@ export async function initializeSlotStore(): Promise<void> {
} }
// Store each product's slots in Redis with key pattern "product:{id}:slots" // Store each product's slots in Redis with key pattern "product:{id}:slots"
for (const [productId, slotInfos] of Object.entries(productSlotsMap)) { // for (const [productId, slotInfos] of Object.entries(productSlotsMap)) {
await redisClient.set( // await redisClient.set(
`product:${productId}:slots`, // `product:${productId}:slots`,
JSON.stringify(slotInfos) // JSON.stringify(slotInfos)
) // )
} // }
console.log('Slot store initialized successfully') console.log('Slot store initialized successfully')
} catch (error) { } catch (error) {
@ -137,10 +176,16 @@ export async function initializeSlotStore(): Promise<void> {
export async function getSlotById(slotId: number): Promise<SlotWithProducts | null> { export async function getSlotById(slotId: number): Promise<SlotWithProducts | null> {
try { try {
const key = `slot:${slotId}` // const key = `slot:${slotId}`
const data = await redisClient.get(key) // const data = await redisClient.get(key)
if (!data) return null // if (!data) return null
return JSON.parse(data) as SlotWithProducts // return JSON.parse(data) as SlotWithProducts
const slots = await getAllSlotsWithProductsForCache()
const slot = slots.find(s => s.id === slotId)
if (!slot) return null
return transformSlotToStoreSlot(slot)
} catch (error) { } catch (error) {
console.error(`Error getting slot ${slotId}:`, error) console.error(`Error getting slot ${slotId}:`, error)
return null return null
@ -150,21 +195,23 @@ export async function getSlotById(slotId: number): Promise<SlotWithProducts | nu
export async function getAllSlots(): Promise<SlotWithProducts[]> { export async function getAllSlots(): Promise<SlotWithProducts[]> {
try { try {
// Get all keys matching the pattern "slot:*" // Get all keys matching the pattern "slot:*"
const keys = await redisClient.KEYS('slot:*') // const keys = await redisClient.KEYS('slot:*')
//
// if (keys.length === 0) return []
//
// // Get all slots using MGET for better performance
// const slotsData = await redisClient.MGET(keys)
//
// const slots: SlotWithProducts[] = []
// for (const slotData of slotsData) {
// if (slotData) {
// slots.push(JSON.parse(slotData) as SlotWithProducts)
// }
// }
//
// return slots
if (keys.length === 0) return [] return fetchAllTransformedSlots()
// Get all slots using MGET for better performance
const slotsData = await redisClient.MGET(keys)
const slots: SlotWithProducts[] = []
for (const slotData of slotsData) {
if (slotData) {
slots.push(JSON.parse(slotData) as SlotWithProducts)
}
}
return slots
} catch (error) { } catch (error) {
console.error('Error getting all slots:', error) console.error('Error getting all slots:', error)
return [] return []
@ -173,10 +220,22 @@ export async function getAllSlots(): Promise<SlotWithProducts[]> {
export async function getProductSlots(productId: number): Promise<SlotInfo[]> { export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
try { try {
const key = `product:${productId}:slots` // const key = `product:${productId}:slots`
const data = await redisClient.get(key) // const data = await redisClient.get(key)
if (!data) return [] // if (!data) return []
return JSON.parse(data) as SlotInfo[] // return JSON.parse(data) as SlotInfo[]
const slots = await getAllSlotsWithProductsForCache()
const productSlots: SlotInfo[] = []
for (const slot of slots) {
const hasProduct = slot.productSlots.some(ps => ps.product.id === productId)
if (hasProduct) {
productSlots.push(extractSlotInfo(slot))
}
}
return productSlots
} catch (error) { } catch (error) {
console.error(`Error getting slots for product ${productId}:`, error) console.error(`Error getting slots for product ${productId}:`, error)
return [] return []
@ -186,23 +245,39 @@ export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> { export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> {
try { try {
// Get all keys matching the pattern "product:*:slots" // Get all keys matching the pattern "product:*:slots"
const keys = await redisClient.KEYS('product:*:slots') // const keys = await redisClient.KEYS('product:*:slots')
//
if (keys.length === 0) return {} // if (keys.length === 0) return {}
//
// Get all product slots using MGET for better performance // // Get all product slots using MGET for better performance
const productsData = await redisClient.MGET(keys) // const productsData = await redisClient.MGET(keys)
//
// const result: Record<number, SlotInfo[]> = {}
// for (const key of keys) {
// // Extract productId from key "product:{id}:slots"
// const match = key.match(/product:(\d+):slots/)
// if (match) {
// const productId = parseInt(match[1], 10)
// const dataIndex = keys.indexOf(key)
// if (productsData[dataIndex]) {
// result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[]
// }
// }
// }
//
// return result
const slots = await getAllSlotsWithProductsForCache()
const result: Record<number, SlotInfo[]> = {} const result: Record<number, SlotInfo[]> = {}
for (const key of keys) {
// Extract productId from key "product:{id}:slots" for (const slot of slots) {
const match = key.match(/product:(\d+):slots/) const slotInfo = extractSlotInfo(slot)
if (match) { for (const productSlot of slot.productSlots) {
const productId = parseInt(match[1], 10) const productId = productSlot.product.id
const dataIndex = keys.indexOf(key) if (!result[productId]) {
if (productsData[dataIndex]) { result[productId] = []
result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[]
} }
result[productId].push(slotInfo)
} }
} }
@ -220,18 +295,38 @@ export async function getMultipleProductsSlots(
if (productIds.length === 0) return {} if (productIds.length === 0) return {}
// Build keys for all productIds // Build keys for all productIds
const keys = productIds.map((id) => `product:${id}:slots`) // const keys = productIds.map((id) => `product:${id}:slots`)
//
// Use MGET for batch retrieval // // Use MGET for batch retrieval
const productsData = await redisClient.MGET(keys) // const productsData = await redisClient.MGET(keys)
//
// const result: Record<number, SlotInfo[]> = {}
// for (let i = 0; i < productIds.length; i++) {
// const data = productsData[i]
// if (data) {
// const slots = JSON.parse(data) as SlotInfo[]
// // Filter out slots that are at full capacity
// result[productIds[i]] = slots.filter((slot) => !slot.isCapacityFull)
// }
// }
//
// return result
const slots = await getAllSlotsWithProductsForCache()
const productIdSet = new Set(productIds)
const result: Record<number, SlotInfo[]> = {} const result: Record<number, SlotInfo[]> = {}
for (let i = 0; i < productIds.length; i++) {
const data = productsData[i] for (const productId of productIds) {
if (data) { result[productId] = []
const slots = JSON.parse(data) as SlotInfo[] }
// Filter out slots that are at full capacity
result[productIds[i]] = slots.filter((slot) => !slot.isCapacityFull) for (const slot of slots) {
const slotInfo = extractSlotInfo(slot)
for (const productSlot of slot.productSlots) {
const pid = productSlot.product.id
if (productIdSet.has(pid) && !slot.isCapacityFull) {
result[pid].push(slotInfo)
}
} }
} }

View file

@ -1,4 +1,4 @@
import redisClient from '@/src/lib/redis-client' // import redisClient from '@/src/lib/redis-client'
import { import {
getAllUserNegativityScores as getAllUserNegativityScoresFromDb, getAllUserNegativityScores as getAllUserNegativityScoresFromDb,
getUserNegativityScore as getUserNegativityScoreFromDb, getUserNegativityScore as getUserNegativityScoreFromDb,
@ -26,12 +26,12 @@ export async function initializeUserNegativityStore(): Promise<void> {
.groupBy(userIncidents.userId); .groupBy(userIncidents.userId);
*/ */
for (const { userId, totalNegativityScore } of results) { // for (const { userId, totalNegativityScore } of results) {
await redisClient.set( // await redisClient.set(
`user:negativity:${userId}`, // `user:negativity:${userId}`,
totalNegativityScore.toString() // totalNegativityScore.toString()
) // )
} // }
console.log(`User negativity store initialized for ${results.length} users`) console.log(`User negativity store initialized for ${results.length} users`)
} catch (error) { } catch (error) {
@ -42,14 +42,16 @@ export async function initializeUserNegativityStore(): Promise<void> {
export async function getUserNegativity(userId: number): Promise<number> { export async function getUserNegativity(userId: number): Promise<number> {
try { try {
const key = `user:negativity:${userId}` // const key = `user:negativity:${userId}`
const data = await redisClient.get(key) // const data = await redisClient.get(key)
//
// if (!data) {
// return 0
// }
//
// return parseInt(data, 10)
if (!data) { return await getUserNegativityScoreFromDb(userId)
return 0
}
return parseInt(data, 10)
} catch (error) { } catch (error) {
console.error(`Error getting negativity score for user ${userId}:`, error) console.error(`Error getting negativity score for user ${userId}:`, error)
return 0 return 0
@ -58,24 +60,32 @@ export async function getUserNegativity(userId: number): Promise<number> {
export async function getAllUserNegativityScores(): Promise<Record<number, number>> { export async function getAllUserNegativityScores(): Promise<Record<number, number>> {
try { try {
const keys = await redisClient.KEYS('user:negativity:*') // const keys = await redisClient.KEYS('user:negativity:*')
//
// if (keys.length === 0) return {}
//
// const values = await redisClient.MGET(keys)
//
// const result: Record<number, number> = {}
// for (let i = 0; i < keys.length; i++) {
// const key = keys[i]
// const value = values[i]
//
// const match = key.match(/user:negativity:(\d+)/)
// if (match && value) {
// const userId = parseInt(match[1], 10)
// result[userId] = parseInt(value, 10)
// }
// }
//
// return result
if (keys.length === 0) return {} const results = await getAllUserNegativityScoresFromDb()
const values = await redisClient.MGET(keys)
const result: Record<number, number> = {} const result: Record<number, number> = {}
for (let i = 0; i < keys.length; i++) { for (const { userId, totalNegativityScore } of results) {
const key = keys[i] result[userId] = totalNegativityScore
const value = values[i]
const match = key.match(/user:negativity:(\d+)/)
if (match && value) {
const userId = parseInt(match[1], 10)
result[userId] = parseInt(value, 10)
}
} }
return result return result
} catch (error) { } catch (error) {
console.error('Error getting all user negativity scores:', error) console.error('Error getting all user negativity scores:', error)
@ -89,20 +99,26 @@ export async function getMultipleUserNegativityScores(
try { try {
if (userIds.length === 0) return {} if (userIds.length === 0) return {}
const keys = userIds.map((id) => `user:negativity:${id}`) // const keys = userIds.map((id) => `user:negativity:${id}`)
//
const values = await redisClient.MGET(keys) // const values = await redisClient.MGET(keys)
//
// const result: Record<number, number> = {}
// for (let i = 0; i < userIds.length; i++) {
// const value = values[i]
// if (value) {
// result[userIds[i]] = parseInt(value, 10)
// } else {
// result[userIds[i]] = 0
// }
// }
//
// return result
const result: Record<number, number> = {} const result: Record<number, number> = {}
for (let i = 0; i < userIds.length; i++) { for (const userId of userIds) {
const value = values[i] result[userId] = await getUserNegativityScoreFromDb(userId)
if (value) {
result[userIds[i]] = parseInt(value, 10)
} else {
result[userIds[i]] = 0
}
} }
return result return result
} catch (error) { } catch (error) {
console.error('Error getting multiple user negativity scores:', error) console.error('Error getting multiple user negativity scores:', error)
@ -131,8 +147,8 @@ export async function recomputeUserNegativityScore(userId: number): Promise<void
const totalScore = result?.totalNegativityScore || 0; const totalScore = result?.totalNegativityScore || 0;
*/ */
const key = `user:negativity:${userId}` // const key = `user:negativity:${userId}`
await redisClient.set(key, totalScore.toString()) // await redisClient.set(key, totalScore.toString())
} catch (error) { } catch (error) {
console.error(`Error recomputing negativity score for user ${userId}:`, error) console.error(`Error recomputing negativity score for user ${userId}:`, error)
throw error throw error

View file

@ -3,8 +3,8 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "@/src/lib/api-error" import { ApiError } from "@/src/lib/api-error"
import { appUrl } from "@/src/lib/env-exporter" import { appUrl } from "@/src/lib/env-exporter"
import redisClient from "@/src/lib/redis-client" // import redisClient from "@/src/lib/redis-client"
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters" // import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer' import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import { import {
getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb, getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb,
@ -595,21 +595,21 @@ export const slotsRouter = router({
const { id } = input; const { id } = input;
const slotId = parseInt(id); const slotId = parseInt(id);
const cacheKey = getSlotSequenceKey(slotId); // const cacheKey = getSlotSequenceKey(slotId);
try { // try {
const cached = await redisClient.get(cacheKey); // const cached = await redisClient.get(cacheKey);
if (cached) { // if (cached) {
const parsed = JSON.parse(cached); // const parsed = JSON.parse(cached);
const validated = cachedSequenceSchema.parse(parsed); // const validated = cachedSequenceSchema.parse(parsed);
console.log('sending cached response') // console.log('sending cached response')
//
return { deliverySequence: validated }; // return { deliverySequence: validated };
} // }
} catch (error) { // } catch (error) {
console.warn('Redis cache read/validation failed, falling back to DB:', error); // console.warn('Redis cache read/validation failed, falling back to DB:', error);
// Continue to DB fallback // // Continue to DB fallback
} // }
// Fallback to DB // Fallback to DB
const slot = await getSlotDeliverySequenceInDb(slotId) const slot = await getSlotDeliverySequenceInDb(slotId)
@ -634,12 +634,12 @@ export const slotsRouter = router({
const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence; const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
// Cache the validated result // Cache the validated result
try { // try {
const validated = cachedSequenceSchema.parse(sequence); // const validated = cachedSequenceSchema.parse(sequence);
await redisClient.set(cacheKey, JSON.stringify(validated), 3600); // await redisClient.set(cacheKey, JSON.stringify(validated), 3600);
} catch (cacheError) { // } catch (cacheError) {
console.warn('Redis cache write failed:', cacheError); // console.warn('Redis cache write failed:', cacheError);
} // }
return { deliverySequence: sequence } return { deliverySequence: sequence }
}), }),
@ -676,13 +676,13 @@ export const slotsRouter = router({
} }
// Cache the updated sequence // Cache the updated sequence
const cacheKey = getSlotSequenceKey(id); // const cacheKey = getSlotSequenceKey(id);
try { // try {
const validated = cachedSequenceSchema.parse(deliverySequence); // const validated = cachedSequenceSchema.parse(deliverySequence);
await redisClient.set(cacheKey, JSON.stringify(validated), 3600); // await redisClient.set(cacheKey, JSON.stringify(validated), 3600);
} catch (cacheError) { // } catch (cacheError) {
console.warn('Redis cache write failed:', cacheError); // console.warn('Redis cache write failed:', cacheError);
} // }
return { return {
slot: updatedSlot, slot: updatedSlot,

View file

@ -1,7 +1,8 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken'; import { SignJWT } from 'jose';
import { encodedJwtSecret } from '@/src/lib/env-exporter';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { import {
getStaffUserByName, getStaffUserByName,
@ -40,11 +41,10 @@ export const staffUserRouter = router({
throw new ApiError('Invalid credentials', 401); throw new ApiError('Invalid credentials', 401);
} }
const token = jwt.sign( const token = await new SignJWT({ staffId: staff.id, name: staff.name })
{ staffId: staff.id, name: staff.name }, .setProtectedHeader({ alg: 'HS256' })
process.env.JWT_SECRET || 'default-secret', .setExpirationTime('30d')
{ expiresIn: '30d' } .sign(encodedJwtSecret);
);
return { return {
message: 'Login successful', message: 'Login successful',

View file

@ -1,10 +1,10 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod' import { z } from 'zod'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken' import { SignJWT } from 'jose';
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { jwtSecret } from '@/src/lib/env-exporter' import { encodedJwtSecret } from '@/src/lib/env-exporter'
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils' import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'
import { import {
getUserAuthByEmail as getUserAuthByEmailInDb, getUserAuthByEmail as getUserAuthByEmailInDb,
@ -41,13 +41,11 @@ interface RegisterRequest {
profileImageUrl?: string | null; profileImageUrl?: string | null;
} }
const generateToken = (userId: number): string => { const generateToken = async (userId: number): Promise<string> => {
const secret = jwtSecret; return await new SignJWT({ userId })
if (!secret) { .setProtectedHeader({ alg: 'HS256' })
throw new ApiError('JWT secret not configured', 500); .setExpirationTime('7d')
} .sign(encodedJwtSecret);
return jwt.sign({ userId }, secret, { expiresIn: '7d' });
}; };
@ -100,7 +98,7 @@ export const authRouter = router({
throw new ApiError('Invalid credentials', 401); throw new ApiError('Invalid credentials', 401);
} }
const token = generateToken(foundUser.id); const token = await generateToken(foundUser.id);
const response: UserAuthResponse = { const response: UserAuthResponse = {
token, token,
@ -177,7 +175,7 @@ export const authRouter = router({
profileImage: profileImageUrl ?? null, profileImage: profileImageUrl ?? null,
}) })
const token = generateToken(newUser.id); const token = await generateToken(newUser.id);
const profileImageSignedUrl = profileImageUrl const profileImageSignedUrl = profileImageUrl
? await generateSignedUrlFromS3Url(profileImageUrl) ? await generateSignedUrlFromS3Url(profileImageUrl)
@ -235,7 +233,7 @@ export const authRouter = router({
} }
// Generate JWT // Generate JWT
const token = generateToken(user.id); const token = await generateToken(user.id);
return { return {
success: true, success: true,

View file

@ -1,8 +1,8 @@
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'
import jwt from 'jsonwebtoken' import { SignJWT } from 'jose'
import { z } from 'zod' import { z } from 'zod'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { jwtSecret } from '@/src/lib/env-exporter' import { encodedJwtSecret } from '@/src/lib/env-exporter'
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { import {
getUserProfileById as getUserProfileByIdInDb, getUserProfileById as getUserProfileByIdInDb,
@ -19,13 +19,11 @@ import type {
UserSavePushTokenResponse, UserSavePushTokenResponse,
} from '@packages/shared' } from '@packages/shared'
const generateToken = (userId: number): string => { const generateToken = async (userId: number): Promise<string> => {
const secret = jwtSecret; return await new SignJWT({ userId })
if (!secret) { .setProtectedHeader({ alg: 'HS256' })
throw new ApiError('JWT secret not configured', 500); .setExpirationTime('7d')
} .sign(encodedJwtSecret);
return jwt.sign({ userId }, secret, { expiresIn: '7d' });
}; };
export const userRouter = router({ export const userRouter = router({

19174
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff