@@ -15,15 +15,27 @@ import type { ValidationError } from './api';
1515let isHandling401 = false ;
1616const AUTH_ENDPOINTS = [ '/api/v1/auth/login' , '/api/v1/auth/register' , '/api/v1/auth/verify-token' ] ;
1717
18+ type ToastType = 'error' | 'warning' | 'info' | 'success' ;
19+
20+ const STATUS_MESSAGES : Record < number , { message : string ; type : ToastType } > = {
21+ 403 : { message : 'Access denied.' , type : 'error' } ,
22+ 429 : { message : 'Too many requests. Please slow down.' , type : 'warning' } ,
23+ } ;
24+
25+ function extractDetail ( err : unknown ) : string | ValidationError [ ] | null {
26+ if ( typeof err === 'object' && err !== null && 'detail' in err ) {
27+ return ( err as { detail : string | ValidationError [ ] } ) . detail ;
28+ }
29+ return null ;
30+ }
31+
1832export function getErrorMessage ( err : unknown , fallback = 'An error occurred' ) : string {
1933 if ( ! err ) return fallback ;
2034
21- if ( typeof err === 'object' && 'detail' in err ) {
22- const detail = ( err as { detail ?: ValidationError [ ] | string } ) . detail ;
23- if ( typeof detail === 'string' ) return detail ;
24- if ( Array . isArray ( detail ) && detail . length > 0 ) {
25- return detail . map ( ( e ) => `${ e . loc [ e . loc . length - 1 ] } : ${ e . msg } ` ) . join ( ', ' ) ;
26- }
35+ const detail = extractDetail ( err ) ;
36+ if ( typeof detail === 'string' ) return detail ;
37+ if ( Array . isArray ( detail ) && detail . length > 0 ) {
38+ return detail . map ( ( e ) => `${ e . loc [ e . loc . length - 1 ] } : ${ e . msg } ` ) . join ( ', ' ) ;
2739 }
2840
2941 if ( err instanceof Error ) return err . message ;
@@ -49,12 +61,56 @@ function clearAuthState(): void {
4961 sessionStorage . removeItem ( 'authState' ) ;
5062}
5163
52- function handleAuthFailure ( currentPath : string ) : void {
53- clearAuthState ( ) ;
54- if ( currentPath !== '/login' && currentPath !== '/register' ) {
55- sessionStorage . setItem ( 'redirectAfterLogin' , currentPath ) ;
64+ function handle401 ( isAuthEndpoint : boolean ) : void {
65+ if ( isAuthEndpoint ) return ;
66+
67+ const wasAuthenticated = get ( isAuthenticated ) ;
68+ if ( wasAuthenticated && ! isHandling401 ) {
69+ isHandling401 = true ;
70+ const currentPath = window . location . pathname + window . location . search ;
71+ addToast ( 'Session expired. Please log in again.' , 'warning' ) ;
72+ clearAuthState ( ) ;
73+ if ( currentPath !== '/login' && currentPath !== '/register' ) {
74+ sessionStorage . setItem ( 'redirectAfterLogin' , currentPath ) ;
75+ }
76+ goto ( '/login' ) ;
77+ setTimeout ( ( ) => { isHandling401 = false ; } , 1000 ) ;
78+ } else {
79+ clearAuthState ( ) ;
80+ }
81+ }
82+
83+ function handleErrorStatus ( status : number | undefined , error : unknown , isAuthEndpoint : boolean ) : boolean {
84+ if ( ! status ) {
85+ if ( ! isAuthEndpoint ) addToast ( 'Network error. Check your connection.' , 'error' ) ;
86+ return true ;
87+ }
88+
89+ if ( status === 401 ) {
90+ handle401 ( isAuthEndpoint ) ;
91+ return true ;
92+ }
93+
94+ const mapped = STATUS_MESSAGES [ status ] ;
95+ if ( mapped ) {
96+ addToast ( mapped . message , mapped . type ) ;
97+ return true ;
98+ }
99+
100+ if ( status === 422 ) {
101+ const detail = extractDetail ( error ) ;
102+ if ( Array . isArray ( detail ) && detail . length > 0 ) {
103+ addToast ( `Validation error:\n${ formatValidationErrors ( detail ) } ` , 'error' ) ;
104+ return true ;
105+ }
106+ }
107+
108+ if ( status >= 500 ) {
109+ addToast ( 'Server error. Please try again later.' , 'error' ) ;
110+ return true ;
56111 }
57- goto ( '/login' ) ;
112+
113+ return false ;
58114}
59115
60116export function initializeApiInterceptors ( ) : void {
@@ -70,59 +126,11 @@ export function initializeApiInterceptors(): void {
70126
71127 console . error ( '[API Error]' , { status, url, error } ) ;
72128
73- // 401: Silent by default. Only show toast + redirect if user HAD an active session.
74- if ( status === 401 ) {
75- if ( isAuthEndpoint ) {
76- return error ; // Auth endpoints handle their own messaging
77- }
78- const wasAuthenticated = get ( isAuthenticated ) ;
79- if ( wasAuthenticated && ! isHandling401 ) {
80- isHandling401 = true ;
81- try {
82- const currentPath = window . location . pathname + window . location . search ;
83- addToast ( 'Session expired. Please log in again.' , 'warning' ) ;
84- handleAuthFailure ( currentPath ) ;
85- } finally {
86- setTimeout ( ( ) => { isHandling401 = false ; } , 1000 ) ;
87- }
88- } else {
89- clearAuthState ( ) ;
90- }
91- return error ;
92- }
93-
94- if ( status === 403 ) {
95- addToast ( 'Access denied.' , 'error' ) ;
96- return error ;
97- }
98-
99- if ( status === 422 && typeof error === 'object' && error !== null && 'detail' in error ) {
100- const detail = ( error as { detail : ValidationError [ ] } ) . detail ;
101- if ( Array . isArray ( detail ) && detail . length > 0 ) {
102- addToast ( `Validation error:\n${ formatValidationErrors ( detail ) } ` , 'error' ) ;
103- return error ;
104- }
105- }
106-
107- if ( status === 429 ) {
108- addToast ( 'Too many requests. Please slow down.' , 'warning' ) ;
109- return error ;
110- }
111-
112- if ( status && status >= 500 ) {
113- addToast ( 'Server error. Please try again later.' , 'error' ) ;
114- return error ;
115- }
116-
117- if ( ! response && ! isAuthEndpoint ) {
118- addToast ( 'Network error. Check your connection.' , 'error' ) ;
119- return error ;
120- }
121-
122- // Don't toast for auth-related silent failures
123- if ( ! isAuthEndpoint ) {
129+ const handled = handleErrorStatus ( status , error , isAuthEndpoint ) ;
130+ if ( ! handled && ! isAuthEndpoint ) {
124131 addToast ( getErrorMessage ( error , 'An error occurred' ) , 'error' ) ;
125132 }
133+
126134 return error ;
127135 } ) ;
128136
0 commit comments