| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||
| import type { ReactNode } from 'react' | import type { ReactNode } from 'react' | ||||
| import { useRouter, useSearchParams } from 'next/navigation' | import { useRouter, useSearchParams } from 'next/navigation' | ||||
| import useRefreshToken from '@/hooks/use-refresh-token' | |||||
| type SwrInitorProps = { | type SwrInitorProps = { | ||||
| children: ReactNode | children: ReactNode | ||||
| }: SwrInitorProps) => { | }: SwrInitorProps) => { | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const searchParams = useSearchParams() | const searchParams = useSearchParams() | ||||
| const consoleToken = searchParams.get('console_token') | |||||
| const consoleToken = searchParams.get('access_token') | |||||
| const refreshToken = searchParams.get('refresh_token') | |||||
| const consoleTokenFromLocalStorage = localStorage?.getItem('console_token') | const consoleTokenFromLocalStorage = localStorage?.getItem('console_token') | ||||
| const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token') | |||||
| const [init, setInit] = useState(false) | const [init, setInit] = useState(false) | ||||
| const { getNewAccessToken } = useRefreshToken() | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!(consoleToken || consoleTokenFromLocalStorage)) | |||||
| if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage)) { | |||||
| router.replace('/signin') | router.replace('/signin') | ||||
| return | |||||
| } | |||||
| if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage) | |||||
| getNewAccessToken(consoleTokenFromLocalStorage, refreshTokenFromLocalStorage) | |||||
| if (consoleToken) { | |||||
| localStorage?.setItem('console_token', consoleToken!) | |||||
| router.replace('/apps', { forceOptimisticNavigation: false } as any) | |||||
| if (consoleToken && refreshToken) { | |||||
| localStorage.setItem('console_token', consoleToken) | |||||
| localStorage.setItem('refresh_token', refreshToken) | |||||
| getNewAccessToken(consoleToken, refreshToken).then(() => { | |||||
| router.replace('/apps', { forceOptimisticNavigation: false } as any) | |||||
| }).catch(() => { | |||||
| router.replace('/signin') | |||||
| }) | |||||
| } | } | ||||
| setInit(true) | setInit(true) | ||||
| }, []) | }, []) | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { login, oauth } from '@/service/common' | import { login, oauth } from '@/service/common' | ||||
| import { getPurifyHref } from '@/utils' | import { getPurifyHref } from '@/utils' | ||||
| import useRefreshToken from '@/hooks/use-refresh-token' | |||||
| type IState = { | type IState = { | ||||
| formValid: boolean | formValid: boolean | ||||
| const NormalForm = () => { | const NormalForm = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { getNewAccessToken } = useRefreshToken() | |||||
| const useEmailLogin = IS_CE_EDITION || SUPPORT_MAIL_LOGIN | const useEmailLogin = IS_CE_EDITION || SUPPORT_MAIL_LOGIN | ||||
| const router = useRouter() | const router = useRouter() | ||||
| }, | }, | ||||
| }) | }) | ||||
| if (res.result === 'success') { | if (res.result === 'success') { | ||||
| localStorage.setItem('console_token', res.data) | |||||
| localStorage.setItem('console_token', res.data.access_token) | |||||
| localStorage.setItem('refresh_token', res.data.refresh_token) | |||||
| getNewAccessToken(res.data.access_token, res.data.refresh_token) | |||||
| router.replace('/apps') | router.replace('/apps') | ||||
| } | } | ||||
| else { | else { |
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| import { getUserOAuth2SSOUrl, getUserOIDCSSOUrl, getUserSAMLSSOUrl } from '@/service/sso' | import { getUserOAuth2SSOUrl, getUserOIDCSSOUrl, getUserSAMLSSOUrl } from '@/service/sso' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import useRefreshToken from '@/hooks/use-refresh-token' | |||||
| type UserSSOFormProps = { | type UserSSOFormProps = { | ||||
| protocol: string | protocol: string | ||||
| const UserSSOForm: FC<UserSSOFormProps> = ({ | const UserSSOForm: FC<UserSSOFormProps> = ({ | ||||
| protocol, | protocol, | ||||
| }) => { | }) => { | ||||
| const { getNewAccessToken } = useRefreshToken() | |||||
| const searchParams = useSearchParams() | const searchParams = useSearchParams() | ||||
| const consoleToken = searchParams.get('console_token') | |||||
| const consoleToken = searchParams.get('access_token') | |||||
| const refreshToken = searchParams.get('refresh_token') | |||||
| const message = searchParams.get('message') | const message = searchParams.get('message') | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (consoleToken) { | |||||
| if (refreshToken && consoleToken) { | |||||
| localStorage.setItem('console_token', consoleToken) | localStorage.setItem('console_token', consoleToken) | ||||
| localStorage.setItem('refresh_token', refreshToken) | |||||
| getNewAccessToken(consoleToken, refreshToken) | |||||
| router.replace('/apps') | router.replace('/apps') | ||||
| } | } | ||||
| message, | message, | ||||
| }) | }) | ||||
| } | } | ||||
| }, []) | |||||
| }, [consoleToken, refreshToken, message, router]) | |||||
| const handleSSOLogin = () => { | const handleSSOLogin = () => { | ||||
| setIsLoading(true) | setIsLoading(true) |
| 'use client' | |||||
| import { useCallback, useEffect, useRef } from 'react' | |||||
| import { jwtDecode } from 'jwt-decode' | |||||
| import dayjs from 'dayjs' | |||||
| import utc from 'dayjs/plugin/utc' | |||||
| import { useRouter } from 'next/navigation' | |||||
| import type { CommonResponse } from '@/models/common' | |||||
| import { fetchNewToken } from '@/service/common' | |||||
| import { fetchWithRetry } from '@/utils' | |||||
| dayjs.extend(utc) | |||||
| const useRefreshToken = () => { | |||||
| const router = useRouter() | |||||
| const timer = useRef<NodeJS.Timeout>() | |||||
| const advanceTime = useRef<number>(5 * 60 * 1000) | |||||
| const interval = useRef<number>(55 * 60 * 1000) | |||||
| const getExpireTime = useCallback((token: string) => { | |||||
| if (!token) | |||||
| return 0 | |||||
| const decoded = jwtDecode(token) | |||||
| return (decoded.exp || 0) * 1000 | |||||
| }, []) | |||||
| const getCurrentTimeStamp = useCallback(() => { | |||||
| return dayjs.utc().valueOf() | |||||
| }, []) | |||||
| const handleError = useCallback(() => { | |||||
| localStorage?.removeItem('is_refreshing') | |||||
| localStorage?.removeItem('console_token') | |||||
| localStorage?.removeItem('refresh_token') | |||||
| localStorage?.removeItem('last_refresh_time') | |||||
| router.replace('/signin') | |||||
| }, []) | |||||
| const getNewAccessToken = useCallback(async (currentAccessToken: string, currentRefreshToken: string) => { | |||||
| if (localStorage?.getItem('is_refreshing') === '1') | |||||
| return null | |||||
| const currentTokenExpireTime = getExpireTime(currentAccessToken) | |||||
| let lastRefreshTime = parseInt(localStorage?.getItem('last_refresh_time') || '0') | |||||
| lastRefreshTime = isNaN(lastRefreshTime) ? 0 : lastRefreshTime | |||||
| if (getCurrentTimeStamp() + advanceTime.current > currentTokenExpireTime | |||||
| && lastRefreshTime + interval.current < getCurrentTimeStamp()) { | |||||
| localStorage?.setItem('is_refreshing', '1') | |||||
| const [e, res] = await fetchWithRetry(fetchNewToken({ | |||||
| body: { refresh_token: currentRefreshToken }, | |||||
| }) as Promise<CommonResponse & { data: { access_token: string; refresh_token: string } }>) | |||||
| if (e) { | |||||
| handleError() | |||||
| return e | |||||
| } | |||||
| const { access_token, refresh_token } = res.data | |||||
| localStorage?.setItem('is_refreshing', '0') | |||||
| localStorage?.setItem('last_refresh_time', getCurrentTimeStamp().toString()) | |||||
| localStorage?.setItem('console_token', access_token) | |||||
| localStorage?.setItem('refresh_token', refresh_token) | |||||
| const newTokenExpireTime = getExpireTime(access_token) | |||||
| timer.current = setTimeout(() => { | |||||
| const consoleTokenFromLocalStorage = localStorage?.getItem('console_token') | |||||
| const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token') | |||||
| if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage) | |||||
| getNewAccessToken(consoleTokenFromLocalStorage, refreshTokenFromLocalStorage) | |||||
| }, newTokenExpireTime - advanceTime.current - getCurrentTimeStamp()) | |||||
| } | |||||
| else { | |||||
| const newTokenExpireTime = getExpireTime(currentAccessToken) | |||||
| timer.current = setTimeout(() => { | |||||
| const consoleTokenFromLocalStorage = localStorage?.getItem('console_token') | |||||
| const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token') | |||||
| if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage) | |||||
| getNewAccessToken(consoleTokenFromLocalStorage, refreshTokenFromLocalStorage) | |||||
| }, newTokenExpireTime - advanceTime.current - getCurrentTimeStamp()) | |||||
| } | |||||
| return null | |||||
| }, [getExpireTime, getCurrentTimeStamp, handleError]) | |||||
| useEffect(() => { | |||||
| return () => { | |||||
| clearTimeout(timer.current) | |||||
| localStorage?.removeItem('is_refreshing') | |||||
| localStorage?.removeItem('last_refresh_time') | |||||
| } | |||||
| }, []) | |||||
| return { | |||||
| getNewAccessToken, | |||||
| } | |||||
| } | |||||
| export default useRefreshToken |
| "immer": "^9.0.19", | "immer": "^9.0.19", | ||||
| "js-audio-recorder": "^1.0.7", | "js-audio-recorder": "^1.0.7", | ||||
| "js-cookie": "^3.0.1", | "js-cookie": "^3.0.1", | ||||
| "jwt-decode": "^4.0.0", | |||||
| "katex": "^0.16.10", | "katex": "^0.16.10", | ||||
| "lamejs": "^1.2.1", | "lamejs": "^1.2.1", | ||||
| "lexical": "^0.16.0", | "lexical": "^0.16.0", |
| import type { RETRIEVE_METHOD } from '@/types/app' | import type { RETRIEVE_METHOD } from '@/types/app' | ||||
| import type { SystemFeatures } from '@/types/feature' | import type { SystemFeatures } from '@/types/feature' | ||||
| export const login: Fetcher<CommonResponse & { data: string }, { url: string; body: Record<string, any> }> = ({ url, body }) => { | |||||
| return post(url, { body }) as Promise<CommonResponse & { data: string }> | |||||
| type LoginSuccess = { | |||||
| result: 'success' | |||||
| data: { access_token: string;refresh_token: string } | |||||
| } | |||||
| type LoginFail = { | |||||
| result: 'fail' | |||||
| data: string | |||||
| } | |||||
| type LoginResponse = LoginSuccess | LoginFail | |||||
| export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { | |||||
| return post(url, { body }) as Promise<LoginResponse> | |||||
| } | |||||
| export const fetchNewToken: Fetcher<CommonResponse & { data: { access_token: string; refresh_token: string } }, { body: Record<string, any> }> = ({ body }) => { | |||||
| return post('/refresh-token', { body }) as Promise<CommonResponse & { data: { access_token: string; refresh_token: string } }> | |||||
| } | } | ||||
| export const setup: Fetcher<CommonResponse, { body: Record<string, any> }> = ({ body }) => { | export const setup: Fetcher<CommonResponse, { body: Record<string, any> }> = ({ body }) => { |
| return escape(href) | return escape(href) | ||||
| } | } | ||||
| export async function fetchWithRetry<T = any>(fn: Promise<T>, retries = 3): Promise<[Error] | [null, T]> { | |||||
| const [error, res] = await asyncRunSafe(fn) | |||||
| if (error) { | |||||
| if (retries > 0) { | |||||
| const res = await fetchWithRetry(fn, retries - 1) | |||||
| return res | |||||
| } | |||||
| else { | |||||
| if (error instanceof Error) | |||||
| return [error] | |||||
| return [new Error('unknown error')] | |||||
| } | |||||
| } | |||||
| else { | |||||
| return [null, res] | |||||
| } | |||||
| } |
| array-includes "^3.1.5" | array-includes "^3.1.5" | ||||
| object.assign "^4.1.3" | object.assign "^4.1.3" | ||||
| jwt-decode@^4.0.0: | |||||
| version "4.0.0" | |||||
| resolved "https://registry.npmmirror.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" | |||||
| integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== | |||||
| katex@^0.16.0, katex@^0.16.10: | katex@^0.16.0, katex@^0.16.10: | ||||
| version "0.16.10" | version "0.16.10" | ||||
| resolved "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz" | resolved "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz" |