Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
136 lines
3.6 KiB
TypeScript
136 lines
3.6 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useEffect, useState, type ReactNode } from "react"
|
|
import { usePathname, useRouter } from "next/navigation"
|
|
import { useAuthStore, AUTH_STORAGE_KEY, LEGACY_AUTH_KEYS } from "@/lib/api/auth-store"
|
|
import { isOidcConfigured } from "@/lib/auth/oidc-config"
|
|
import {
|
|
fetchSession,
|
|
applySessionToStore,
|
|
type SessionPayload,
|
|
} from "@/lib/auth/session-sync"
|
|
import {
|
|
isSessionExpired,
|
|
useSessionGuardStore,
|
|
} from "@/lib/auth/session-guard-store"
|
|
import { isAuthPublicPath } from "@/lib/auth/public-paths"
|
|
|
|
const REFRESH_LEAD_MS = 5 * 60 * 1000
|
|
const REFRESH_CHECK_MS = 60 * 1000
|
|
|
|
function isPublicPath(pathname: string) {
|
|
return isAuthPublicPath(pathname)
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const pathname = usePathname()
|
|
const router = useRouter()
|
|
const logout = useAuthStore((s) => s.logout)
|
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
|
|
const [ready, setReady] = useState(() => !isOidcConfigured())
|
|
|
|
const applySession = useCallback(
|
|
(data: SessionPayload) => applySessionToStore(data),
|
|
[]
|
|
)
|
|
|
|
const syncSession = useCallback(async () => {
|
|
const data = await fetchSession()
|
|
if (data && applySession(data)) return true
|
|
logout()
|
|
return false
|
|
}, [applySession, logout])
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
async function bootstrap() {
|
|
if (!isOidcConfigured()) {
|
|
setReady(true)
|
|
return
|
|
}
|
|
|
|
const data = await fetchSession()
|
|
if (cancelled) return
|
|
|
|
if (data && applySession(data)) {
|
|
setReady(true)
|
|
return
|
|
}
|
|
|
|
const hadMemoryAuth = useAuthStore.getState().isAuthenticated()
|
|
logout()
|
|
if (hadMemoryAuth && !isPublicPath(pathname) && !isSessionExpired()) {
|
|
useSessionGuardStore.getState().setExpired()
|
|
}
|
|
setReady(true)
|
|
}
|
|
|
|
if (!useAuthStore.persist.hasHydrated()) {
|
|
const unsubHydrate = useAuthStore.persist.onFinishHydration(() => {
|
|
void bootstrap()
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
unsubHydrate()
|
|
}
|
|
}
|
|
|
|
void bootstrap()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [applySession, logout, pathname])
|
|
|
|
useEffect(() => {
|
|
if (!ready || !isOidcConfigured()) return
|
|
|
|
const interval = setInterval(() => {
|
|
const { accessToken, expiresAt } = useAuthStore.getState()
|
|
if (!accessToken || !expiresAt) return
|
|
if (Date.now() >= expiresAt - REFRESH_LEAD_MS) {
|
|
void syncSession()
|
|
}
|
|
}, REFRESH_CHECK_MS)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [ready, syncSession])
|
|
|
|
useEffect(() => {
|
|
if (!ready || !isOidcConfigured()) return
|
|
if (isPublicPath(pathname)) return
|
|
if (isAuthenticated()) return
|
|
|
|
let cancelled = false
|
|
void syncSession().then((ok) => {
|
|
if (cancelled || ok) return
|
|
if (useSessionGuardStore.getState().status === "expired") return
|
|
const returnTo = encodeURIComponent(pathname)
|
|
router.replace(`/login?returnTo=${returnTo}`)
|
|
})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [ready, pathname, isAuthenticated, router, syncSession])
|
|
|
|
return <>{children}</>
|
|
}
|
|
|
|
export function useAuthLogout() {
|
|
const logout = useAuthStore((s) => s.logout)
|
|
const router = useRouter()
|
|
|
|
return async () => {
|
|
await fetch("/api/auth/logout", { method: "POST", credentials: "include" })
|
|
logout()
|
|
if (typeof window !== "undefined") {
|
|
localStorage.removeItem(AUTH_STORAGE_KEY)
|
|
for (const legacy of LEGACY_AUTH_KEYS) {
|
|
localStorage.removeItem(legacy)
|
|
}
|
|
}
|
|
router.replace("/login")
|
|
}
|
|
}
|