@@ -43,6 +43,7 @@ export default function App() {
4343 const [ showActivityDialog , setShowActivityDialog ] = useState ( false ) ;
4444 const [ showStepDialog , setShowStepDialog ] = useState ( false ) ;
4545 const [ activeTab , setActiveTab ] = useState < TabType > ( 'edit' ) ;
46+ const [ authChecked , setAuthChecked ] = useState ( false ) ;
4647
4748 // Persistence hook for selections
4849 const { selections, isLoading : isLoadingSelections , saveSelection } = usePersistedSelections ( ) ;
@@ -57,16 +58,110 @@ export default function App() {
5758 const { script } = state ;
5859 const { isValid, errors : validationErrors } = state . validation ;
5960
61+ // Immediate authentication check and redirect
62+ useEffect ( ( ) => {
63+ const checkAuthAndRedirect = async ( ) => {
64+ // Skip auth check if we're handling OAuth callback
65+ if ( window . location . pathname === '/account/callback' ) {
66+ setAuthChecked ( true ) ;
67+ return ;
68+ }
69+
70+ // Check if we just came back from logout
71+ const urlParams = new URLSearchParams ( window . location . search ) ;
72+ const loggedOut = urlParams . get ( 'logged_out' ) ;
73+ const forceLogin = urlParams . get ( 'force_login' ) ;
74+ const completeLogout = urlParams . get ( 'complete_logout' ) ;
75+ const logoutComplete = urlParams . get ( 'logout_complete' ) ;
76+ const promptLogin = urlParams . get ( 'prompt' ) ;
77+
78+ if ( loggedOut || forceLogin || completeLogout || logoutComplete ) {
79+ // Clear all logout-related parameters from URL
80+ window . history . replaceState ( { } , document . title , '/' ) ;
81+ console . log ( '🔄 Returned from logout, forcing fresh login...' ) ;
82+
83+ // Clear any remaining authentication state to ensure fresh login
84+ try {
85+ const { authService } = await import ( './lib/auth-service' ) ;
86+ authService . clearToken ( ) ; // Make sure all tokens are cleared
87+
88+ // Also try to clear localStorage/sessionStorage again
89+ localStorage . clear ( ) ;
90+ sessionStorage . clear ( ) ;
91+ } catch ( error ) {
92+ console . warn ( 'Failed to clear tokens:' , error ) ;
93+ }
94+
95+ // If this is the logout completion page, show a brief message then redirect to OAuth
96+ if ( logoutComplete ) {
97+ console . log ( '✅ Logout completed successfully, redirecting to login...' ) ;
98+ // Add a small delay to show the logout was successful
99+ setTimeout ( async ( ) => {
100+ try {
101+ const { authService } = await import ( './lib/auth-service' ) ;
102+ await authService . startOAuthLogin ( 'login' ) ; // Force login screen
103+ } catch ( error ) {
104+ console . error ( 'Failed to start OAuth login:' , error ) ;
105+ window . location . reload ( ) ; // Fallback
106+ }
107+ } , 1000 ) ;
108+ setAuthChecked ( true ) ;
109+ return ;
110+ }
111+ }
112+
113+ try {
114+ const { authService } = await import ( './lib/auth-service' ) ;
115+
116+ if ( ! authService . isAuthenticated ( ) || loggedOut || forceLogin || completeLogout ) {
117+ console . log ( '🔐 User not authenticated or logout forced, redirecting to TrackMan login...' ) ;
118+
119+ // If we have a prompt parameter, pass it to the OAuth login to force login screen
120+ if ( promptLogin === 'login' ) {
121+ console . log ( '🔑 Adding prompt=login to force login screen...' ) ;
122+ }
123+
124+ await authService . startOAuthLogin ( promptLogin === 'login' ? 'login' : undefined ) ;
125+ // This will redirect away, so we won't reach the next line
126+ return ;
127+ }
128+
129+ console . log ( '✅ User is authenticated' ) ;
130+ setAuthChecked ( true ) ;
131+ } catch ( error ) {
132+ console . error ( '❌ Auth check failed:' , error ) ;
133+ setAuthChecked ( true ) ; // Show app anyway if auth check fails
134+ }
135+ } ;
136+
137+ checkAuthAndRedirect ( ) ;
138+ } , [ ] ) ;
139+
60140 // Handle OAuth callback - monitor URL changes
61141 useEffect ( ( ) => {
62142 const handleOAuthCallback = async ( ) => {
63143 const currentUrl = window . location . href ;
64144 const urlPath = window . location . pathname ;
65145
66- // Check if we're on the callback route with an authorization code
67- console . log ( '🔍 Checking OAuth callback:' , { urlPath, hasCode : currentUrl . includes ( 'code=' ) } ) ;
146+ // Extract code from URL to create unique processing key
147+ const urlParams = new URLSearchParams ( window . location . search ) ;
148+ const code = urlParams . get ( 'code' ) ;
149+ const callbackKey = `oauth_callback_processed_${ code } ` ;
150+
151+ // Check if we've already processed this specific callback
152+ const alreadyProcessed = sessionStorage . getItem ( callbackKey ) ;
68153
69- if ( urlPath === '/account/callback' && currentUrl . includes ( 'code=' ) ) {
154+ console . log ( '🔍 Checking OAuth callback:' , {
155+ urlPath,
156+ hasCode : currentUrl . includes ( 'code=' ) ,
157+ processed : ! ! alreadyProcessed ,
158+ codePreview : code ?. substring ( 0 , 8 ) + '...'
159+ } ) ;
160+
161+ if ( urlPath === '/account/callback' && code && ! alreadyProcessed ) {
162+ // Mark this specific callback as being processed
163+ sessionStorage . setItem ( callbackKey , 'true' ) ;
164+
70165 console . log ( '🔄 OAuth callback detected:' , currentUrl ) ;
71166 try {
72167 const { authService } = await import ( './lib/auth-service' ) ;
@@ -79,7 +174,24 @@ export default function App() {
79174 window . location . reload ( ) ;
80175 } catch ( error ) {
81176 console . error ( '❌ OAuth callback failed:' , error ) ;
82- alert ( 'Login failed: ' + ( error as Error ) . message ) ;
177+ console . error ( '❌ Full error details:' , {
178+ name : ( error as Error ) . name ,
179+ message : ( error as Error ) . message ,
180+ stack : ( error as Error ) . stack
181+ } ) ;
182+
183+ // Clear the processing flag on error so user can retry
184+ sessionStorage . removeItem ( callbackKey ) ;
185+
186+ // Show a more user-friendly error message
187+ const errorMsg = ( error as Error ) . message ;
188+ if ( errorMsg . includes ( 'invalid_grant' ) ) {
189+ alert ( 'Login failed: Authorization code has already been used or expired.\n\nThis can happen if the login process runs multiple times. Please try logging in again.' ) ;
190+ } else if ( errorMsg . includes ( 'Failed to fetch' ) ) {
191+ alert ( 'Login failed: Unable to connect to authentication server. This may be a network or CORS issue.\n\nPlease check:\n1. Your internet connection\n2. If you are on a corporate network, contact your IT administrator\n3. Try refreshing the page' ) ;
192+ } else {
193+ alert ( 'Login failed: ' + errorMsg ) ;
194+ }
83195 window . history . replaceState ( { } , document . title , '/' ) ;
84196 }
85197 }
@@ -443,6 +555,71 @@ export default function App() {
443555 updateStep ( step . id , { logic : logicPatch } as any ) ;
444556 } , [ selectedNode ] ) ;
445557
558+ // Don't render anything until auth check is complete
559+ if ( ! authChecked ) {
560+ // Check if we're on the logout completion page
561+ const urlParams = new URLSearchParams ( window . location . search ) ;
562+ const logoutComplete = urlParams . get ( 'logout_complete' ) ;
563+
564+ if ( logoutComplete ) {
565+ return (
566+ < div style = { {
567+ height : '100vh' ,
568+ display : 'flex' ,
569+ flexDirection : 'column' ,
570+ alignItems : 'center' ,
571+ justifyContent : 'center' ,
572+ backgroundColor : '#f5f5f5' ,
573+ fontFamily : 'system-ui, -apple-system, sans-serif'
574+ } } >
575+ < div style = { {
576+ padding : '2rem' ,
577+ backgroundColor : 'white' ,
578+ borderRadius : '8px' ,
579+ boxShadow : '0 2px 8px rgba(0,0,0,0.1)' ,
580+ textAlign : 'center' ,
581+ maxWidth : '400px'
582+ } } >
583+ < div style = { { fontSize : '48px' , marginBottom : '1rem' } } > ✅</ div >
584+ < h2 style = { { margin : '0 0 1rem 0' , color : '#333' } } > Logout Successful</ h2 >
585+ < p style = { { margin : '0' , color : '#666' } } >
586+ You have been logged out successfully.< br />
587+ Redirecting to login page...
588+ </ p >
589+ < div style = { {
590+ marginTop : '1rem' ,
591+ padding : '0.5rem' ,
592+ backgroundColor : '#f8f9fa' ,
593+ borderRadius : '4px' ,
594+ fontSize : '0.9em' ,
595+ color : '#666'
596+ } } >
597+ < div className = "spinner" style = { {
598+ display : 'inline-block' ,
599+ width : '16px' ,
600+ height : '16px' ,
601+ border : '2px solid #ddd' ,
602+ borderTop : '2px solid #007acc' ,
603+ borderRadius : '50%' ,
604+ animation : 'spin 1s linear infinite' ,
605+ marginRight : '8px'
606+ } } > </ div >
607+ Please wait...
608+ </ div >
609+ </ div >
610+ < style > { `
611+ @keyframes spin {
612+ 0% { transform: rotate(0deg); }
613+ 100% { transform: rotate(360deg); }
614+ }
615+ ` } </ style >
616+ </ div >
617+ ) ;
618+ }
619+
620+ return null ; // This prevents any UI from showing during redirect
621+ }
622+
446623 return (
447624 < div className = "app-container" >
448625 < TopBar
0 commit comments