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>
249 lines
8.9 KiB
TypeScript
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;
|