- Updated .env.example to include configuration for OnlyOffice Document Server. - Modified the workspace configuration to remove the drive-suite path. - Adjusted TypeScript environment imports for consistency. - Enhanced Next.js configuration to disable canvas in Webpack. - Updated package.json to include new dependencies for OnlyOffice and PDF.js. - Added global styles for OnlyOffice theme integration in the CSS. - Created new layout and page components for the Drive feature, including public sharing and editing functionalities. - Updated metadata handling across various layouts to reflect the new app structure.
171 lines
4.3 KiB
TypeScript
171 lines
4.3 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 type { PlatformUser } from "@/lib/auth/jwt-claims"
|
|
|
|
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)
|
|
)
|
|
}
|
|
|
|
type SessionPayload = {
|
|
authenticated?: boolean
|
|
accessToken?: string
|
|
refreshToken?: string | null
|
|
expiresAt?: number
|
|
user?: PlatformUser | null
|
|
}
|
|
|
|
async function fetchSession(): Promise<SessionPayload | null> {
|
|
try {
|
|
const res = await fetch("/api/auth/session", { credentials: "include" })
|
|
if (!res.ok) return null
|
|
return (await res.json()) as SessionPayload
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function canTrustPersistedAuth() {
|
|
return useAuthStore.persist.hasHydrated() && useAuthStore.getState().isAuthenticated()
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const pathname = usePathname()
|
|
const router = useRouter()
|
|
const login = useAuthStore((s) => s.login)
|
|
const logout = useAuthStore((s) => s.logout)
|
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
|
|
const [ready, setReady] = useState(
|
|
() => !isOidcConfigured() || canTrustPersistedAuth()
|
|
)
|
|
|
|
const applySession = useCallback(
|
|
(data: SessionPayload) => {
|
|
if (data.authenticated && data.accessToken && data.expiresAt) {
|
|
login(
|
|
data.accessToken,
|
|
data.refreshToken ?? "",
|
|
data.expiresAt,
|
|
data.user ?? null
|
|
)
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
[login]
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
if (canTrustPersistedAuth()) {
|
|
setReady(true)
|
|
}
|
|
|
|
const data = await fetchSession()
|
|
if (cancelled) return
|
|
|
|
if (data && applySession(data)) {
|
|
setReady(true)
|
|
return
|
|
}
|
|
|
|
if (data?.authenticated === false || !canTrustPersistedAuth()) {
|
|
logout()
|
|
}
|
|
setReady(true)
|
|
}
|
|
|
|
if (!useAuthStore.persist.hasHydrated()) {
|
|
const unsubHydrate = useAuthStore.persist.onFinishHydration(() => {
|
|
if (useAuthStore.getState().isAuthenticated()) {
|
|
setReady(true)
|
|
}
|
|
})
|
|
void bootstrap()
|
|
return () => {
|
|
cancelled = true
|
|
unsubHydrate()
|
|
}
|
|
}
|
|
|
|
void bootstrap()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [applySession, logout])
|
|
|
|
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
|
|
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")
|
|
}
|
|
}
|