feat: Complete Google Search Console integration with Clerk authentication

- Add GSC API service with OAuth2 authentication
- Implement Clerk authentication for frontend and backend
- Add GSC login button and OAuth callback handling
- Create comprehensive GSC data fetching and caching
- Add authentication middleware for backend API protection
- Implement real-time GSC data integration in SEO dashboard
- Add user-specific GSC site management
- Include comprehensive logging and error handling
- Add TypeScript support and proper type definitions
- Create environment templates and setup documentation
- Update gitignore to exclude sensitive credential files

Features added:
- GSC OAuth2 authentication flow
- Real-time search analytics data
- Site list management
- Sitemap analysis
- User-specific data isolation
- Comprehensive error handling
- Authentication token management
- Popup-based OAuth flow
- Data caching and refresh mechanisms

Note: gsc_credentials.json should be created locally with your Google OAuth credentials
This commit is contained in:
Om-Singh1808
2025-09-06 02:43:50 +05:30
committed by ي
parent aeb7751d48
commit 0a7d9bfd21
19 changed files with 1912 additions and 87 deletions

View File

@@ -90,7 +90,7 @@ const BusinessDescriptionStep: React.FC<BusinessDescriptionStepProps> = ({ onBac
rows={4}
margin="normal"
required
helperText={`${formData.business_description.length}/1000 characters`}
helperText={`${formData.business_description?.length || 0}/1000 characters`}
inputProps={{ maxLength: 1000 }}
disabled={loading}
/>
@@ -101,7 +101,7 @@ const BusinessDescriptionStep: React.FC<BusinessDescriptionStepProps> = ({ onBac
onChange={handleChange}
fullWidth
margin="normal"
helperText={`${formData.industry.length}/100 characters`}
helperText={`${formData.industry?.length || 0}/100 characters`}
inputProps={{ maxLength: 100 }}
disabled={loading}
/>

View File

@@ -11,6 +11,7 @@ import {
Button
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth, useUser, SignInButton, SignOutButton } from '@clerk/clerk-react';
// Shared components
import { DashboardContainer, GlassCard } from '../shared/styled';
@@ -19,6 +20,9 @@ import { SEOCopilotKitProvider, SEOCopilotSuggestions } from './index';
// Removed SEOCopilotTest
import useSEOCopilotStore from '../../stores/seoCopilotStore';
// GSC Components
import GSCLoginButton from './components/GSCLoginButton';
// Zustand store
import { useSEODashboardStore } from '../../stores/seoDashboardStore';
@@ -29,6 +33,10 @@ import { userDataAPI } from '../../api/userData';
const SEODashboard: React.FC = () => {
const theme = useTheme();
// Clerk authentication hooks
const { isSignedIn, isLoaded } = useAuth();
const { user } = useUser();
// Zustand store hooks
const {
loading,
@@ -141,6 +149,52 @@ const SEODashboard: React.FC = () => {
return <Alert severity="error">Failed to load dashboard data</Alert>;
}
// Show sign-in prompt if not authenticated
if (!isLoaded) {
return <Skeleton variant="rectangular" height={200} />;
}
if (!isSignedIn) {
return (
<DashboardContainer>
<Container maxWidth="md">
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '60vh',
textAlign: 'center',
gap: 3
}}>
<Typography variant="h4" sx={{ color: 'white', fontWeight: 700 }}>
🔍 SEO Dashboard
</Typography>
<Typography variant="h6" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Sign in to access your SEO analytics and Google Search Console data
</Typography>
<SignInButton mode="modal">
<Button
variant="contained"
size="large"
sx={{
bgcolor: '#4285f4',
'&:hover': { bgcolor: '#3367d6' },
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600
}}
>
Sign In to Continue
</Button>
</SignInButton>
</Box>
</Container>
</DashboardContainer>
);
}
return (
<SEOCopilotKitProvider enableDebugMode={false}>
<DashboardContainer>
@@ -161,7 +215,38 @@ const SEODashboard: React.FC = () => {
AI-powered insights and actionable recommendations
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{/* User Info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={`Signed in as ${user?.primaryEmailAddress?.emailAddress || 'User'}`}
size="small"
sx={{
bgcolor: 'rgba(76, 175, 80, 0.25)',
border: '1px solid rgba(76, 175, 80, 0.45)',
color: 'white',
fontWeight: 600
}}
/>
<SignOutButton>
<Button
variant="outlined"
size="small"
sx={{
borderColor: 'rgba(255, 255, 255, 0.3)',
color: 'white',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
bgcolor: 'rgba(255, 255, 255, 0.1)'
}
}}
>
Sign Out
</Button>
</SignOutButton>
</Box>
{/* Freshness Indicator */}
{(() => {
const freshness = getAnalysisFreshness();
const chipColor = freshness.isStale ? 'rgba(255, 193, 7, 0.25)' : 'rgba(76, 175, 80, 0.25)';
@@ -195,6 +280,11 @@ const SEODashboard: React.FC = () => {
</Box>
</Box>
{/* GSC Connection Section */}
<Box sx={{ mb: 3 }}>
<GSCLoginButton />
</Box>
{/* CopilotKit Test Panel removed */}
{/* Executive Summary */}

View File

@@ -0,0 +1,162 @@
/** Google Search Console OAuth Callback Handler Component. */
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
CircularProgress,
Alert,
Paper
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon
} from '@mui/icons-material';
import { gscAPI } from '../../../api/gsc';
const GSCAuthCallback: React.FC = () => {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [message, setMessage] = useState<string>('Processing authentication...');
useEffect(() => {
handleOAuthCallback();
}, []);
const handleOAuthCallback = async () => {
try {
console.log('GSC Auth Callback: Processing OAuth callback');
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
throw new Error(`OAuth error: ${error}`);
}
if (!code || !state) {
throw new Error('Missing authorization code or state parameter');
}
console.log('GSC Auth Callback: Code and state received, processing...');
// Handle the callback
const result = await gscAPI.handleCallback(code, state);
if (result.success) {
setStatus('success');
setMessage('Successfully connected to Google Search Console!');
console.log('GSC Auth Callback: Authentication successful');
// Notify parent window
if (window.opener) {
window.opener.postMessage({ type: 'GSC_AUTH_SUCCESS' }, '*');
}
// Close popup after a short delay
setTimeout(() => {
window.close();
}, 2000);
} else {
throw new Error(result.message || 'Authentication failed');
}
} catch (error) {
console.error('GSC Auth Callback: Error processing callback:', error);
setStatus('error');
setMessage(error instanceof Error ? error.message : 'Authentication failed');
// Notify parent window of error
if (window.opener) {
window.opener.postMessage({
type: 'GSC_AUTH_ERROR',
error: message
}, '*');
}
}
};
const getStatusIcon = () => {
switch (status) {
case 'loading':
return <CircularProgress size={48} />;
case 'success':
return <CheckCircleIcon sx={{ fontSize: 48, color: 'success.main' }} />;
case 'error':
return <ErrorIcon sx={{ fontSize: 48, color: 'error.main' }} />;
default:
return null;
}
};
const getStatusColor = () => {
switch (status) {
case 'success':
return 'success';
case 'error':
return 'error';
default:
return 'info';
}
};
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
p: 3,
backgroundColor: 'background.default'
}}
>
<Paper
elevation={3}
sx={{
p: 4,
textAlign: 'center',
maxWidth: 400,
width: '100%'
}}
>
<Box sx={{ mb: 3 }}>
{getStatusIcon()}
</Box>
<Typography variant="h5" component="h1" gutterBottom>
{status === 'loading' && 'Connecting to Google Search Console...'}
{status === 'success' && 'Connection Successful!'}
{status === 'error' && 'Connection Failed'}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{message}
</Typography>
{status === 'success' && (
<Alert severity="success" sx={{ mb: 2 }}>
You can now close this window and return to the SEO Dashboard.
</Alert>
)}
{status === 'error' && (
<Alert severity="error" sx={{ mb: 2 }}>
Please try again or contact support if the problem persists.
</Alert>
)}
{status === 'loading' && (
<Typography variant="body2" color="text.secondary">
Please wait while we complete the authentication process...
</Typography>
)}
</Paper>
</Box>
);
};
export default GSCAuthCallback;

View File

@@ -0,0 +1,279 @@
/** Google Search Console Login Button Component for ALwrity SEO Dashboard. */
import React, { useState, useEffect } from 'react';
import {
Button,
Chip,
Box,
Typography,
CircularProgress,
Alert,
Dialog,
DialogTitle,
DialogContent,
DialogActions
} from '@mui/material';
import {
Google as GoogleIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Link as LinkIcon,
LinkOff as LinkOffIcon
} from '@mui/icons-material';
import { useAuth } from '@clerk/clerk-react';
import { gscAPI, GSCStatusResponse } from '../../../api/gsc';
interface GSCLoginButtonProps {
onStatusChange?: (connected: boolean) => void;
}
const GSCLoginButton: React.FC<GSCLoginButtonProps> = ({ onStatusChange }) => {
const { getToken } = useAuth();
const [status, setStatus] = useState<GSCStatusResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
// Set up auth token getter for GSC API
useEffect(() => {
const setupAuth = async () => {
try {
const token = await getToken();
if (token) {
gscAPI.setAuthTokenGetter(async () => {
try {
return await getToken();
} catch (error) {
console.error('Error getting auth token:', error);
return null;
}
});
}
} catch (error) {
console.error('Error setting up auth:', error);
}
};
setupAuth();
}, [getToken]);
// Check GSC connection status on component mount
useEffect(() => {
checkGSCStatus();
}, []);
const checkGSCStatus = async () => {
try {
console.log('GSC Login Button: Checking connection status');
setLoading(true);
setError(null);
const statusResponse = await gscAPI.getStatus();
setStatus(statusResponse);
if (onStatusChange) {
onStatusChange(statusResponse.connected);
}
console.log('GSC Login Button: Status checked, connected:', statusResponse.connected);
} catch (err) {
console.error('GSC Login Button: Error checking status:', err);
setError('Failed to check GSC connection status');
} finally {
setLoading(false);
}
};
const handleConnectGSC = async () => {
try {
console.log('GSC Login Button: Initiating GSC connection');
setLoading(true);
setError(null);
const { auth_url } = await gscAPI.getAuthUrl();
// Open OAuth popup
const popup = window.open(
auth_url,
'gsc-auth',
'width=600,height=700,scrollbars=yes,resizable=yes'
);
if (!popup) {
throw new Error('Popup blocked. Please allow popups for this site.');
}
// Listen for popup completion
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
console.log('GSC Login Button: OAuth popup closed, checking status');
checkGSCStatus();
}
}, 1000);
} catch (err) {
console.error('GSC Login Button: Error connecting to GSC:', err);
setError(err instanceof Error ? err.message : 'Failed to connect to GSC');
} finally {
setLoading(false);
}
};
const handleDisconnectGSC = async () => {
try {
console.log('GSC Login Button: Disconnecting GSC');
setLoading(true);
setError(null);
await gscAPI.disconnect();
setShowDisconnectDialog(false);
// Refresh status
await checkGSCStatus();
console.log('GSC Login Button: GSC disconnected successfully');
} catch (err) {
console.error('GSC Login Button: Error disconnecting GSC:', err);
setError(err instanceof Error ? err.message : 'Failed to disconnect GSC');
} finally {
setLoading(false);
}
};
const getStatusChip = () => {
if (loading) {
return (
<Chip
icon={<CircularProgress size={16} />}
label="Checking..."
color="default"
variant="outlined"
/>
);
}
if (status?.connected) {
return (
<Chip
icon={<CheckCircleIcon />}
label="Connected"
color="success"
variant="filled"
/>
);
}
return (
<Chip
icon={<ErrorIcon />}
label="Not Connected"
color="error"
variant="outlined"
/>
);
};
const getButtonContent = () => {
if (loading) {
return (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
{status?.connected ? 'Disconnecting...' : 'Connecting...'}
</>
);
}
if (status?.connected) {
return (
<>
<LinkOffIcon sx={{ mr: 1 }} />
Disconnect GSC
</>
);
}
return (
<>
<GoogleIcon sx={{ mr: 1 }} />
Connect GSC
</>
);
};
return (
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Typography variant="h6" component="h3">
Google Search Console
</Typography>
{getStatusChip()}
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
{error}
</Alert>
)}
{status?.connected && status.sites && status.sites.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Connected Sites:
</Typography>
{status.sites.map((site, index) => (
<Chip
key={index}
icon={<LinkIcon />}
label={site.siteUrl}
size="small"
sx={{ mr: 1, mb: 1 }}
/>
))}
</Box>
)}
<Button
variant={status?.connected ? "outlined" : "contained"}
color={status?.connected ? "error" : "primary"}
onClick={status?.connected ? () => setShowDisconnectDialog(true) : handleConnectGSC}
disabled={loading}
startIcon={status?.connected ? <LinkOffIcon /> : <GoogleIcon />}
sx={{ minWidth: 200 }}
>
{getButtonContent()}
</Button>
{/* Disconnect Confirmation Dialog */}
<Dialog
open={showDisconnectDialog}
onClose={() => setShowDisconnectDialog(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Disconnect Google Search Console</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to disconnect your Google Search Console account?
This will remove all stored credentials and you'll need to reconnect to access GSC data.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowDisconnectDialog(false)}>
Cancel
</Button>
<Button
onClick={handleDisconnectGSC}
color="error"
variant="contained"
disabled={loading}
>
{loading ? <CircularProgress size={20} /> : 'Disconnect'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default GSCLoginButton;