109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { apiCall } from '../utils/api';
|
|
|
|
const OAuthCallback: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [processing, setProcessing] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const handleCallback = async () => {
|
|
// Check for errors from OAuth provider
|
|
const errorParam = searchParams.get('error');
|
|
if (errorParam) {
|
|
setError(`Authentication failed: ${errorParam}`);
|
|
setProcessing(false);
|
|
return;
|
|
}
|
|
|
|
// Get the authorization code
|
|
const code = searchParams.get('code');
|
|
if (!code) {
|
|
setError('No authorization code received');
|
|
setProcessing(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Exchange the code for a token
|
|
const response = await apiCall('/auth/google/exchange', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ code }),
|
|
});
|
|
|
|
if (response.error === 'pending_approval') {
|
|
// User needs approval
|
|
localStorage.setItem('authToken', response.data.token);
|
|
localStorage.setItem('user', JSON.stringify(response.data.user));
|
|
navigate('/pending-approval');
|
|
return;
|
|
}
|
|
|
|
if (response.data && response.data.token) {
|
|
// Success! Store the token and user data
|
|
localStorage.setItem('authToken', response.data.token);
|
|
localStorage.setItem('user', JSON.stringify(response.data.user));
|
|
|
|
// Redirect to dashboard
|
|
window.location.href = '/';
|
|
} else {
|
|
setError('Failed to authenticate');
|
|
}
|
|
} catch (err: any) {
|
|
console.error('OAuth callback error:', err);
|
|
if (err.message?.includes('pending_approval')) {
|
|
// This means the user was created but needs approval
|
|
navigate('/');
|
|
} else {
|
|
setError(err.message || 'Authentication failed');
|
|
}
|
|
} finally {
|
|
setProcessing(false);
|
|
}
|
|
};
|
|
|
|
handleCallback();
|
|
}, [navigate, searchParams]);
|
|
|
|
if (processing) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center">
|
|
<div className="bg-white rounded-2xl shadow-xl p-8 text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent mx-auto mb-4"></div>
|
|
<p className="text-lg font-medium text-slate-700">Completing sign in...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center">
|
|
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md text-center">
|
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<svg className="w-8 h-8 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-xl font-bold text-slate-800 mb-2">Authentication Failed</h2>
|
|
<p className="text-slate-600 mb-4">{error}</p>
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
className="btn btn-primary"
|
|
>
|
|
Back to Login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
export default OAuthCallback; |