Clearer RefreshTokenError reasons and login page message.

This commit is contained in:
Jesse Brault 2024-08-08 12:28:26 -05:00
parent 57355fae1f
commit 90feb0e963
4 changed files with 32 additions and 13 deletions

View File

@ -4,7 +4,7 @@ import { useRouter } from '@tanstack/react-router'
import React, { useState } from 'react' import React, { useState } from 'react'
import { ApiError } from './api/ApiError' import { ApiError } from './api/ApiError'
import ExpiredTokenError from './api/ExpiredTokenError' import ExpiredTokenError from './api/ExpiredTokenError'
import refresh, { ExpiredRefreshTokenError } from './api/refresh' import refresh, { RefreshTokenError } from './api/refresh'
import LoginView from './api/types/LoginView' import LoginView from './api/types/LoginView'
import { useAuth } from './auth' import { useAuth } from './auth'
@ -21,14 +21,14 @@ const AuthAwareQueryClientProvider = ({ children }: React.PropsWithChildren) =>
try { try {
refreshResult = await refresh() refreshResult = await refresh()
} catch (error) { } catch (error) {
if (error instanceof ExpiredRefreshTokenError) { if (error instanceof RefreshTokenError) {
console.log('refresh-token expired') console.log(`RefreshTokenError: ${error.reason}`)
setCurrentlyRefreshing(false) setCurrentlyRefreshing(false)
clearToken() clearToken()
await router.navigate({ await router.navigate({
to: '/login', to: '/login',
search: { search: {
expired: true, reason: error.reason,
redirect: router.state.location.href redirect: router.state.location.href
} }
}) })

View File

@ -1,10 +1,13 @@
import { ApiError } from './ApiError' import { ApiError } from './ApiError'
import LoginExceptionView from './types/LoginExceptionView'
import LoginView, { RawLoginView } from './types/LoginView' import LoginView, { RawLoginView } from './types/LoginView'
export class ExpiredRefreshTokenError extends ApiError { export type RefreshTokenErrorReason = 'INVALID_REFRESH_TOKEN' | 'EXPIRED_REFRESH_TOKEN' | 'NO_REFRESH_TOKEN'
constructor() {
super(401, 'Expired refresh token.') export class RefreshTokenError extends ApiError {
Object.setPrototypeOf(this, ExpiredRefreshTokenError.prototype) constructor(public reason: RefreshTokenErrorReason) {
super(401, 'Refresh token error.')
Object.setPrototypeOf(this, RefreshTokenError.prototype)
} }
} }
@ -31,7 +34,8 @@ const refresh = async (): Promise<LoginView> => {
expires: new Date(rawExpires) expires: new Date(rawExpires)
} }
} else if (response.status === 401) { } else if (response.status === 401) {
throw new ExpiredRefreshTokenError() const { reason } = (await response.json()) as LoginExceptionView
throw new RefreshTokenError(reason as RefreshTokenErrorReason)
} else { } else {
throw new ApiError(response.status, response.statusText) throw new ApiError(response.status, response.statusText)
} }

View File

@ -0,0 +1,6 @@
interface LoginExceptionView {
reason: 'INVALID_CREDENTIALS' | 'INVALID_REFRESH_TOKEN' | 'EXPIRED_REFRESH_TOKEN' | 'NO_REFRESH_TOKEN'
message: string
}
export default LoginExceptionView

View File

@ -10,7 +10,7 @@ const Login = () => {
const router = useRouter() const router = useRouter()
const navigate = useNavigate() const navigate = useNavigate()
const { redirect, expired } = useSearch({ from: '/login' }) const { redirect, reason } = useSearch({ from: '/login' })
const onSubmit = async (event: FormEvent<HTMLFormElement>) => { const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault() 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 ( return (
<div> <div>
<h2>Login Page</h2> <h2>Login Page</h2>
{expired ? <p>Your session has expired. Please login again.</p> : null} {message !== null ? <p>{message}</p> : null}
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<label htmlFor="username">Username</label> <label htmlFor="username">Username</label>
<input id="username" name="username" type="text" /> <input id="username" name="username" type="text" />
@ -52,11 +61,11 @@ const Login = () => {
export const Route = createFileRoute('/login')({ export const Route = createFileRoute('/login')({
validateSearch: z.object({ 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('') redirect: z.string().optional().catch('')
}), }),
beforeLoad({ context, search }) { beforeLoad({ context, search }) {
if (!(search.expired || context.auth.token === null)) { if (!(search.reason !== undefined || context.auth.token === null)) {
throw redirect({ to: '/recipes' }) throw redirect({ to: '/recipes' })
} }
}, },