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
This commit is contained in:
@@ -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<Order[]>([])
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<User | null>(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 (
|
||||
<div>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<Metadata> {
|
||||
try {
|
||||
@@ -23,7 +24,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
return (
|
||||
<html lang="it">
|
||||
<body className="bg-gray-50 text-gray-900 min-h-screen flex flex-col">
|
||||
{children}
|
||||
<UserProvider>
|
||||
{children}
|
||||
</UserProvider>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter, useSearchParams } 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 LoginPage() {
|
||||
return (
|
||||
@@ -23,6 +24,7 @@ function LoginForm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const redirect = searchParams.get('redirect') || '/'
|
||||
const { refreshUser } = useUser()
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
@@ -43,11 +45,11 @@ function LoginForm() {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(data.user))
|
||||
const refreshedUser = await refreshUser()
|
||||
|
||||
if (data.user.mustChangePassword) {
|
||||
if (refreshedUser?.mustChangePassword) {
|
||||
router.push('/admin/change-password')
|
||||
} else if (data.user.role === 'ADMIN' || data.user.role === 'OWNER') {
|
||||
} else if (refreshedUser?.role === 'ADMIN' || refreshedUser?.role === 'OWNER') {
|
||||
router.push('/admin')
|
||||
} else {
|
||||
router.push(redirect)
|
||||
|
||||
@@ -6,6 +6,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 RegisterPage() {
|
||||
const [name, setName] = useState('')
|
||||
@@ -14,6 +15,7 @@ export default function RegisterPage() {
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { refreshUser } = useUser()
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
@@ -34,7 +36,7 @@ export default function RegisterPage() {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(data.user))
|
||||
await refreshUser()
|
||||
router.push('/')
|
||||
} catch {
|
||||
setError('Something went wrong. Please try again.')
|
||||
|
||||
@@ -3,31 +3,22 @@
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useUser } from '@/context/UserContext'
|
||||
|
||||
export function Navbar() {
|
||||
const [cartCount, setCartCount] = useState(0)
|
||||
const [user, setUser] = useState<{ name?: string; email: string; role: string } | null>(null)
|
||||
const { user, refreshUser } = useUser()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const cart = JSON.parse(localStorage.getItem('cart') || '[]')
|
||||
const count = cart.reduce((sum: number, item: { quantity: number }) => sum + item.quantity, 0)
|
||||
setCartCount(count)
|
||||
|
||||
const userData = localStorage.getItem('user')
|
||||
if (userData) {
|
||||
try {
|
||||
setUser(JSON.parse(userData))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch('/api/auth/logout', { method: 'POST' })
|
||||
localStorage.removeItem('user')
|
||||
setUser(null)
|
||||
await refreshUser()
|
||||
router.push('/')
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
role: string
|
||||
mustChangePassword: boolean
|
||||
}
|
||||
|
||||
interface UserContextValue {
|
||||
user: User | null
|
||||
isLoading: boolean
|
||||
setUser: (user: User | null) => void
|
||||
refreshUser: () => Promise<User | null>
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextValue | null>(null)
|
||||
|
||||
export function UserProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const refreshUser = useCallback(async (): Promise<User | null> => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setUser(data.user)
|
||||
return data.user
|
||||
} else {
|
||||
setUser(null)
|
||||
return null
|
||||
}
|
||||
} catch {
|
||||
setUser(null)
|
||||
return null
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
refreshUser()
|
||||
}, [refreshUser])
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, isLoading, setUser, refreshUser }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useUser(): UserContextValue {
|
||||
const ctx = useContext(UserContext)
|
||||
if (!ctx) throw new Error('useUser must be used within a UserProvider')
|
||||
return ctx
|
||||
}
|
||||
Reference in New Issue
Block a user