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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -9,22 +10,19 @@ import {
|
|||||||
} from '@linagora/twake-mui'
|
} from '@linagora/twake-mui'
|
||||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
|
||||||
import ReplayIcon from '@mui/icons-material/Replay'
|
import ReplayIcon from '@mui/icons-material/Replay'
|
||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import { push } from 'redux-first-history'
|
|
||||||
import { useI18n } from 'twake-i18n'
|
import { useI18n } from 'twake-i18n'
|
||||||
|
|
||||||
export function Error() {
|
export const Error: React.FC = () => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const userError = useAppSelector(state => state.user.error)
|
const userError = useAppSelector(state => state.user.error)
|
||||||
const calendarError = useAppSelector(state => state.calendars.error)
|
const calendarError = useAppSelector(state => state.calendars.error)
|
||||||
const initialUserError = useRef(userError)
|
const initialError = useRef(userError || calendarError)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialUserError.current) {
|
if (!initialError.current) {
|
||||||
dispatch(push('/'))
|
window.location.replace('/')
|
||||||
}
|
}
|
||||||
}, [dispatch])
|
}, [])
|
||||||
|
|
||||||
const errorMessage = userError || calendarError || t('error.unknown')
|
const errorMessage = userError || calendarError || t('error.unknown')
|
||||||
|
|
||||||
@@ -69,7 +67,11 @@ export function Error() {
|
|||||||
{t('error.title')}
|
{t('error.title')}
|
||||||
</Typography>
|
</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}
|
{errorMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ export function Error() {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="error"
|
color="error"
|
||||||
startIcon={<ReplayIcon />}
|
startIcon={<ReplayIcon />}
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => (window.location.href = '/')}
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
|||||||
+92
-52
@@ -1,7 +1,17 @@
|
|||||||
import { Auth } from '@/features/User/oidcAuth'
|
import { Auth } from '@/features/User/oidcAuth'
|
||||||
import { assertWebSocketAlive } from '@/websocket/connection/lifecycle/assertWebSocketAlive'
|
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 { getRetryDelay } from './getRetryDelay'
|
||||||
|
import {
|
||||||
|
TokenEndpointResponse,
|
||||||
|
TokenEndpointResponseHelpers
|
||||||
|
} from 'openid-client'
|
||||||
|
|
||||||
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
|
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
|
||||||
|
|
||||||
@@ -11,50 +21,40 @@ const RETRY_CONFIG = {
|
|||||||
maxDelay: 120000
|
maxDelay: 120000
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = ky.extend({
|
let isRedirectingToSso = false
|
||||||
prefixUrl: window.CALENDAR_BASE_URL,
|
|
||||||
retry: {
|
const redirectSSO = async (
|
||||||
limit: RETRY_CONFIG.maxRetries,
|
response: KyResponse,
|
||||||
backoffLimit: RETRY_CONFIG.maxDelay,
|
request: KyRequest,
|
||||||
delay: attemptCount =>
|
options: NormalizedOptions
|
||||||
getRetryDelay(attemptCount - 1, {
|
): Promise<void> => {
|
||||||
initialDelay: RETRY_CONFIG.initialDelay,
|
if (isRedirectingToSso) {
|
||||||
maxDelay: RETRY_CONFIG.maxDelay
|
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: [
|
const handleUnauthorizeRequest = async (
|
||||||
async (request, options, response) => {
|
response: KyResponse,
|
||||||
if (response.status === 401) {
|
request: KyRequest,
|
||||||
|
options: NormalizedOptions
|
||||||
|
): Promise<KyResponse | void> => {
|
||||||
// Check if we're already on login flow to prevent redirect loop
|
// Check if we're already on login flow to prevent redirect loop
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
if (currentPath === '/callback') {
|
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)
|
// 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(
|
export const api: KyInstance = ky.extend({
|
||||||
'redirectState',
|
prefixUrl: window.CALENDAR_BASE_URL,
|
||||||
JSON.stringify({
|
retry: {
|
||||||
code_verifier: loginurl.code_verifier,
|
limit: RETRY_CONFIG.maxRetries,
|
||||||
state: loginurl.state
|
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
|
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)
|
window.location.assign(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLocation() {
|
export function getLocation(): string {
|
||||||
return window.location.href
|
return window.location.href
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidUrl(string?: string) {
|
export function isValidUrl(string?: string): URL | boolean {
|
||||||
let url
|
let url
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -104,7 +144,7 @@ export function isValidUrl(string?: string) {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importFile(file: File) {
|
export async function importFile(file: File): Promise<KyResponse<unknown>> {
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`api/files?mimetype=${file.type}&name=${file.name}&size=${file.size}`,
|
`api/files?mimetype=${file.type}&name=${file.name}&size=${file.size}`,
|
||||||
{ body: await file.text() }
|
{ body: await file.text() }
|
||||||
|
|||||||
Reference in New Issue
Block a user