Skip to content

Commit a936a40

Browse files
committed
Implements user authentication via OAuth
Removes the previous client credentials approach in favor of a user-based login flow. Adds an immediate auth check, handles forced logout or re-login, and introduces a dedicated login page for error handling. Streamlines the UI with improved logout feedback and secure token management.
1 parent e92cd8b commit a936a40

File tree

6 files changed

+401
-172
lines changed

6 files changed

+401
-172
lines changed

editor/src/App.tsx

Lines changed: 181 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useEffect } from 'react';
2+
import { useAuth } from '../lib/AuthProvider';
3+
4+
export const LoginPage: React.FC = () => {
5+
const { loginWithOAuth, error } = useAuth();
6+
7+
// Automatically redirect to OAuth on component mount
8+
useEffect(() => {
9+
const initiateLogin = async () => {
10+
try {
11+
await loginWithOAuth();
12+
} catch (err) {
13+
console.error('Auto-login failed:', err);
14+
}
15+
};
16+
17+
// Add a small delay to avoid potential race conditions
18+
const timer = setTimeout(initiateLogin, 100);
19+
return () => clearTimeout(timer);
20+
}, [loginWithOAuth]);
21+
22+
// Only show this if there's an error
23+
if (error) {
24+
return (
25+
<div className="login-page">
26+
<div className="login-container">
27+
<div className="login-header">
28+
<div className="login-logo">
29+
<svg width="48" height="48" viewBox="0 0 215 215">
30+
<g>
31+
<path d="M198.385 0C207.593 0 215 7.40689 215 16.6155V198.385C215 207.593 207.593 215 198.385 215H85.5796C83.9781 215 82.6769 213.599 82.8771 211.997C92.9865 125.517 139.029 33.9316 169.558 0.300279C169.758 0.100093 170.058 0 170.358 0H198.385Z" fill="#EC691A"></path>
32+
<path d="M16.6155 0H153.843C154.143 0 154.344 0.400372 154.043 0.600559C97.2905 48.2449 52.5489 145.535 33.9316 212.298C33.5312 213.899 32.0298 215 30.4283 215H16.6155C7.40689 215 0 207.593 0 198.385V16.6155C0 7.40689 7.40689 0 16.6155 0Z" fill="#EC691A"></path>
33+
</g>
34+
</svg>
35+
</div>
36+
<h1 className="login-title">APP SCRIPT EDITOR</h1>
37+
<p className="login-subtitle">Authentication Error</p>
38+
</div>
39+
40+
<div className="login-content">
41+
<div className="login-error">
42+
<p>⚠️ {error}</p>
43+
</div>
44+
45+
<button
46+
onClick={() => window.location.reload()}
47+
className="login-button"
48+
>
49+
🔄 Try Again
50+
</button>
51+
52+
<div className="login-info">
53+
<p>Click to retry authentication</p>
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
);
59+
}
60+
61+
// Show loading while redirecting to OAuth
62+
return (
63+
<div className="login-page">
64+
<div className="login-container">
65+
<div className="login-header">
66+
<div className="login-logo">
67+
<svg width="48" height="48" viewBox="0 0 215 215">
68+
<g>
69+
<path d="M198.385 0C207.593 0 215 7.40689 215 16.6155V198.385C215 207.593 207.593 215 198.385 215H85.5796C83.9781 215 82.6769 213.599 82.8771 211.997C92.9865 125.517 139.029 33.9316 169.558 0.300279C169.758 0.100093 170.058 0 170.358 0H198.385Z" fill="#EC691A"></path>
70+
<path d="M16.6155 0H153.843C154.143 0 154.344 0.400372 154.043 0.600559C97.2905 48.2449 52.5489 145.535 33.9316 212.298C33.5312 213.899 32.0298 215 30.4283 215H16.6155C7.40689 215 0 207.593 0 198.385V16.6155C0 7.40689 7.40689 0 16.6155 0Z" fill="#EC691A"></path>
71+
</g>
72+
</svg>
73+
</div>
74+
<h1 className="login-title">APP SCRIPT EDITOR</h1>
75+
<p className="login-subtitle">🔄 Redirecting to TrackMan login...</p>
76+
</div>
77+
</div>
78+
</div>
79+
);
80+
};

editor/src/components/TopBar.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const TopBar: React.FC<TopBarProps> = ({
2222
selectedFacilityId,
2323
onFacilitySelect
2424
}) => {
25-
const { isAuthenticated, isLoading, loginWithOAuth, logout } = useAuth();
25+
const { isAuthenticated, isLoading, logout } = useAuth();
2626

2727
const handleFacilitySelect = (facility: Facility | null) => {
2828
onFacilitySelect(facility);
@@ -59,11 +59,7 @@ export const TopBar: React.FC<TopBarProps> = ({
5959
<button onClick={logout} className="auth-button logout">
6060
🔓 Logout
6161
</button>
62-
) : (
63-
<button onClick={loginWithOAuth} className="auth-button login">
64-
🔑 Login
65-
</button>
66-
)}
62+
) : null}
6763
</div>
6864
<BuildVersion />
6965
</div>

editor/src/lib/AuthProvider.tsx

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
4646
};
4747

4848
const login = async () => {
49-
// This method is for client credential authentication (API-only access)
50-
// For user authentication, use loginWithOAuth() instead
51-
setIsLoading(true);
52-
setError(null);
53-
54-
try {
55-
await authService.getAccessToken();
56-
setIsAuthenticated(true);
57-
console.log('✅ Successfully authenticated with client credentials');
58-
} catch (err) {
59-
const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
60-
setError(errorMessage);
61-
setIsAuthenticated(false);
62-
console.error('❌ Authentication failed:', errorMessage);
63-
} finally {
64-
setIsLoading(false);
65-
}
49+
// Client credential authentication has been removed - only OAuth login is supported
50+
console.log('❌ Client credential login is no longer supported. Use OAuth login instead.');
51+
throw new Error('Client credential authentication is disabled. Please use OAuth login.');
6652
};
6753

6854
const loginWithOAuth = async () => {
@@ -85,19 +71,15 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
8571
setIsLoading(true);
8672
try {
8773
await authService.logoutOAuth();
88-
// Update local state immediately since we're not redirecting
89-
setIsAuthenticated(false);
90-
setError(null);
91-
console.log('🔓 Logged out successfully');
74+
console.log('🔓 Logged out successfully, auth service will handle redirect...');
75+
// Don't reload here - let the auth service handle the redirect to logout completion page
9276
} catch (err) {
9377
// Fallback to local logout if server logout fails
9478
console.warn('⚠️ Server logout failed, falling back to local logout:', err);
9579
authService.clearToken();
96-
setIsAuthenticated(false);
97-
setError(null);
98-
console.log('🔓 Logged out locally');
99-
} finally {
100-
setIsLoading(false);
80+
console.log('🔓 Logged out locally, redirecting to login...');
81+
// Only reload if the auth service logout failed
82+
window.location.reload();
10183
}
10284
};
10385

0 commit comments

Comments
 (0)