feat: implement refresh token functionality and update local storage management
This commit is contained in:
@@ -51,9 +51,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
const refreshToken = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
localStorage.setItem('ciphera_token_refreshed_at', Date.now().toString())
|
||||||
|
}
|
||||||
|
return res.ok
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const login = (userData: User) => {
|
const login = (userData: User) => {
|
||||||
// * We still store user profile in localStorage for optimistic UI, but NOT the token
|
// * We still store user profile in localStorage for optimistic UI, but NOT the token
|
||||||
localStorage.setItem('user', JSON.stringify(userData))
|
localStorage.setItem('user', JSON.stringify(userData))
|
||||||
|
localStorage.setItem('ciphera_token_refreshed_at', Date.now().toString())
|
||||||
setUser(userData)
|
setUser(userData)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
// * Fetch full profile (including display_name) so header shows correct name without page refresh
|
// * Fetch full profile (including display_name) so header shows correct name without page refresh
|
||||||
@@ -76,6 +92,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setIsLoggingOut(true)
|
setIsLoggingOut(true)
|
||||||
await logoutAction()
|
await logoutAction()
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem('user')
|
||||||
|
localStorage.removeItem('ciphera_token_refreshed_at')
|
||||||
|
localStorage.removeItem('ciphera_last_activity')
|
||||||
// * Broadcast logout to other tabs (BroadcastChannel will handle if available)
|
// * Broadcast logout to other tabs (BroadcastChannel will handle if available)
|
||||||
if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
|
if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
|
||||||
const channel = new BroadcastChannel('ciphera_session')
|
const channel = new BroadcastChannel('ciphera_session')
|
||||||
@@ -131,6 +149,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (session) {
|
if (session) {
|
||||||
setUser(session)
|
setUser(session)
|
||||||
localStorage.setItem('user', JSON.stringify(session))
|
localStorage.setItem('user', JSON.stringify(session))
|
||||||
|
localStorage.setItem('ciphera_token_refreshed_at', Date.now().toString())
|
||||||
// * Fetch full profile (including display_name) from API; preserve org_id/role from session
|
// * Fetch full profile (including display_name) from API; preserve org_id/role from session
|
||||||
try {
|
try {
|
||||||
const userData = await apiRequest<User>('/auth/user/me')
|
const userData = await apiRequest<User>('/auth/user/me')
|
||||||
@@ -221,7 +240,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
{isLoggingOut && <LoadingOverlay logoSrc="/pulse_icon_no_margins.png" title="Pulse" />}
|
{isLoggingOut && <LoadingOverlay logoSrc="/pulse_icon_no_margins.png" title="Pulse" />}
|
||||||
<SessionExpiryWarning
|
<SessionExpiryWarning
|
||||||
isAuthenticated={!!user}
|
isAuthenticated={!!user}
|
||||||
onExtendSession={refresh}
|
onRefreshToken={refreshToken}
|
||||||
onExpired={logout}
|
onExpired={logout}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.11.1-alpha",
|
"version": "0.11.1-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.77",
|
"@ciphera-net/ui": "^0.0.78",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
@@ -1543,9 +1543,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ciphera-net/ui": {
|
"node_modules/@ciphera-net/ui": {
|
||||||
"version": "0.0.77",
|
"version": "0.0.78",
|
||||||
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.77/c243fce3b29ee4b3d90dd5b6b5a7d93ff3a955b9",
|
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.78/d012b211ddba7a83f7468ec269a842709a2409b9",
|
||||||
"integrity": "sha512-Z+aLef5843t9TIvaSV+ecC4sQi2VzX+hE6/7A/4/Y49CT91vg0x9QuQuQiV7B4j93Ui1yv+aZyWN21NY3mhKbA==",
|
"integrity": "sha512-c9B/cZggjWnCSpICEvRBAAPgsRfjN2j3NFczQRZxRR6sZmzkwd3KzEbDfTEa2DUExBMWB4+bOgiCz+AnP2OR3g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.77",
|
"@ciphera-net/ui": "^0.0.78",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user