From 90feb0e9637d1eaeffd9e98c09dd6c81d42c61dd Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Thu, 8 Aug 2024 12:28:26 -0500 Subject: [PATCH] Clearer RefreshTokenError reasons and login page message. --- src/AuthAwareQueryClientProvider.tsx | 8 ++++---- src/api/refresh.ts | 14 +++++++++----- src/api/types/LoginExceptionView.ts | 6 ++++++ src/routes/login.tsx | 17 +++++++++++++---- 4 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 src/api/types/LoginExceptionView.ts diff --git a/src/AuthAwareQueryClientProvider.tsx b/src/AuthAwareQueryClientProvider.tsx index 82ca3fa..6459973 100644 --- a/src/AuthAwareQueryClientProvider.tsx +++ b/src/AuthAwareQueryClientProvider.tsx @@ -4,7 +4,7 @@ import { useRouter } from '@tanstack/react-router' import React, { useState } from 'react' import { ApiError } from './api/ApiError' import ExpiredTokenError from './api/ExpiredTokenError' -import refresh, { ExpiredRefreshTokenError } from './api/refresh' +import refresh, { RefreshTokenError } from './api/refresh' import LoginView from './api/types/LoginView' import { useAuth } from './auth' @@ -21,14 +21,14 @@ const AuthAwareQueryClientProvider = ({ children }: React.PropsWithChildren) => try { refreshResult = await refresh() } catch (error) { - if (error instanceof ExpiredRefreshTokenError) { - console.log('refresh-token expired') + if (error instanceof RefreshTokenError) { + console.log(`RefreshTokenError: ${error.reason}`) setCurrentlyRefreshing(false) clearToken() await router.navigate({ to: '/login', search: { - expired: true, + reason: error.reason, redirect: router.state.location.href } }) diff --git a/src/api/refresh.ts b/src/api/refresh.ts index 10c2dcb..b8dd20c 100644 --- a/src/api/refresh.ts +++ b/src/api/refresh.ts @@ -1,10 +1,13 @@ import { ApiError } from './ApiError' +import LoginExceptionView from './types/LoginExceptionView' import LoginView, { RawLoginView } from './types/LoginView' -export class ExpiredRefreshTokenError extends ApiError { - constructor() { - super(401, 'Expired refresh token.') - Object.setPrototypeOf(this, ExpiredRefreshTokenError.prototype) +export type RefreshTokenErrorReason = 'INVALID_REFRESH_TOKEN' | 'EXPIRED_REFRESH_TOKEN' | 'NO_REFRESH_TOKEN' + +export class RefreshTokenError extends ApiError { + constructor(public reason: RefreshTokenErrorReason) { + super(401, 'Refresh token error.') + Object.setPrototypeOf(this, RefreshTokenError.prototype) } } @@ -31,7 +34,8 @@ const refresh = async (): Promise => { expires: new Date(rawExpires) } } else if (response.status === 401) { - throw new ExpiredRefreshTokenError() + const { reason } = (await response.json()) as LoginExceptionView + throw new RefreshTokenError(reason as RefreshTokenErrorReason) } else { throw new ApiError(response.status, response.statusText) } diff --git a/src/api/types/LoginExceptionView.ts b/src/api/types/LoginExceptionView.ts new file mode 100644 index 0000000..10d84dd --- /dev/null +++ b/src/api/types/LoginExceptionView.ts @@ -0,0 +1,6 @@ +interface LoginExceptionView { + reason: 'INVALID_CREDENTIALS' | 'INVALID_REFRESH_TOKEN' | 'EXPIRED_REFRESH_TOKEN' | 'NO_REFRESH_TOKEN' + message: string +} + +export default LoginExceptionView diff --git a/src/routes/login.tsx b/src/routes/login.tsx index c0ebf0a..6b65cbf 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -10,7 +10,7 @@ const Login = () => { const router = useRouter() const navigate = useNavigate() - const { redirect, expired } = useSearch({ from: '/login' }) + const { redirect, reason } = useSearch({ from: '/login' }) const onSubmit = async (event: FormEvent) => { event.preventDefault() @@ -31,10 +31,19 @@ const Login = () => { } } + let message: string | null = null + if (reason !== undefined) { + if (reason === 'INVALID_REFRESH_TOKEN' || reason === 'EXPIRED_REFRESH_TOKEN') { + message = 'Your session has expired. Please login again.' + } else { + message = 'Please login to view this page.' + } + } + return (

Login Page

- {expired ?

Your session has expired. Please login again.

: null} + {message !== null ?

{message}

: null}
@@ -52,11 +61,11 @@ const Login = () => { export const Route = createFileRoute('/login')({ validateSearch: z.object({ - expired: z.boolean().optional().catch(false), + reason: z.enum(['INVALID_REFRESH_TOKEN', 'EXPIRED_REFRESH_TOKEN', 'NO_REFRESH_TOKEN']).optional(), redirect: z.string().optional().catch('') }), beforeLoad({ context, search }) { - if (!(search.expired || context.auth.token === null)) { + if (!(search.reason !== undefined || context.auth.token === null)) { throw redirect({ to: '/recipes' }) } },