diff --git a/src/app/api/mood/route.ts b/src/app/api/mood/route.ts index 0034b39..2ed8dd5 100644 --- a/src/app/api/mood/route.ts +++ b/src/app/api/mood/route.ts @@ -101,10 +101,18 @@ export async function POST(request: NextRequest) { const body = await request.json() as { mood: 'amazing' | 'good' | 'neutral' | 'bad' | 'terrible'; score: number; comment?: string; date?: string }; const { mood, score, comment, date } = body; - if (!mood) { + if (!mood || !['amazing', 'good', 'neutral', 'bad', 'terrible'].includes(mood)) { return NextResponse.json({ error: 'Invalid mood' }, { status: 400 }); } + if (comment && (typeof comment !== 'string' || comment.length > 500)) { + return NextResponse.json({ error: 'Comment too long' }, { status: 400 }); + } + + if (date && !/^\d{4}-\d{2}-\d{2}$/.test(date)) { + return NextResponse.json({ error: 'Invalid date format' }, { status: 400 }); + } + // Validate score is between 0 and 100 const finalScore = Math.max(0, Math.min(100, score ?? 50)); diff --git a/src/app/api/preferences/route.ts b/src/app/api/preferences/route.ts index 6862183..6ba1b84 100644 --- a/src/app/api/preferences/route.ts +++ b/src/app/api/preferences/route.ts @@ -71,6 +71,26 @@ export async function POST(request: NextRequest) { lastWeedUsageTime?: string; }; + // Validation + if (body.substance && !['nicotine', 'weed'].includes(body.substance)) { + return NextResponse.json({ error: 'Invalid substance' }, { status: 400 }); + } + if (body.trackingStartDate && !/^\d{4}-\d{2}-\d{2}$/.test(body.trackingStartDate)) { + return NextResponse.json({ error: 'Invalid trackingStartDate format' }, { status: 400 }); + } + if (body.dailyGoal !== undefined && (typeof body.dailyGoal !== 'number' || body.dailyGoal < 0)) { + return NextResponse.json({ error: 'Invalid dailyGoal' }, { status: 400 }); + } + if (body.userName && (typeof body.userName !== 'string' || body.userName.length > 100)) { + return NextResponse.json({ error: 'Invalid userName' }, { status: 400 }); + } + if (body.userAge !== undefined && (typeof body.userAge !== 'number' || body.userAge < 0 || body.userAge > 120)) { + return NextResponse.json({ error: 'Invalid userAge' }, { status: 400 }); + } + if (body.religion && !['christian', 'secular'].includes(body.religion)) { + return NextResponse.json({ error: 'Invalid religion' }, { status: 400 }); + } + // If quitState is provided in body, save it to quitPlanJson const quitPlanJson = body.quitState ? JSON.stringify(body.quitState) diff --git a/src/app/api/usage/route.ts b/src/app/api/usage/route.ts index 37cc3df..b63b133 100644 --- a/src/app/api/usage/route.ts +++ b/src/app/api/usage/route.ts @@ -43,6 +43,17 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } + // Validation + if (typeof count !== 'number' || count < 0 || !Number.isInteger(count)) { + return NextResponse.json({ error: 'Invalid count' }, { status: 400 }); + } + if (!['nicotine', 'weed'].includes(substance)) { + return NextResponse.json({ error: 'Invalid substance' }, { status: 400 }); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + return NextResponse.json({ error: 'Invalid date format' }, { status: 400 }); + } + // Add to existing count const entry = await upsertUsageEntryD1(session.user.id, date, count, substance, true); @@ -75,6 +86,17 @@ export async function PUT(request: NextRequest) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } + // Validation + if (typeof count !== 'number' || count < 0 || !Number.isInteger(count)) { + return NextResponse.json({ error: 'Invalid count' }, { status: 400 }); + } + if (!['nicotine', 'weed'].includes(substance)) { + return NextResponse.json({ error: 'Invalid substance' }, { status: 400 }); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + return NextResponse.json({ error: 'Invalid date format' }, { status: 400 }); + } + // Set the exact count (replace, not add) const entry = await upsertUsageEntryD1(session.user.id, date, count, substance, false); diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index bd2108d..54ef562 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -35,6 +35,7 @@ import { SavingsTrackerCard } from './SavingsTrackerCard'; import { MoodTracker } from './MoodTracker'; import { ScrollWheelLogger } from './ScrollWheelLogger'; import { UsageLoggerDropUp } from './UsageLoggerDropUp'; +import { VersionUpdateModal } from './VersionUpdateModal'; import { Button } from '@/components/ui/button'; import { PlusCircle, ChevronLeft, ChevronRight, X } from 'lucide-react'; import { useTheme } from '@/lib/theme-context'; @@ -468,6 +469,8 @@ export function Dashboard({ user }: DashboardProps) { /> )} + + {showCelebration && newBadge && ( { + // Check local storage for the flag + const hasSeenUpdate = localStorage.getItem('seen_version_1.0_update'); + if (!hasSeenUpdate) { + setIsOpen(true); + } + }, []); + + const handleClose = () => { + // Set the flag in local storage + localStorage.setItem('seen_version_1.0_update', 'true'); + setIsOpen(false); + }; + + return ( + !open && handleClose()}> + + + {/* Header Section with Gradient Background */} +
+
+
+
+ +
+
+ Version 1.0 is Live! + + The biggest update to QuitTraq is finally here. + +
+
+
+ + {/* Content Section */} +
+

What's New

+ +
+ + {/* Security */} +
+
+ +
+
+

Major Security Overhaul

+

+ implemented complete security audit and fixes. This is why you had to login again—we've secured your session data with industry-standard encryption. +

+
+
+ + {/* Notifications & Input */} +
+
+ +

Smart Notifications

+

+ Updated messaging system that adapts based on the time of day to keep you motivated. +

+
+ +
+ +

PWA & UI Polish

+

+ New login screen, landing page, and optimization fixes for a smoother app-like experience. +

+
+
+ + {/* Tracking Improvements */} +
+
+ +
+
+

Refined Tracking

+

+ Fixed input logging on desktop, improved the independent track buttons for weed/nicotine, and fixed the scroll system. +

+
+
+ + {/* Goals & Moods */} +
+
+ +

Independent Goals

+

+ Set separate quit plans for nicotine and marijuana. Fixed achievement unlocking celebrations. +

+
+ +
+ +

Mood Tracker 2.0

+

+ Friendlier messages, more mood options, daily average charts, and weekly score tracking. +

+
+
+
+
+ + {/* Footer */} +
+ +
+ + +
+ ); +} diff --git a/src/lib/session.ts b/src/lib/session.ts index 722e314..96c64b1 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -16,6 +16,43 @@ export interface Session { } const SESSION_COOKIE_NAME = 'quit_smoking_session'; +const SESSION_SECRET = process.env.SESSION_SECRET || 'fallback-secret-for-dev-only'; + +// Helper to sign the session +async function sign(data: string, secret: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const dataData = encoder.encode(data); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign('HMAC', key, dataData); + return btoa(String.fromCharCode(...new Uint8Array(signature))); +} + +// Helper to verify the session +async function verify(data: string, signature: string, secret: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const dataData = encoder.encode(data); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['verify'] + ); + + const sigBytes = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)); + return await crypto.subtle.verify('HMAC', key, sigBytes, dataData); +} export async function getSession(): Promise { const cookieStore = await cookies(); @@ -26,8 +63,20 @@ export async function getSession(): Promise { } try { - return JSON.parse(sessionCookie.value); - } catch { + const [payloadBase64, signature] = sessionCookie.value.split('.'); + if (!payloadBase64 || !signature) return null; + + const payload = atob(payloadBase64); + const isValid = await verify(payload, signature, SESSION_SECRET); + + if (!isValid) { + console.warn('Invalid session signature detected'); + return null; + } + + return JSON.parse(payload); + } catch (error) { + console.error('Error parsing session:', error); return null; } } @@ -36,7 +85,11 @@ export async function setSession(session: Session): Promise { const cookieStore = await cookies(); const maxAge = session.stayLoggedIn ? 60 * 60 * 24 * 30 : 60 * 60 * 24; // 30 days if stay logged in, else 1 day - cookieStore.set(SESSION_COOKIE_NAME, JSON.stringify(session), { + const payload = JSON.stringify(session); + const payloadBase64 = btoa(payload); + const signature = await sign(payload, SESSION_SECRET); + + cookieStore.set(SESSION_COOKIE_NAME, `${payloadBase64}.${signature}`, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax',