ultisuite-client/components/auth/auth-provider.tsx
R3D347HR4Y 5304790ed5
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance session management and identity provider settings
- Added SessionGuard component to manage session expiration and online status.
- Updated AuthProvider to streamline session fetching and handling.
- Introduced IdentityProvidersSection for managing OAuth, SAML, and LDAP identity providers.
- Implemented identity provider guides for easier configuration.
- Enhanced mail settings with infinite scroll option for improved user experience.
- Updated global styles and layout components for better consistency across the application.
2026-06-09 09:36:46 +02:00

139 lines
3.7 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"
const PUBLIC_PREFIXES = ["/login", "/auth/", "/api/auth/"]
const REFRESH_LEAD_MS = 5 * 60 * 1000
const REFRESH_CHECK_MS = 60 * 1000
function isPublicPath(pathname: string) {
if (pathname.startsWith("/drive/s/")) return true
return PUBLIC_PREFIXES.some(
(prefix) => pathname === prefix || pathname.startsWith(prefix)
)
}
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")
}
}