Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useAppSelector } from '@/app/hooks'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -9,22 +10,19 @@ import {
|
||||
} from '@linagora/twake-mui'
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
|
||||
import ReplayIcon from '@mui/icons-material/Replay'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { push } from 'redux-first-history'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
|
||||
export function Error() {
|
||||
export const Error: React.FC = () => {
|
||||
const { t } = useI18n()
|
||||
const dispatch = useAppDispatch()
|
||||
const userError = useAppSelector(state => state.user.error)
|
||||
const calendarError = useAppSelector(state => state.calendars.error)
|
||||
const initialUserError = useRef(userError)
|
||||
const initialError = useRef(userError || calendarError)
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialUserError.current) {
|
||||
dispatch(push('/'))
|
||||
if (!initialError.current) {
|
||||
window.location.replace('/')
|
||||
}
|
||||
}, [dispatch])
|
||||
}, [])
|
||||
|
||||
const errorMessage = userError || calendarError || t('error.unknown')
|
||||
|
||||
@@ -69,7 +67,11 @@ export function Error() {
|
||||
{t('error.title')}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 2, width: '100%', wordBreak: 'break-all' }}
|
||||
>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
|
||||
@@ -77,7 +79,7 @@ export function Error() {
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<ReplayIcon />}
|
||||
onClick={() => window.location.reload()}
|
||||
onClick={() => (window.location.href = '/')}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
|
||||
+92
-52
@@ -1,7 +1,17 @@
|
||||
import { Auth } from '@/features/User/oidcAuth'
|
||||
import { assertWebSocketAlive } from '@/websocket/connection/lifecycle/assertWebSocketAlive'
|
||||
import ky from 'ky'
|
||||
import ky, {
|
||||
type KyInstance,
|
||||
type KyRequest,
|
||||
type KyResponse,
|
||||
HTTPError,
|
||||
type NormalizedOptions
|
||||
} from 'ky'
|
||||
import { getRetryDelay } from './getRetryDelay'
|
||||
import {
|
||||
TokenEndpointResponse,
|
||||
TokenEndpointResponseHelpers
|
||||
} from 'openid-client'
|
||||
|
||||
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
|
||||
@@ -11,50 +21,40 @@ const RETRY_CONFIG = {
|
||||
maxDelay: 120000
|
||||
}
|
||||
|
||||
export const api = ky.extend({
|
||||
prefixUrl: window.CALENDAR_BASE_URL,
|
||||
retry: {
|
||||
limit: RETRY_CONFIG.maxRetries,
|
||||
backoffLimit: RETRY_CONFIG.maxDelay,
|
||||
delay: attemptCount =>
|
||||
getRetryDelay(attemptCount - 1, {
|
||||
initialDelay: RETRY_CONFIG.initialDelay,
|
||||
maxDelay: RETRY_CONFIG.maxDelay
|
||||
let isRedirectingToSso = false
|
||||
|
||||
const redirectSSO = async (
|
||||
response: KyResponse,
|
||||
request: KyRequest,
|
||||
options: NormalizedOptions
|
||||
): Promise<void> => {
|
||||
if (isRedirectingToSso) {
|
||||
return
|
||||
}
|
||||
isRedirectingToSso = true
|
||||
try {
|
||||
const loginurl = await Auth()
|
||||
|
||||
sessionStorage.setItem(
|
||||
'redirectState',
|
||||
JSON.stringify({
|
||||
code_verifier: loginurl.code_verifier,
|
||||
state: loginurl.state
|
||||
})
|
||||
},
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async request => {
|
||||
const saved = sessionStorage.getItem('tokenSet')
|
||||
? JSON.parse(sessionStorage.getItem('tokenSet') ?? '{}')
|
||||
: null
|
||||
const access_token = saved?.access_token
|
||||
if (access_token) {
|
||||
request.headers.set('Authorization', `Bearer ${access_token}`)
|
||||
}
|
||||
|
||||
if (MUTATING_METHODS.has(request.method)) {
|
||||
await assertWebSocketAlive()
|
||||
}
|
||||
return request
|
||||
}
|
||||
],
|
||||
|
||||
beforeRetry: [
|
||||
({ request, error, retryCount }) => {
|
||||
console.warn(
|
||||
`[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`,
|
||||
{
|
||||
url: request.url,
|
||||
error: error?.message
|
||||
}
|
||||
)
|
||||
redirectTo(loginurl.redirectTo)
|
||||
} catch (error) {
|
||||
isRedirectingToSso = false
|
||||
console.error('SSO Redirect failed:', error)
|
||||
throw new HTTPError(response, request, options)
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
afterResponse: [
|
||||
async (request, options, response) => {
|
||||
if (response.status === 401) {
|
||||
const handleUnauthorizeRequest = async (
|
||||
response: KyResponse,
|
||||
request: KyRequest,
|
||||
options: NormalizedOptions
|
||||
): Promise<KyResponse | void> => {
|
||||
// Check if we're already on login flow to prevent redirect loop
|
||||
const currentPath = window.location.pathname
|
||||
if (currentPath === '/callback') {
|
||||
@@ -68,16 +68,56 @@ export const api = ky.extend({
|
||||
}
|
||||
|
||||
// Only redirect to SSO if we're sure token is invalid (not just missing)
|
||||
const loginurl = await Auth()
|
||||
await redirectSSO(response, request, options)
|
||||
}
|
||||
|
||||
sessionStorage.setItem(
|
||||
'redirectState',
|
||||
JSON.stringify({
|
||||
code_verifier: loginurl.code_verifier,
|
||||
state: loginurl.state
|
||||
export const api: KyInstance = ky.extend({
|
||||
prefixUrl: window.CALENDAR_BASE_URL,
|
||||
retry: {
|
||||
limit: RETRY_CONFIG.maxRetries,
|
||||
backoffLimit: RETRY_CONFIG.maxDelay,
|
||||
delay: attemptCount =>
|
||||
getRetryDelay(attemptCount - 1, {
|
||||
initialDelay: RETRY_CONFIG.initialDelay,
|
||||
maxDelay: RETRY_CONFIG.maxDelay
|
||||
})
|
||||
},
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async (request: KyRequest): Promise<KyRequest> => {
|
||||
const saved = sessionStorage.getItem('tokenSet')
|
||||
? (JSON.parse(
|
||||
sessionStorage.getItem('tokenSet') ?? '{}'
|
||||
) as TokenEndpointResponse & TokenEndpointResponseHelpers)
|
||||
: null
|
||||
const access_token = saved?.access_token as string
|
||||
if (access_token) {
|
||||
request.headers.set('Authorization', `Bearer ${access_token}`)
|
||||
}
|
||||
|
||||
if (MUTATING_METHODS.has(request.method)) {
|
||||
await assertWebSocketAlive()
|
||||
}
|
||||
return request
|
||||
}
|
||||
],
|
||||
|
||||
beforeRetry: [
|
||||
({ request, error, retryCount }): void => {
|
||||
console.warn(
|
||||
`[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`,
|
||||
{
|
||||
url: request.url,
|
||||
error: error?.message
|
||||
}
|
||||
)
|
||||
redirectTo(loginurl.redirectTo)
|
||||
}
|
||||
],
|
||||
|
||||
afterResponse: [
|
||||
async (request, options, response): Promise<KyResponse> => {
|
||||
if (response.status === 401) {
|
||||
await handleUnauthorizeRequest(response, request, options)
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -85,15 +125,15 @@ export const api = ky.extend({
|
||||
}
|
||||
})
|
||||
|
||||
export function redirectTo(url: URL) {
|
||||
export function redirectTo(url: URL): void {
|
||||
window.location.assign(url)
|
||||
}
|
||||
|
||||
export function getLocation() {
|
||||
export function getLocation(): string {
|
||||
return window.location.href
|
||||
}
|
||||
|
||||
export function isValidUrl(string?: string) {
|
||||
export function isValidUrl(string?: string): URL | boolean {
|
||||
let url
|
||||
|
||||
try {
|
||||
@@ -104,7 +144,7 @@ export function isValidUrl(string?: string) {
|
||||
return url
|
||||
}
|
||||
|
||||
export async function importFile(file: File) {
|
||||
export async function importFile(file: File): Promise<KyResponse<unknown>> {
|
||||
const response = await api.post(
|
||||
`api/files?mimetype=${file.type}&name=${file.name}&size=${file.size}`,
|
||||
{ body: await file.text() }
|
||||
|
||||
Reference in New Issue
Block a user