Files
smoothschedule/frontend/src/pages/OAuthCallback.tsx
poduck 4cd6610f2a Fix double /api/ prefix in API endpoint calls
When VITE_API_URL=/api, axios baseURL is already set to /api. However, all endpoint calls included the /api/ prefix, creating double paths like /api/api/auth/login/.

Removed /api/ prefix from 81 API endpoint calls across 22 files:
- src/api/auth.ts - Fixed login, logout, me, refresh, hijack endpoints
- src/api/client.ts - Fixed token refresh endpoint
- src/api/profile.ts - Fixed all profile, email, password, MFA, sessions endpoints
- src/hooks/*.ts - Fixed all remaining API calls (users, appointments, resources, etc)
- src/pages/*.tsx - Fixed signup and email verification endpoints

This ensures API requests use the correct path: /api/auth/login/ instead of /api/api/auth/login/

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 15:27:57 -05:00

249 lines
8.9 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 { getCookieDomain, buildSubdomainUrl } from '../utils/domain';
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
const cookieDomain = getCookieDomain();
document.cookie = `sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${cookieDomain}`;
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;
let targetUrl = '/';
let needsRedirect = false;
let targetSubdomain: string | null = null;
// Platform users (superuser, platform_manager, platform_support)
if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) {
targetSubdomain = 'platform';
}
// Business users - redirect to their business subdomain
else if (user.business_subdomain) {
targetSubdomain = user.business_subdomain;
}
// Check if redirect is needed
if (targetSubdomain) {
const baseDomain = window.location.hostname.split('.').slice(-2).join('.');
const targetHostname = `${targetSubdomain}.${baseDomain}`;
needsRedirect = currentHostname !== targetHostname;
if (needsRedirect) {
targetUrl = buildSubdomainUrl(targetSubdomain, '/');
}
}
// 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 = () => {
// Simply navigate to login on current subdomain
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;