Skip to content

Commit 444c0d3

Browse files
committed
feat: Enhance date handling by implementing timezone utilities for consistent IST management across components and services
1 parent 60f6513 commit 444c0d3

File tree

7 files changed

+243
-173
lines changed

7 files changed

+243
-173
lines changed

client-test/src/components/Admin/datepicker.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { motion, AnimatePresence } from "framer-motion";
44
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react";
55
import { Button } from "@/components/ui/button";
66
import { Label } from "@/components/ui/label";
7+
import { isPastDateIST, istToUTC } from "@/lib/timezone";
78

89
interface DatePickerProps {
910
selectedDate: Date;
@@ -52,12 +53,7 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, onChange, label,
5253

5354
// Check if date is in the past (comparing against IST midnight)
5455
const isPastDate = (date: Date) => {
55-
const today = new Date();
56-
// Get current IST date by adding offset
57-
const istOffset = 5.5 * 60 * 60 * 1000; // 5.5 hours in milliseconds
58-
const istDate = new Date(today.getTime() + istOffset);
59-
istDate.setUTCHours(18, 30, 0, 0); // Set to previous day 18:30 UTC (00:00 IST)
60-
return date < istDate;
56+
return isPastDateIST(date);
6157
};
6258

6359
// Navigate to previous month
@@ -73,11 +69,14 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, onChange, label,
7369
// Handle date selection and set to midnight IST (UTC+5:30)
7470
const selectDate = (day: number) => {
7571
// Create date at midnight IST
76-
const newDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
77-
// Set to midnight IST by adjusting for UTC offset
78-
// First, set to previous day 18:30 UTC (equivalent to 00:00 IST next day)
79-
newDate.setUTCHours(18, 30, 0, 0);
80-
onChange(newDate);
72+
const istDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
73+
istDate.setHours(0, 0, 0, 0); // Set to midnight IST
74+
75+
// Convert IST date to UTC for storage
76+
const utcDate = istToUTC(istDate);
77+
if (utcDate) {
78+
onChange(utcDate);
79+
}
8180
setShowCalendar(false);
8281
};
8382

client-test/src/components/ProfilePage/index.tsx

Lines changed: 126 additions & 104 deletions
Large diffs are not rendered by default.

client-test/src/lib/timezone.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Client-side timezone utility for consistent IST (Indian Standard Time) handling
3+
* All business logic uses IST dates, database stores UTC
4+
*/
5+
6+
const IST_OFFSET_MS = 5.5 * 60 * 60 * 1000; // IST is UTC+5:30
7+
8+
/**
9+
* Get current date/time in IST
10+
*/
11+
export const getISTNow = () => {
12+
return new Date(Date.now() + IST_OFFSET_MS);
13+
};
14+
15+
/**
16+
* Convert UTC date to IST date
17+
*/
18+
export const utcToIST = (utcDate: Date) => {
19+
if (!utcDate) return null;
20+
return new Date(utcDate.getTime() + IST_OFFSET_MS);
21+
};
22+
23+
/**
24+
* Convert IST date to UTC date
25+
*/
26+
export const istToUTC = (istDate: Date) => {
27+
if (!istDate) return null;
28+
return new Date(istDate.getTime() - IST_OFFSET_MS);
29+
};
30+
31+
/**
32+
* Get IST date boundaries for a given date (defaults to today)
33+
* Returns UTC dates that correspond to IST day boundaries
34+
*/
35+
export const getISTDateBounds = (date: Date | null = null) => {
36+
const referenceDate = date || getISTNow();
37+
38+
// Create IST date boundaries
39+
const istDate = new Date(referenceDate);
40+
istDate.setHours(0, 0, 0, 0);
41+
42+
const istTomorrow = new Date(istDate);
43+
istTomorrow.setDate(istTomorrow.getDate() + 1);
44+
45+
// Convert back to UTC for database queries
46+
return {
47+
start: istToUTC(istDate),
48+
end: istToUTC(istTomorrow)
49+
};
50+
};
51+
52+
/**
53+
* Format date to IST date string (YYYY-MM-DD)
54+
*/
55+
export const formatISTDateString = (date: Date | null = null) => {
56+
const istDate = date ? utcToIST(date) : getISTNow();
57+
if (!istDate) return '';
58+
return istDate.toISOString().split('T')[0];
59+
};
60+
61+
/**
62+
* Check if date is in the past compared to current IST date
63+
*/
64+
export const isPastDateIST = (date: Date) => {
65+
const todayIST = getISTNow();
66+
const dateIST = utcToIST(date);
67+
68+
if (!dateIST) return false;
69+
70+
// Compare dates by setting both to start of day
71+
const todayStart = new Date(todayIST);
72+
todayStart.setHours(0, 0, 0, 0);
73+
74+
const dateStart = new Date(dateIST);
75+
dateStart.setHours(0, 0, 0, 0);
76+
77+
return dateStart < todayStart;
78+
};

server/controllers/adminControllers.js

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Solution } from "../models/Solution.js";
44
import { Category } from '../models/Category.js';
55
import { ensureCategoriesExist } from "../utils/categoryHelper.js";
66
import auditService from "../services/auditService.js";
7+
import { getISTDateBounds, istToUTC } from "../utils/timezone.js";
78

89
export const getUsers = async (req, res) => {
910
try {
@@ -98,33 +99,26 @@ export const addChallenge = async (req, res) => {
9899
solution,
99100
} = req.body;
100101

101-
// Convert createdAt to midnight IST (18:30 UTC of previous day)
102102
let scheduleDate;
103103
if (createdAt) {
104-
const date = new Date(createdAt);
105-
// Set to 18:30:00 UTC of the previous day (00:00 IST of selected day)
106-
date.setUTCHours(18, 30, 0, 0);
107-
scheduleDate = date;
104+
const istDate = new Date(createdAt);
105+
istDate.setHours(0, 0, 0, 0);
106+
scheduleDate = istToUTC(istDate);
108107
} else {
109-
// If no date provided, use next day midnight IST
110-
const now = new Date();
111-
const tomorrow = new Date(now);
112-
tomorrow.setDate(tomorrow.getDate() + 1);
113-
tomorrow.setUTCHours(18, 30, 0, 0);
114-
scheduleDate = tomorrow;
108+
const tomorrowIST = new Date();
109+
tomorrowIST.setDate(tomorrowIST.getDate() + 1);
110+
tomorrowIST.setHours(0, 0, 0, 0);
111+
scheduleDate = istToUTC(tomorrowIST);
115112
}
116113

117-
// Validate required fields
118114
if (!title || !description || !category || !difficulty || !points || !problemLink || !platform) {
119115
return res.status(400).json({ success: false, message: "All fields are required" });
120116
}
121117

122-
// Ensure category is an array of strings
123118
if (!Array.isArray(category)) {
124119
return res.status(400).json({ success: false, message: "Category must be an array of strings" });
125120
}
126121

127-
// Ensure categories exist in database (create if they don't)
128122
await ensureCategoriesExist(category);
129123

130124
const newChallenge = new Challenge({
@@ -142,7 +136,6 @@ export const addChallenge = async (req, res) => {
142136

143137
let savedSolution = null;
144138
if (solution) {
145-
// Create solution with all fields, including explanation
146139
const newSolution = new Solution({
147140
explanation: solution.explanation || "No explanation provided",
148141
cpp: solution.cpp || "",
@@ -188,13 +181,11 @@ export const updateChallenge = async (req, res) => {
188181
solution,
189182
} = req.body;
190183

191-
// Convert createdAt to midnight IST if provided
192184
let scheduleDate;
193185
if (createdAt) {
194-
const date = new Date(createdAt);
195-
// Set to 18:30:00 UTC of the previous day (00:00 IST of selected day)
196-
date.setUTCHours(18, 30, 0, 0);
197-
scheduleDate = date;
186+
const istDate = new Date(createdAt);
187+
istDate.setHours(0, 0, 0, 0);
188+
scheduleDate = istToUTC(istDate);
198189
}
199190
if (!challengeId) {
200191
return res.status(400).json({ success: false, message: "Challenge ID is required" });
@@ -203,11 +194,10 @@ export const updateChallenge = async (req, res) => {
203194
if (!challenge) {
204195
return res.status(404).json({ success: false, message: "Challenge not found" });
205196
}
206-
// Update challenge fields if provided
197+
207198
if (title) challenge.title = title;
208199
if (description) challenge.description = description;
209200
if (category) {
210-
// Ensure new categories exist (create if they don't)
211201
await ensureCategoriesExist(category);
212202
challenge.category = category;
213203
}
@@ -268,26 +258,12 @@ export const updateChallenge = async (req, res) => {
268258
// Example: GET /admin/getchallenge
269259
export const getTodayChallenge = async (req, res) => {
270260
try {
271-
// Get current time in UTC
272-
const now = new Date();
273-
274-
// Calculate today's and tomorrow's midnight in IST (18:30 UTC of previous day)
275-
const todayIST = new Date(now);
276-
todayIST.setUTCHours(18, 30, 0, 0);
277-
if (now < todayIST) {
278-
// If current time is before today's IST midnight, adjust to previous day
279-
todayIST.setDate(todayIST.getDate() - 1);
280-
}
281-
282-
const tomorrowIST = new Date(todayIST);
283-
tomorrowIST.setDate(tomorrowIST.getDate() + 1);
261+
const { start: todayUTC, end: tomorrowUTC } = getISTDateBounds();
284262

285-
// Find today's challenge using createdAt
286-
// The dates are already in UTC since we set them using setUTCHours
287263
const challenge = await Challenge.findOne({
288264
createdAt: {
289-
$gte: todayIST,
290-
$lt: tomorrowIST
265+
$gte: todayUTC,
266+
$lt: tomorrowUTC
291267
}
292268
}).sort({ createdAt: -1 });
293269

@@ -298,7 +274,6 @@ export const getTodayChallenge = async (req, res) => {
298274
});
299275
}
300276

301-
// Manually fetch related solution (since challenge schema has no `solution` field)
302277
const solution = await Solution.findOne({ challenge: challenge._id });
303278

304279
return res.status(200).json({

server/controllers/authController.js

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,25 @@ import mongoose from "mongoose";
88
// import { client } from "../utils/typesenseClient.js";
99
import { warmupLeaderboardCache } from "../utils/leaderBoardCache.js";
1010
import auditService from "../services/auditService.js";
11+
import { getISTNow } from "../utils/timezone.js";
1112
import dotenv from "dotenv";
1213

1314
dotenv.config();
1415

1516

1617
const setAuthCookies = (res, user, token) => {
17-
// Store JWT in HTTP-only cookie
1818
res.cookie("jwt", token, {
1919
httpOnly: true,
2020
secure: process.env.NODE_ENV === "production",
2121
sameSite: process.env.NODE_ENV === "production" ? "Strict" : "Lax",
22-
maxAge: 24 * 60 * 60 * 1000, // 1 day
22+
maxAge: 24 * 60 * 60 * 1000,
2323
});
2424

25-
// Store user ID and email in HTTP-only cookie
2625
res.cookie("user", JSON.stringify({ id: user._id, email: user.email }), {
27-
httpOnly: true, // Prevent JavaScript access
26+
httpOnly: true,
2827
secure: process.env.NODE_ENV === "production",
2928
sameSite: process.env.NODE_ENV === "production" ? "Strict" : "Lax",
30-
maxAge: 24 * 60 * 60 * 1000, // 1 day
29+
maxAge: 24 * 60 * 60 * 1000,
3130
});
3231
};
3332

@@ -116,8 +115,7 @@ export const registerUser = async (req, res) => {
116115

117116
// Generate a 6-digit OTP
118117
const otp = crypto.randomInt(100000, 999999).toString();
119-
const nowIST = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Kolkata' }));
120-
const otpExpires = nowIST.getTime() + 5 * 60 * 1000; // OTP expires in 5 minutes
118+
const otpExpires = getISTNow().getTime() + 5 * 60 * 1000;
121119

122120
// Use upsert with atomic operation to prevent race conditions
123121
await User.findOneAndUpdate(
@@ -245,8 +243,7 @@ export const verifyEmail = async (req, res) => {
245243
return res.status(400).json({ error: "Email already verified, Please login" });
246244
}
247245

248-
const nowIST = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Kolkata' }));
249-
if (user.otpExpires < nowIST.getTime()) {
246+
if (user.otpExpires < getISTNow().getTime()) {
250247
auditService.authEvent('verification_failed', {
251248
requestId: req.auditContext?.requestId,
252249
email,
@@ -463,8 +460,7 @@ export const forgotPassword = async (req, res) => {
463460

464461
// Save Token & Expiry in DB
465462
user.resetPasswordToken = resetToken;
466-
const nowIST = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Kolkata' }));
467-
user.resetPasswordExpires = nowIST.getTime() + 15 * 60 * 1000;
463+
user.resetPasswordExpires = getISTNow().getTime() + 15 * 60 * 1000;
468464
await user.save();
469465

470466
// Send Reset Link via Email
@@ -487,7 +483,7 @@ export const resetPassword = async (req, res) => {
487483
const decoded = jwt.verify(token, process.env.JWT_SECRET);
488484
const user = await User.findById(decoded.id);
489485

490-
const nowIST = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Kolkata' }));
486+
const nowIST = getISTNow();
491487
if (!user || user.resetPasswordToken !== token || user.resetPasswordExpires < nowIST.getTime()) {
492488
return res.status(400).json({ message: "Invalid or expired token" });
493489
}

server/services/challengeCacheService.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import cacheService from "../services/cacheService.js";
22
import auditService from "../services/auditService.js";
3+
import { formatISTDateString } from "../utils/timezone.js";
34

45
const CACHE_NAMESPACE = 'challenges';
56
const CHALLENGE_TTL = 3600; // 1 hour
@@ -10,7 +11,7 @@ class ChallengeCacheService {
1011
// Cache today's challenge (POTD)
1112
async cacheTodaysChallenge(challengeData) {
1213
try {
13-
const today = new Date().toISOString().split('T')[0];
14+
const today = formatISTDateString();
1415
await cacheService.set(CACHE_NAMESPACE, `potd_${today}`, challengeData, POTD_TTL);
1516
await cacheService.set(CACHE_NAMESPACE, 'potd_latest', challengeData, POTD_TTL);
1617
auditService.cacheEvent('potd_cached', { date: today });
@@ -22,7 +23,7 @@ class ChallengeCacheService {
2223
// Get cached today's challenge
2324
async getCachedTodaysChallenge() {
2425
try {
25-
const today = new Date().toISOString().split('T')[0];
26+
const today = formatISTDateString();
2627
let potd = await cacheService.get(CACHE_NAMESPACE, `potd_${today}`);
2728

2829
if (!potd) {

server/utils/streakResetJob.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cron from 'node-cron';
22
import { User } from '../models/User.js';
33
import auditService from '../services/auditService.js';
4+
import { getISTNow, formatISTDateString } from './timezone.js';
45

56
const getLastSolvedDate = (challenges = []) => {
67
if (!challenges.length) return null;
@@ -23,13 +24,11 @@ const resetUserStreaks = async () => {
2324

2425
try {
2526
// Use IST timezone for consistent date calculation
26-
const now = new Date();
27-
const istOffset = 5.5 * 60 * 60 * 1000;
28-
const istNow = new Date(now.getTime() + istOffset);
27+
const istNow = getISTNow();
2928

3029
const yesterday = new Date(istNow);
3130
yesterday.setDate(istNow.getDate() - 1);
32-
const yesterStr = yesterday.toISOString().split("T")[0];
31+
const yesterStr = formatISTDateString(yesterday);
3332

3433
auditService.systemEvent('streak_reset_started', {
3534
targetDate: yesterStr,

0 commit comments

Comments
 (0)