Files
smoothschedule/legacy_reference/frontend/src/pages/OAuthCallback.tsx
poduck 2e111364a2 Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:43:20 -05:00

259 lines
9.4 KiB
TypeScript

/**
* OAuth Callback Page
* Handles OAuth provider redirects and completes authentication
*/
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { Loader2, AlertCircle, CheckCircle } from 'lucide-react';
import { handleOAuthCallback } from '../api/oauth';
import { setCookie } from '../utils/cookies';
import SmoothScheduleLogo from '../components/SmoothScheduleLogo';
const OAuthCallback: React.FC = () => {
const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing');
const [errorMessage, setErrorMessage] = useState('');
const navigate = useNavigate();
const { provider } = useParams<{ provider: string }>();
const location = useLocation();
useEffect(() => {
const processCallback = async () => {
try {
// Check if we're in a popup window
const isPopup = window.opener && window.opener !== window;
// Extract OAuth callback parameters
// Try both query params and hash params (some providers use hash)
const searchParams = new URLSearchParams(location.search);
const hashParams = new URLSearchParams(location.hash.substring(1));
const code = searchParams.get('code') || hashParams.get('code');
const state = searchParams.get('state') || hashParams.get('state');
const error = searchParams.get('error') || hashParams.get('error');
const errorDescription = searchParams.get('error_description') || hashParams.get('error_description');
// Check for OAuth errors
if (error) {
const message = errorDescription || error || 'Authentication failed';
throw new Error(message);
}
// Validate required parameters
if (!code || !state) {
throw new Error('Missing required OAuth parameters');
}
if (!provider) {
throw new Error('Missing OAuth provider');
}
// Exchange code for tokens
const response = await handleOAuthCallback(provider, code, state);
// Store tokens in cookies (accessible across subdomains)
setCookie('access_token', response.access, 7);
setCookie('refresh_token', response.refresh, 7);
// Clear session cookie to prevent interference with JWT
document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.lvh.me';
document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
setStatus('success');
// Determine redirect URL based on user role
const user = response.user;
const currentHostname = window.location.hostname;
const currentPort = window.location.port;
let targetUrl = '/';
let needsRedirect = false;
// Platform users (superuser, platform_manager, platform_support)
if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) {
const targetHostname = 'platform.lvh.me';
needsRedirect = currentHostname !== targetHostname;
if (needsRedirect) {
const portStr = currentPort ? `:${currentPort}` : '';
targetUrl = `http://${targetHostname}${portStr}/`;
}
}
// Business users - redirect to their business subdomain
else if (user.business_subdomain) {
const targetHostname = `${user.business_subdomain}.lvh.me`;
needsRedirect = currentHostname !== targetHostname;
if (needsRedirect) {
const portStr = currentPort ? `:${currentPort}` : '';
targetUrl = `http://${targetHostname}${portStr}/`;
}
}
// Handle popup vs redirect flows
if (isPopup) {
// Post message to parent window
window.opener.postMessage(
{
type: 'oauth-success',
provider,
user: response.user,
needsRedirect,
targetUrl,
},
window.location.origin
);
// Close popup after short delay
setTimeout(() => {
window.close();
}, 1000);
} else {
// Standard redirect flow
setTimeout(() => {
if (needsRedirect) {
// Redirect to different subdomain
window.location.href = targetUrl;
} else {
// Navigate to dashboard on same subdomain
navigate(targetUrl);
}
}, 1500);
}
} catch (err: any) {
console.error('OAuth callback error:', err);
setStatus('error');
setErrorMessage(err.message || 'Authentication failed. Please try again.');
// If in popup, post error to parent
if (window.opener && window.opener !== window) {
window.opener.postMessage(
{
type: 'oauth-error',
provider,
error: err.message || 'Authentication failed',
},
window.location.origin
);
// Close popup after delay
setTimeout(() => {
window.close();
}, 3000);
}
}
};
processCallback();
}, [provider, location, navigate]);
const handleTryAgain = () => {
const currentHostname = window.location.hostname;
const currentPort = window.location.port;
const portStr = currentPort ? `:${currentPort}` : '';
// Redirect to login page
if (currentHostname.includes('platform.lvh.me')) {
window.location.href = `http://platform.lvh.me${portStr}/login`;
} else if (currentHostname.includes('.lvh.me')) {
// On business subdomain - go to their login
window.location.href = `http://${currentHostname}${portStr}/login`;
} else {
// Fallback
navigate('/login');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="max-w-md w-full px-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
{/* Logo */}
<div className="flex justify-center mb-6">
<div className="flex items-center gap-3">
<SmoothScheduleLogo className="w-10 h-10 text-brand-500" />
<span className="font-bold text-xl tracking-tight text-gray-900 dark:text-white">
Smooth Schedule
</span>
</div>
</div>
{/* Processing State */}
{status === 'processing' && (
<div className="text-center">
<div className="flex justify-center mb-4">
<Loader2 className="w-12 h-12 text-blue-600 animate-spin" />
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Completing Sign In...
</h2>
<p className="text-gray-600 dark:text-gray-400">
Please wait while we authenticate your account
</p>
</div>
)}
{/* Success State */}
{status === 'success' && (
<div className="text-center">
<div className="flex justify-center mb-4">
<div className="rounded-full bg-green-100 dark:bg-green-900/30 p-3">
<CheckCircle className="w-12 h-12 text-green-600 dark:text-green-400" />
</div>
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Authentication Successful!
</h2>
<p className="text-gray-600 dark:text-gray-400">
Redirecting to your dashboard...
</p>
</div>
)}
{/* Error State */}
{status === 'error' && (
<div className="text-center">
<div className="flex justify-center mb-4">
<div className="rounded-full bg-red-100 dark:bg-red-900/30 p-3">
<AlertCircle className="w-12 h-12 text-red-600 dark:text-red-400" />
</div>
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Authentication Failed
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
{errorMessage}
</p>
<button
onClick={handleTryAgain}
className="w-full px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Try Again
</button>
</div>
)}
{/* Provider Info */}
{provider && status === 'processing' && (
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<p className="text-center text-sm text-gray-500 dark:text-gray-400">
Authenticating with{' '}
<span className="font-medium capitalize">{provider}</span>
</p>
</div>
)}
</div>
{/* Additional Help Text */}
{status === 'error' && (
<div className="mt-4 text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">
If the problem persists, please contact support
</p>
</div>
)}
</div>
</div>
);
};
export default OAuthCallback;