Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
"use client"
|
|
|
|
import { create } from "zustand"
|
|
import { persist } from "zustand/middleware"
|
|
import { debouncedPersistJSONStorage } from "@/lib/stores/debounced-json-storage"
|
|
import { useSessionGuardStore } from "@/lib/auth/session-guard-store"
|
|
import type { PlatformUser } from "@/lib/auth/jwt-claims"
|
|
|
|
const AUTH_STORAGE_KEY = "ulti-auth"
|
|
const LEGACY_AUTH_KEYS = ["ultimail-auth", "ultidrive-auth"] as const
|
|
|
|
export { AUTH_STORAGE_KEY, LEGACY_AUTH_KEYS }
|
|
|
|
function migrateLegacyAuthStorage() {
|
|
if (typeof window === "undefined") return
|
|
try {
|
|
if (localStorage.getItem(AUTH_STORAGE_KEY)) return
|
|
for (const legacy of LEGACY_AUTH_KEYS) {
|
|
const raw = localStorage.getItem(legacy)
|
|
if (raw) {
|
|
localStorage.setItem(AUTH_STORAGE_KEY, raw)
|
|
return
|
|
}
|
|
}
|
|
} catch {
|
|
/* private mode / quota */
|
|
}
|
|
}
|
|
|
|
migrateLegacyAuthStorage()
|
|
|
|
interface AuthState {
|
|
accessToken: string | null
|
|
refreshToken: string | null
|
|
expiresAt: number | null
|
|
user: PlatformUser | null
|
|
login: (
|
|
accessToken: string,
|
|
refreshToken: string,
|
|
expiresAt: number,
|
|
user?: PlatformUser | null
|
|
) => void
|
|
logout: () => void
|
|
isAuthenticated: () => boolean
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
accessToken: null,
|
|
refreshToken: null,
|
|
expiresAt: null,
|
|
user: null,
|
|
login: (accessToken, refreshToken, expiresAt, user = null) => {
|
|
set({ accessToken, refreshToken, expiresAt, user })
|
|
useSessionGuardStore.getState().clear()
|
|
},
|
|
logout: () => {
|
|
set({
|
|
accessToken: null,
|
|
refreshToken: null,
|
|
expiresAt: null,
|
|
user: null,
|
|
})
|
|
if (typeof window !== "undefined") {
|
|
try {
|
|
localStorage.removeItem(AUTH_STORAGE_KEY)
|
|
for (const legacy of LEGACY_AUTH_KEYS) {
|
|
localStorage.removeItem(legacy)
|
|
}
|
|
} catch {
|
|
/* private mode / quota */
|
|
}
|
|
}
|
|
},
|
|
isAuthenticated: () => {
|
|
const { accessToken, expiresAt, refreshToken } = get()
|
|
if (!accessToken) return false
|
|
if (expiresAt && Date.now() < expiresAt) return true
|
|
// Access token expired — session may still be renewed via refresh token.
|
|
return Boolean(refreshToken)
|
|
},
|
|
}),
|
|
{
|
|
name: AUTH_STORAGE_KEY,
|
|
storage: debouncedPersistJSONStorage,
|
|
version: 1,
|
|
migrate: (persisted) => {
|
|
const state = (persisted as { state?: AuthState }).state
|
|
if (state) {
|
|
state.accessToken = null
|
|
state.refreshToken = null
|
|
state.expiresAt = null
|
|
}
|
|
return persisted as { state: AuthState; version: number }
|
|
},
|
|
// Tokens stay in httpOnly cookies + in-memory store (via /api/auth/session).
|
|
partialize: (state) => ({
|
|
user: state.user,
|
|
}),
|
|
}
|
|
)
|
|
)
|