Skip to content

Commit b26ce33

Browse files
committed
feat: add unified error handling with AuthError class
1 parent 33873aa commit b26ce33

File tree

7 files changed

+537
-66
lines changed

7 files changed

+537
-66
lines changed

src/module.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,19 @@ export default defineNuxtModule<ModuleOptions>({
166166
from: resolve(
167167
`./runtime/composables/${options.provider.type}/useAuthState`
168168
)
169+
},
170+
// Error utilities for handling authentication errors
171+
{
172+
name: 'AuthError',
173+
from: resolve('./runtime/utils/authError')
174+
},
175+
{
176+
name: 'createAuthError',
177+
from: resolve('./runtime/utils/authError')
178+
},
179+
{
180+
name: 'toAuthError',
181+
from: resolve('./runtime/utils/authError')
169182
}
170183
])
171184

@@ -189,7 +202,8 @@ export default defineNuxtModule<ModuleOptions>({
189202
[
190203
'// AUTO-GENERATED BY @sidebase/nuxt-auth',
191204
'declare module \'#auth\' {',
192-
` const { getServerSession, getToken, NuxtAuthHandler }: typeof import('${resolve('./runtime/server/services')}')`,
205+
` const { getServerSession, getToken, NuxtAuthHandler, AuthError, createAuthError, toAuthError }: typeof import('${resolve('./runtime/server/services')}')`,
206+
` export type { AuthErrorCode, AuthErrorData } from '${resolve('./runtime/utils/authError')}'`,
193207
...(options.provider.type === 'local'
194208
? [genInterface(
195209
'SessionData',
@@ -270,6 +284,8 @@ export default defineNuxtModule<ModuleOptions>({
270284

271285
// Used by nuxt/module-builder for `types.d.ts` generation
272286
export type { ModuleOptions, RefreshHandler }
287+
export { AuthError, createAuthError, toAuthError } from './runtime/utils/authError'
288+
export type { AuthErrorCode, AuthErrorData } from './runtime/utils/authError'
273289
export interface ModulePublicRuntimeConfig {
274290
auth: ModuleOptionsNormalized
275291
}

src/runtime/composables/authjs/useAuth.ts

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { CommonUseAuthReturn, GetSessionOptions, SecondarySignInOptions, Si
1010
import { useTypedBackendConfig } from '../../helpers'
1111
import { getRequestURLWN } from '../common/getRequestURL'
1212
import { determineCallbackUrl } from '../../utils/callbackUrl'
13+
import { createAuthError, toAuthError } from '../../utils/authError'
1314
import type { SessionData } from './useAuthState'
1415
import { navigateToAuthPageWN } from './utils/navigateToAuthPage'
1516
import type { NuxtApp } from '#app/nuxt'
@@ -78,7 +79,10 @@ export function useAuth(): UseAuthReturn {
7879
data,
7980
loading,
8081
status,
81-
lastRefreshedAt
82+
lastRefreshedAt,
83+
error,
84+
setError,
85+
clearError
8286
} = useAuthState()
8387

8488
/**
@@ -93,12 +97,18 @@ export function useAuth(): UseAuthReturn {
9397
options?: SecondarySignInOptions,
9498
authorizationParams?: Record<string, string>
9599
): Promise<SignInResult> {
100+
// Clear previous error
101+
clearError()
102+
96103
// 1. Lead to error page if no providers are available
97104
const configuredProviders = await getProviders()
98105
if (!configuredProviders) {
99106
const errorUrl = resolveApiUrlPath('error', runtimeConfig)
100107
const navigationResult = await navigateToAuthPageWN(nuxt, errorUrl, true)
101108

109+
const authError = createAuthError.invalidProvider()
110+
setError(authError)
111+
102112
return {
103113
// Future AuthJS compat here and in other places
104114
// https://authjs.dev/reference/core/errors#invalidprovider
@@ -130,6 +140,9 @@ export function useAuth(): UseAuthReturn {
130140
if (!selectedProvider) {
131141
const navigationResult = await navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage, true)
132142

143+
const authError = createAuthError.invalidProvider(provider)
144+
setError(authError)
145+
133146
return {
134147
// https://authjs.dev/reference/core/errors#invalidprovider
135148
error: 'InvalidProvider',
@@ -173,19 +186,28 @@ export function useAuth(): UseAuthReturn {
173186
},
174187
/* proxyCookies = */ true
175188
)
176-
.catch<Record<string, any>>((error: { data: any }) => error.data)
189+
.catch<Record<string, any>>((err: { data: any }) => {
190+
// Set error state for network/server errors
191+
const authError = toAuthError(err)
192+
setError(authError)
193+
return err.data
194+
})
177195

178-
const data = await callWithNuxt(nuxt, fetchSignIn)
196+
const responseData = await callWithNuxt(nuxt, fetchSignIn)
179197

180198
if (redirect || !isSupportingReturn) {
181-
const href = data.url ?? callbackUrl
199+
const href = responseData.url ?? callbackUrl
182200
const navigationResult = await navigateToAuthPageWN(nuxt, href)
183201

184202
// We use `http://_` as a base to allow relative URLs in `callbackUrl`. We only need the `error` query param
185-
const error = new URL(href, 'http://_').searchParams.get('error')
203+
const urlError = new URL(href, 'http://_').searchParams.get('error')
204+
205+
if (urlError) {
206+
setError(createAuthError.invalidCredentials(urlError))
207+
}
186208

187209
return {
188-
error,
210+
error: urlError,
189211
ok: true,
190212
status: 302,
191213
url: href,
@@ -194,14 +216,19 @@ export function useAuth(): UseAuthReturn {
194216
}
195217

196218
// At this point the request succeeded (i.e., it went through)
197-
const error = new URL(data.url).searchParams.get('error')
219+
const urlError = new URL(responseData.url).searchParams.get('error')
220+
221+
if (urlError) {
222+
setError(createAuthError.invalidCredentials(urlError))
223+
}
224+
198225
await getSessionWithNuxt(nuxt)
199226

200227
return {
201-
error,
228+
error: urlError,
202229
status: 200,
203230
ok: true,
204-
url: error ? null : data.url,
231+
url: urlError ? null : responseData.url,
205232
navigationResult: undefined,
206233
}
207234
}
@@ -213,11 +240,18 @@ export function useAuth(): UseAuthReturn {
213240
// Pass the `Host` header when making internal requests
214241
const headers = await getRequestHeaders(nuxt, false)
215242

216-
return _fetch<GetProvidersResult>(
217-
nuxt,
218-
'/providers',
219-
{ headers }
220-
)
243+
try {
244+
return await _fetch<GetProvidersResult>(
245+
nuxt,
246+
'/providers',
247+
{ headers }
248+
)
249+
}
250+
catch (err) {
251+
const authError = toAuthError(err)
252+
setError(authError)
253+
return null
254+
}
221255
}
222256

223257
/**
@@ -262,6 +296,11 @@ export function useAuth(): UseAuthReturn {
262296
data.value = isNonEmptyObject(sessionData) ? sessionData : null
263297
loading.value = false
264298

299+
// Clear error on successful session fetch
300+
if (data.value) {
301+
clearError()
302+
}
303+
265304
if (required && status.value === 'unauthenticated') {
266305
return onUnauthenticated()
267306
}
@@ -276,8 +315,21 @@ export function useAuth(): UseAuthReturn {
276315
callbackUrl: callbackUrl || callbackUrlFallback
277316
}
278317
},
279-
onRequestError: onError,
280-
onResponseError: onError,
318+
onRequestError: ({ error: fetchError }) => {
319+
onError()
320+
const authError = toAuthError(fetchError)
321+
setError(authError)
322+
},
323+
onResponseError: ({ error: fetchError, response }) => {
324+
onError()
325+
if (response?.status === 401) {
326+
setError(createAuthError.sessionExpired())
327+
}
328+
else {
329+
const authError = toAuthError(fetchError)
330+
setError(authError)
331+
}
332+
},
281333
headers
282334
}, /* proxyCookies = */ true)
283335
}
@@ -291,6 +343,9 @@ export function useAuth(): UseAuthReturn {
291343
* @param options - Options for sign out, e.g., to `redirect` the user to a specific page after sign out has completed
292344
*/
293345
async function signOut(options?: SignOutOptions) {
346+
// Clear previous error
347+
clearError()
348+
294349
const { callbackUrl: userCallbackUrl, redirect = true } = options ?? {}
295350
const csrfToken = await getCsrfTokenWithNuxt(nuxt)
296351

@@ -302,23 +357,33 @@ export function useAuth(): UseAuthReturn {
302357
)
303358

304359
if (!csrfToken) {
360+
const authError = createAuthError.unknown('Could not fetch CSRF Token for signing out')
361+
setError(authError)
305362
throw createError({ statusCode: 400, message: 'Could not fetch CSRF Token for signing out' })
306363
}
307364

308-
const signoutData = await _fetch<{ url: string }>(nuxt, '/signout', {
309-
method: 'POST',
310-
headers: {
311-
'Content-Type': 'application/x-www-form-urlencoded',
312-
...(await getRequestHeaders(nuxt))
313-
},
314-
onRequest: ({ options }) => {
315-
options.body = new URLSearchParams({
316-
csrfToken: csrfToken as string,
317-
callbackUrl,
318-
json: 'true'
319-
})
320-
}
321-
}).catch(error => error.data)
365+
let signoutData: { url: string }
366+
try {
367+
signoutData = await _fetch<{ url: string }>(nuxt, '/signout', {
368+
method: 'POST',
369+
headers: {
370+
'Content-Type': 'application/x-www-form-urlencoded',
371+
...(await getRequestHeaders(nuxt))
372+
},
373+
onRequest: ({ options }) => {
374+
options.body = new URLSearchParams({
375+
csrfToken: csrfToken as string,
376+
callbackUrl,
377+
json: 'true'
378+
})
379+
}
380+
})
381+
}
382+
catch (err) {
383+
const authError = toAuthError(err)
384+
setError(authError)
385+
signoutData = (err as any).data ?? { url: callbackUrl }
386+
}
322387

323388
if (redirect) {
324389
const url = signoutData.url ?? callbackUrl
@@ -350,7 +415,15 @@ export function useAuth(): UseAuthReturn {
350415
*/
351416
async function getCsrfToken() {
352417
const headers = await getRequestHeaders(nuxt)
353-
return _fetch<{ csrfToken: string }>(nuxt, '/csrf', { headers }).then(response => response.csrfToken)
418+
try {
419+
const response = await _fetch<{ csrfToken: string }>(nuxt, '/csrf', { headers })
420+
return response.csrfToken
421+
}
422+
catch (err) {
423+
const authError = toAuthError(err)
424+
setError(authError)
425+
throw err
426+
}
354427
}
355428
function getCsrfTokenWithNuxt(nuxt: NuxtApp) {
356429
return callWithNuxt(nuxt, getCsrfToken)
@@ -360,6 +433,8 @@ export function useAuth(): UseAuthReturn {
360433
status,
361434
data: readonly(data) as Readonly<Ref<SessionData | null | undefined>>,
362435
lastRefreshedAt: readonly(lastRefreshedAt),
436+
error: readonly(error),
437+
clearError,
363438
getSession,
364439
getCsrfToken,
365440
getProviders,

src/runtime/composables/commonAuthState.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { computed } from 'vue'
22
import type { SessionLastRefreshedAt, SessionStatus } from '../types'
3+
import type { AuthError } from '../utils/authError'
34
import { useState } from '#imports'
45

56
export function makeCommonAuthState<SessionData>() {
@@ -18,6 +19,10 @@ export function makeCommonAuthState<SessionData>() {
1819

1920
// If session exists, initialize as not loading
2021
const loading = useState<boolean>('auth:loading', () => false)
22+
23+
// Error state for tracking authentication errors
24+
const error = useState<AuthError | null>('auth:error', () => null)
25+
2126
const status = computed<SessionStatus>(() => {
2227
if (loading.value) {
2328
return 'loading'
@@ -28,10 +33,27 @@ export function makeCommonAuthState<SessionData>() {
2833
return 'unauthenticated'
2934
})
3035

36+
/**
37+
* Set error state
38+
*/
39+
function setError(authError: AuthError | null) {
40+
error.value = authError
41+
}
42+
43+
/**
44+
* Clear error state
45+
*/
46+
function clearError() {
47+
error.value = null
48+
}
49+
3150
return {
3251
data,
3352
loading,
3453
lastRefreshedAt,
3554
status,
55+
error,
56+
setError,
57+
clearError
3658
}
3759
}

0 commit comments

Comments
 (0)