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 { Navbar } from '@/components/storefront/Navbar'
|
||||||
import { Badge } from '@/components/ui/Badge'
|
import { Badge } from '@/components/ui/Badge'
|
||||||
import { Alert } from '@/components/ui/Alert'
|
import { Alert } from '@/components/ui/Alert'
|
||||||
|
import { useUser } from '@/context/UserContext'
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
id: string
|
id: string
|
||||||
@@ -35,13 +36,14 @@ export default function OrdersPage() {
|
|||||||
function OrdersContent() {
|
function OrdersContent() {
|
||||||
const [orders, setOrders] = useState<Order[]>([])
|
const [orders, setOrders] = useState<Order[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const { user, isLoading: userLoading } = useUser()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const success = searchParams.get('success')
|
const success = searchParams.get('success')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = localStorage.getItem('user')
|
if (userLoading) return
|
||||||
if (!stored) {
|
if (!user) {
|
||||||
router.push('/login?redirect=/account/orders')
|
router.push('/login?redirect=/account/orders')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,22 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Navbar } from '@/components/storefront/Navbar'
|
import { Navbar } from '@/components/storefront/Navbar'
|
||||||
|
import { useUser } from '@/context/UserContext'
|
||||||
interface User {
|
|
||||||
id: string
|
|
||||||
email: string
|
|
||||||
name?: string
|
|
||||||
role: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AccountPage() {
|
export default function AccountPage() {
|
||||||
const [user, setUser] = useState<User | null>(null)
|
const { user, isLoading } = useUser()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = localStorage.getItem('user')
|
if (!isLoading && !user) {
|
||||||
if (!stored) {
|
|
||||||
router.push('/login?redirect=/account')
|
router.push('/login?redirect=/account')
|
||||||
return
|
|
||||||
}
|
}
|
||||||
try {
|
}, [isLoading, user, router])
|
||||||
setUser(JSON.parse(stored))
|
|
||||||
} catch {
|
|
||||||
router.push('/login')
|
|
||||||
}
|
|
||||||
}, [router])
|
|
||||||
|
|
||||||
if (!user) return null
|
if (isLoading || !user) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
|||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Alert } from '@/components/ui/Alert'
|
import { Alert } from '@/components/ui/Alert'
|
||||||
|
import { useUser } from '@/context/UserContext'
|
||||||
|
|
||||||
export default function ChangePasswordPage() {
|
export default function ChangePasswordPage() {
|
||||||
const [currentPassword, setCurrentPassword] = useState('')
|
const [currentPassword, setCurrentPassword] = useState('')
|
||||||
@@ -14,6 +15,7 @@ export default function ChangePasswordPage() {
|
|||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { user, setUser } = useUser()
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -41,12 +43,9 @@ export default function ChangePasswordPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSuccess(true)
|
setSuccess(true)
|
||||||
// Update localStorage to remove mustChangePassword flag
|
// Update context to remove mustChangePassword flag
|
||||||
const userStr = localStorage.getItem('user')
|
if (user) {
|
||||||
if (userStr) {
|
setUser({ ...user, mustChangePassword: false })
|
||||||
const user = JSON.parse(userStr)
|
|
||||||
user.mustChangePassword = false
|
|
||||||
localStorage.setItem('user', JSON.stringify(user))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => router.push('/admin'), 1500)
|
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 { Navbar } from '@/components/storefront/Navbar'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Alert } from '@/components/ui/Alert'
|
import { Alert } from '@/components/ui/Alert'
|
||||||
|
import { useUser } from '@/context/UserContext'
|
||||||
|
|
||||||
interface CartItem {
|
interface CartItem {
|
||||||
productId: string
|
productId: string
|
||||||
@@ -19,20 +20,23 @@ export default function CheckoutPage() {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { user, isLoading: userLoading } = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (userLoading) return
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
router.push('/login?redirect=/checkout')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const stored = JSON.parse(localStorage.getItem('cart') || '[]')
|
const stored = JSON.parse(localStorage.getItem('cart') || '[]')
|
||||||
if (stored.length === 0) {
|
if (stored.length === 0) {
|
||||||
router.push('/cart')
|
router.push('/cart')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setCart(stored)
|
setCart(stored)
|
||||||
|
}, [router, user, userLoading])
|
||||||
const user = localStorage.getItem('user')
|
|
||||||
if (!user) {
|
|
||||||
router.push('/login?redirect=/checkout')
|
|
||||||
}
|
|
||||||
}, [router])
|
|
||||||
|
|
||||||
const subtotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
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 './globals.css'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { Footer } from '@/components/storefront/Footer'
|
import { Footer } from '@/components/storefront/Footer'
|
||||||
|
import { UserProvider } from '@/context/UserContext'
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
try {
|
try {
|
||||||
@@ -23,7 +24,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
return (
|
return (
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
<body className="bg-gray-50 text-gray-900 min-h-screen flex flex-col">
|
<body className="bg-gray-50 text-gray-900 min-h-screen flex flex-col">
|
||||||
{children}
|
<UserProvider>
|
||||||
|
{children}
|
||||||
|
</UserProvider>
|
||||||
<Footer />
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from 'next/navigation'
|
|||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Alert } from '@/components/ui/Alert'
|
import { Alert } from '@/components/ui/Alert'
|
||||||
|
import { useUser } from '@/context/UserContext'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
@@ -23,6 +24,7 @@ function LoginForm() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const redirect = searchParams.get('redirect') || '/'
|
const redirect = searchParams.get('redirect') || '/'
|
||||||
|
const { refreshUser } = useUser()
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -43,11 +45,11 @@ function LoginForm() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(data.user))
|
const refreshedUser = await refreshUser()
|
||||||
|
|
||||||
if (data.user.mustChangePassword) {
|
if (refreshedUser?.mustChangePassword) {
|
||||||
router.push('/admin/change-password')
|
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')
|
router.push('/admin')
|
||||||
} else {
|
} else {
|
||||||
router.push(redirect)
|
router.push(redirect)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'
|
|||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Alert } from '@/components/ui/Alert'
|
import { Alert } from '@/components/ui/Alert'
|
||||||
|
import { useUser } from '@/context/UserContext'
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
@@ -14,6 +15,7 @@ export default function RegisterPage() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { refreshUser } = useUser()
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -34,7 +36,7 @@ export default function RegisterPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(data.user))
|
await refreshUser()
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} catch {
|
} catch {
|
||||||
setError('Something went wrong. Please try again.')
|
setError('Something went wrong. Please try again.')
|
||||||
|
|||||||
@@ -3,31 +3,22 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useUser } from '@/context/UserContext'
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const [cartCount, setCartCount] = useState(0)
|
const [cartCount, setCartCount] = useState(0)
|
||||||
const [user, setUser] = useState<{ name?: string; email: string; role: string } | null>(null)
|
const { user, refreshUser } = useUser()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cart = JSON.parse(localStorage.getItem('cart') || '[]')
|
const cart = JSON.parse(localStorage.getItem('cart') || '[]')
|
||||||
const count = cart.reduce((sum: number, item: { quantity: number }) => sum + item.quantity, 0)
|
const count = cart.reduce((sum: number, item: { quantity: number }) => sum + item.quantity, 0)
|
||||||
setCartCount(count)
|
setCartCount(count)
|
||||||
|
|
||||||
const userData = localStorage.getItem('user')
|
|
||||||
if (userData) {
|
|
||||||
try {
|
|
||||||
setUser(JSON.parse(userData))
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await fetch('/api/auth/logout', { method: 'POST' })
|
await fetch('/api/auth/logout', { method: 'POST' })
|
||||||
localStorage.removeItem('user')
|
await refreshUser()
|
||||||
setUser(null)
|
|
||||||
router.push('/')
|
router.push('/')
|
||||||
router.refresh()
|
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