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>
This commit is contained in:
258
legacy_reference/frontend/src/pages/OAuthCallback.tsx
Normal file
258
legacy_reference/frontend/src/pages/OAuthCallback.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user