From bda01f9c4124b26ff0600e68b278f0a404d69c6d Mon Sep 17 00:00:00 2001 From: Gandrothu Naga Sai Rishitha Date: Fri, 27 Feb 2026 21:04:30 +0530 Subject: [PATCH] feat: extend order listing with date range and sorting filters --- backend/db.js | 16 ++++++++++--- backend/env.js | 61 +---------------------------------------------- backend/server.js | 48 +++++++++++++++++++++---------------- 3 files changed, 42 insertions(+), 83 deletions(-) diff --git a/backend/db.js b/backend/db.js index 34e5835..40b1f95 100644 --- a/backend/db.js +++ b/backend/db.js @@ -214,11 +214,21 @@ export const database = { })); }, - getOrders({ spotId, userId }) { + // UPDATED: now supports from, to (date range on createdAt) and sort (asc/desc) + getOrders({ spotId, userId, from, to, sort = 'desc' }) { + const fromTime = from ? new Date(from).getTime() : null; + const toTime = to ? new Date(to).getTime() : null; + const orders = state.orders .filter((order) => !spotId || order.spot_id === spotId) .filter((order) => !userId || order.user_id === userId) - .sort((a, b) => b.created_at.localeCompare(a.created_at)); + .filter((order) => !fromTime || new Date(order.created_at).getTime() >= fromTime) + .filter((order) => !toTime || new Date(order.created_at).getTime() <= toTime) + .sort((a, b) => + sort === 'asc' + ? a.created_at.localeCompare(b.created_at) + : b.created_at.localeCompare(a.created_at) + ); return orders.map((order) => { const orderItems = state.order_items.filter((item) => item.order_id === order.id); @@ -356,4 +366,4 @@ export const database = { }, }; -export { dbPath }; +export { dbPath }; \ No newline at end of file diff --git a/backend/env.js b/backend/env.js index 85af5e4..e94917a 100644 --- a/backend/env.js +++ b/backend/env.js @@ -40,50 +40,6 @@ const loadDotEnv = () => { } process.env[parsed.key] = parsed.value; -import dotenv from 'dotenv'; -import { z } from 'zod'; - -dotenv.config(); - -const envSchema = z.object({ - VITE_SUPABASE_URL: z.string().url(), - VITE_SUPABASE_ANON_KEY: z.string().min(10), - PORT: z.string().optional(), - AUTH_TOKEN_SECRET: z.string().min(16).optional(), - AUTH_TOKEN_TTL_SECONDS: z.string().regex(/^\d+$/).optional(), - CORS_ALLOW_ORIGIN: z.string().optional(), - LOGIN_RATE_LIMIT_MAX_ATTEMPTS: z.string().regex(/^\d+$/).optional(), - LOGIN_RATE_LIMIT_WINDOW_MS: z.string().regex(/^\d+$/).optional(), - LOGIN_RATE_LIMIT_BLOCK_MS: z.string().regex(/^\d+$/).optional(), - GLOBAL_RATE_LIMIT_MAX_REQUESTS: z.string().regex(/^\d+$/).optional(), - GLOBAL_RATE_LIMIT_WINDOW_MS: z.string().regex(/^\d+$/).optional(), - SECURITY_HEADERS_CSP: z.string().optional(), - DATABASE_URL: z.string().optional(), - REDIS_URL: z.string().optional(), - UPSTASH_REDIS_REST_URL: z.string().optional(), - UPSTASH_REDIS_REST_TOKEN: z.string().optional(), - STORAGE_DRIVER: z.enum(['s3', 'cloudinary', 'local']).optional(), - AWS_REGION: z.string().optional(), - AWS_S3_BUCKET: z.string().optional(), - AWS_ACCESS_KEY_ID: z.string().optional(), - AWS_SECRET_ACCESS_KEY: z.string().optional(), - CLOUDINARY_CLOUD_NAME: z.string().optional(), - CLOUDINARY_API_KEY: z.string().optional(), - CLOUDINARY_API_SECRET: z.string().optional(), - REDIS_URL: z.string().url().optional(), - REDIS_KEY_PREFIX: z.string().optional(), - CACHE_DEFAULT_TTL_SECONDS: z.string().regex(/^\d+$/).optional(), - PRESENCE_TTL_SECONDS: z.string().regex(/^\d+$/).optional(), - EVENT_STATE_DEFAULT_TTL_SECONDS: z.string().regex(/^\d+$/).optional(), -}); - -const result = envSchema.safeParse(process.env); - -if (!result.success) { - console.error('\n❌ Invalid environment configuration:\n'); - - result.error.errors.forEach((err) => { - console.error(`- ${err.path.join('.')}: ${err.message}`); }); }; @@ -91,21 +47,6 @@ const isIntegerString = (value) => typeof value === 'string' && /^\d+$/.test(val const validateEnv = () => { const errors = []; - const { VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY } = process.env; - - if (!VITE_SUPABASE_URL) { - errors.push('VITE_SUPABASE_URL is required'); - } else { - try { - new URL(VITE_SUPABASE_URL); - } catch { - errors.push('VITE_SUPABASE_URL must be a valid URL'); - } - } - - if (!VITE_SUPABASE_ANON_KEY || VITE_SUPABASE_ANON_KEY.length < 10) { - errors.push('VITE_SUPABASE_ANON_KEY is required and must be at least 10 characters'); - } const numericKeys = [ 'AUTH_TOKEN_TTL_SECONDS', @@ -136,4 +77,4 @@ const validateEnv = () => { loadDotEnv(); validateEnv(); -export default process.env; +export default process.env; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index a7dbef9..6823f81 100644 --- a/backend/server.js +++ b/backend/server.js @@ -50,7 +50,6 @@ const getRateLimitState = (key) => { return existing; }; - const getGlobalRateLimitState = (key) => { const now = Date.now(); const existing = globalRequests.get(key); @@ -168,7 +167,6 @@ const recordFailedLoginAttempt = (key) => { }; const sendJson = (res, statusCode, body, extraHeaders = {}) => { -const sendJson = (res, statusCode, body) => { res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': CORS_ALLOW_ORIGIN, @@ -287,19 +285,6 @@ const server = createServer(async (req, res) => { } const loginKey = getLoginKey(req, username); - const rateLimitState = getRateLimitState(loginKey); - const now = Date.now(); - if (rateLimitState.blockedUntil > now) { - const retryAfterSeconds = Math.ceil((rateLimitState.blockedUntil - now) / 1000); - sendJson( - res, - 429, - { - error: 'Too many failed login attempts. Please try again later.', - retryAfterSeconds, - }, - { 'Retry-After': String(Math.max(retryAfterSeconds, 1)) } - ); const retryAfterSeconds = await rateLimiter.getBlockedSeconds(loginKey); if (retryAfterSeconds > 0) { sendJson(res, 429, { @@ -372,6 +357,7 @@ const server = createServer(async (req, res) => { return; } + // UPDATED: supports from, to (date range) and sort (asc/desc) query params if (method === 'GET' && path === '/api/orders') { const authedUser = await getUserFromAuthHeader(req.headers.authorization); if (!authedUser) { @@ -381,6 +367,24 @@ const server = createServer(async (req, res) => { const spotId = parsedUrl.searchParams.get('spotId'); const userId = parsedUrl.searchParams.get('userId'); + const from = parsedUrl.searchParams.get('from'); + const to = parsedUrl.searchParams.get('to'); + const sort = parsedUrl.searchParams.get('sort') ?? 'desc'; + + if (!['asc', 'desc'].includes(sort)) { + sendJson(res, 400, { error: "Invalid sort value. Must be 'asc' or 'desc'." }); + return; + } + + if (from && isNaN(Date.parse(from))) { + sendJson(res, 400, { error: "Invalid 'from' date. Use ISO 8601 format (e.g. 2025-01-01)." }); + return; + } + + if (to && isNaN(Date.parse(to))) { + sendJson(res, 400, { error: "Invalid 'to' date. Use ISO 8601 format (e.g. 2025-12-31)." }); + return; + } if (authedUser.role !== 'admin' && userId && userId !== authedUser.id) { sendJson(res, 403, { error: 'Forbidden' }); @@ -389,10 +393,11 @@ const server = createServer(async (req, res) => { const effectiveUserId = authedUser.role === 'admin' ? userId : authedUser.id; - const orders = database.getOrders({ spotId, userId: effectiveUserId }); + const orders = database.getOrders({ spotId, userId: effectiveUserId, from, to, sort }); sendJson(res, 200, orders); return; } + if (method === 'GET' && path.startsWith('/api/orders/')) { const orderId = path.replace('/api/orders/', ''); @@ -507,7 +512,7 @@ const server = createServer(async (req, res) => { } if (method === 'POST' && path === '/api/jobs/reminders/run') { - const authedUser = getUserFromAuthHeader(req.headers.authorization); + const authedUser = await getUserFromAuthHeader(req.headers.authorization); if (!authedUser || authedUser.role !== 'admin') { sendJson(res, 403, { error: 'Forbidden' }); return; @@ -521,7 +526,7 @@ const server = createServer(async (req, res) => { } if (method === 'POST' && path === '/api/jobs/cleanup/run') { - const authedUser = getUserFromAuthHeader(req.headers.authorization); + const authedUser = await getUserFromAuthHeader(req.headers.authorization); if (!authedUser || authedUser.role !== 'admin') { sendJson(res, 403, { error: 'Forbidden' }); return; @@ -529,6 +534,9 @@ const server = createServer(async (req, res) => { await jobSystem.enqueueExpiredSpotCleanup(); sendJson(res, 202, { accepted: true, jobsEnabled: jobSystem.enabled }); + return; + } + if (method === 'POST' && path === '/api/presence/heartbeat') { const authedUser = await getUserFromAuthHeader(req.headers.authorization); if (!authedUser) { @@ -606,6 +614,7 @@ server.listen(port, () => { console.log(`Backend API running on http://localhost:${port}`); console.log(`Using local database at: ${dbPath}`); console.log(`Swagger docs available at http://localhost:${port}/api/docs`); + console.log(`Cache mode: ${cache.mode}`); }); process.on('SIGINT', async () => { @@ -616,5 +625,4 @@ process.on('SIGINT', async () => { process.on('SIGTERM', async () => { await jobSystem.shutdown(); process.exit(0); - console.log(`Cache mode: ${cache.mode}`); -}); +}); \ No newline at end of file