From fcfa0707a18a6d1a6d4ab1f632de1fecb5ec7a98 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Tue, 19 May 2026 10:10:17 +0200 Subject: [PATCH] fix(security): replace localStorage user state with server-side session - Add GET /api/auth/me endpoint returning current user from httpOnly cookie - Add UserContext + useUser() hook that fetches from /api/auth/me on mount - Wrap root layout with UserProvider - Remove all localStorage.setItem/getItem('user') calls from login, register, navbar, account pages, change-password, and checkout - mustChangePassword redirect now reads from refreshed server session --- app/src/app/account/orders/page.tsx | 6 ++- app/src/app/account/page.tsx | 25 +++------ app/src/app/admin/change-password/page.tsx | 11 ++-- app/src/app/api/auth/me/route.ts | 16 ++++++ app/src/app/checkout/page.tsx | 16 +++--- app/src/app/layout.tsx | 5 +- app/src/app/login/page.tsx | 8 +-- app/src/app/register/page.tsx | 4 +- app/src/components/storefront/Navbar.tsx | 15 ++---- app/src/context/UserContext.tsx | 60 ++++++++++++++++++++++ 10 files changed, 116 insertions(+), 50 deletions(-) create mode 100644 app/src/app/api/auth/me/route.ts create mode 100644 app/src/context/UserContext.tsx diff --git a/app/src/app/account/orders/page.tsx b/app/src/app/account/orders/page.tsx index 8637469..40057d9 100644 --- a/app/src/app/account/orders/page.tsx +++ b/app/src/app/account/orders/page.tsx @@ -6,6 +6,7 @@ import Link from 'next/link' import { Navbar } from '@/components/storefront/Navbar' import { Badge } from '@/components/ui/Badge' import { Alert } from '@/components/ui/Alert' +import { useUser } from '@/context/UserContext' interface Order { id: string @@ -35,13 +36,14 @@ export default function OrdersPage() { function OrdersContent() { const [orders, setOrders] = useState([]) const [loading, setLoading] = useState(true) + const { user, isLoading: userLoading } = useUser() const router = useRouter() const searchParams = useSearchParams() const success = searchParams.get('success') useEffect(() => { - const stored = localStorage.getItem('user') - if (!stored) { + if (userLoading) return + if (!user) { router.push('/login?redirect=/account/orders') return } diff --git a/app/src/app/account/page.tsx b/app/src/app/account/page.tsx index 35959ab..3b08ea2 100644 --- a/app/src/app/account/page.tsx +++ b/app/src/app/account/page.tsx @@ -1,35 +1,22 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { Navbar } from '@/components/storefront/Navbar' - -interface User { - id: string - email: string - name?: string - role: string -} +import { useUser } from '@/context/UserContext' export default function AccountPage() { - const [user, setUser] = useState(null) + const { user, isLoading } = useUser() const router = useRouter() useEffect(() => { - const stored = localStorage.getItem('user') - if (!stored) { + if (!isLoading && !user) { router.push('/login?redirect=/account') - return } - try { - setUser(JSON.parse(stored)) - } catch { - router.push('/login') - } - }, [router]) + }, [isLoading, user, router]) - if (!user) return null + if (isLoading || !user) return null return (
diff --git a/app/src/app/admin/change-password/page.tsx b/app/src/app/admin/change-password/page.tsx index 89cd4cc..89aeb2a 100644 --- a/app/src/app/admin/change-password/page.tsx +++ b/app/src/app/admin/change-password/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation' import { Input } from '@/components/ui/Input' import { Button } from '@/components/ui/Button' import { Alert } from '@/components/ui/Alert' +import { useUser } from '@/context/UserContext' export default function ChangePasswordPage() { const [currentPassword, setCurrentPassword] = useState('') @@ -14,6 +15,7 @@ export default function ChangePasswordPage() { const [success, setSuccess] = useState(false) const [loading, setLoading] = useState(false) const router = useRouter() + const { user, setUser } = useUser() async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -41,12 +43,9 @@ export default function ChangePasswordPage() { } setSuccess(true) - // Update localStorage to remove mustChangePassword flag - const userStr = localStorage.getItem('user') - if (userStr) { - const user = JSON.parse(userStr) - user.mustChangePassword = false - localStorage.setItem('user', JSON.stringify(user)) + // Update context to remove mustChangePassword flag + if (user) { + setUser({ ...user, mustChangePassword: false }) } setTimeout(() => router.push('/admin'), 1500) diff --git a/app/src/app/api/auth/me/route.ts b/app/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..8cfa28f --- /dev/null +++ b/app/src/app/api/auth/me/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server' +import { getCurrentUser } from '@/lib/auth' + +export async function GET() { + const user = await getCurrentUser() + if (!user) return NextResponse.json({ user: null }, { status: 401 }) + return NextResponse.json({ + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + mustChangePassword: user.mustChangePassword, + }, + }) +} diff --git a/app/src/app/checkout/page.tsx b/app/src/app/checkout/page.tsx index 2838bab..21b3721 100644 --- a/app/src/app/checkout/page.tsx +++ b/app/src/app/checkout/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation' import { Navbar } from '@/components/storefront/Navbar' import { Button } from '@/components/ui/Button' import { Alert } from '@/components/ui/Alert' +import { useUser } from '@/context/UserContext' interface CartItem { productId: string @@ -19,20 +20,23 @@ export default function CheckoutPage() { const [loading, setLoading] = useState(false) const [error, setError] = useState('') const router = useRouter() + const { user, isLoading: userLoading } = useUser() useEffect(() => { + if (userLoading) return + + if (!user) { + router.push('/login?redirect=/checkout') + return + } + const stored = JSON.parse(localStorage.getItem('cart') || '[]') if (stored.length === 0) { router.push('/cart') return } setCart(stored) - - const user = localStorage.getItem('user') - if (!user) { - router.push('/login?redirect=/checkout') - } - }, [router]) + }, [router, user, userLoading]) const subtotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0) diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index a4d370e..2df6b2d 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import './globals.css' import { prisma } from '@/lib/prisma' import { Footer } from '@/components/storefront/Footer' +import { UserProvider } from '@/context/UserContext' export async function generateMetadata(): Promise { try { @@ -23,7 +24,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - {children} + + {children} +