Implement direct standalone Google OAuth 2.0 flow for live Google Sheets and Drive sync
This commit is contained in:
@@ -110,13 +110,23 @@ export const GoogleWorkspaceHub: React.FC<GoogleWorkspaceHubProps> = ({
|
||||
const [customTokenInput, setCustomTokenInput] = useState('');
|
||||
|
||||
const handleConnectGoogle = async () => {
|
||||
// 100% Pure Local Execution Mode: Connect directly without external Firebase Auth popup dependency
|
||||
const userObj = { email: 'dkifer19@gmail.com', name: 'David Kifer' };
|
||||
setIsConnected(true);
|
||||
setConnectedUser(userObj);
|
||||
localStorage.setItem('ai_job_portal_google_connected', 'true');
|
||||
localStorage.setItem('ai_job_portal_google_user', JSON.stringify(userObj));
|
||||
showToast('✓ Google Workspace connected for David Kifer (dkifer19@gmail.com)!');
|
||||
try {
|
||||
showToast('Opening Google Account login window...');
|
||||
const res = await googleWorkspaceSignIn();
|
||||
if (res) {
|
||||
setIsConnected(true);
|
||||
setAccessToken(res.accessToken);
|
||||
setConnectedUser({
|
||||
email: res.email || 'dkifer19@gmail.com',
|
||||
name: res.name || 'David Kifer'
|
||||
});
|
||||
showToast('✓ Google Workspace connected! Live Google Drive & Sheets enabled.');
|
||||
loadDriveFiles(res.accessToken);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Google Direct OAuth Error:", err);
|
||||
showToast(`Google Sign-In Notice: ${err.message || String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyCustomToken = () => {
|
||||
|
||||
+86
-73
@@ -1,16 +1,9 @@
|
||||
import { initializeApp, getApps, getApp } from 'firebase/app';
|
||||
import {
|
||||
getAuth,
|
||||
signInWithPopup,
|
||||
GoogleAuthProvider,
|
||||
onAuthStateChanged,
|
||||
User,
|
||||
signOut
|
||||
} from 'firebase/auth';
|
||||
import rawFirebaseConfig from '../../firebase-applet-config.json';
|
||||
import { JobApplication } from '../types';
|
||||
|
||||
// Load config dynamically from environment variables with json fallback
|
||||
let cachedAccessToken: string | null = localStorage.getItem('ai_job_portal_google_token');
|
||||
|
||||
export const getCachedAccessToken = () => cachedAccessToken;
|
||||
|
||||
const getEnvVar = (key: string) => {
|
||||
try {
|
||||
return (import.meta as any).env?.[key];
|
||||
@@ -19,80 +12,100 @@ const getEnvVar = (key: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const firebaseConfig = {
|
||||
projectId: getEnvVar('VITE_FIREBASE_PROJECT_ID') || rawFirebaseConfig.projectId,
|
||||
appId: getEnvVar('VITE_FIREBASE_APP_ID') || rawFirebaseConfig.appId,
|
||||
apiKey: getEnvVar('VITE_FIREBASE_API_KEY') || rawFirebaseConfig.apiKey,
|
||||
authDomain: getEnvVar('VITE_FIREBASE_AUTH_DOMAIN') || rawFirebaseConfig.authDomain,
|
||||
storageBucket: getEnvVar('VITE_FIREBASE_STORAGE_BUCKET') || rawFirebaseConfig.storageBucket,
|
||||
messagingSenderId: getEnvVar('VITE_FIREBASE_MESSAGING_SENDER_ID') || rawFirebaseConfig.messagingSenderId,
|
||||
oAuthClientId: getEnvVar('VITE_GOOGLE_OAUTH_CLIENT_ID') || rawFirebaseConfig.oAuthClientId
|
||||
};
|
||||
const OAUTH_CLIENT_ID = getEnvVar('VITE_GOOGLE_OAUTH_CLIENT_ID') || '488548028456-tal52pta8fm2uc9t4o2u82itqvi91rp7.apps.googleusercontent.com';
|
||||
|
||||
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
|
||||
export const auth = getAuth(app);
|
||||
/**
|
||||
* Direct Google OAuth 2.0 Flow via Google Identity Services
|
||||
* Obtains real access token directly from accounts.google.com for Google Drive/Sheets API calls.
|
||||
*/
|
||||
export const googleWorkspaceSignIn = async (): Promise<{ email: string; name: string; accessToken: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scope = encodeURIComponent('https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents');
|
||||
const redirectUri = encodeURIComponent(`${window.location.origin}`);
|
||||
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${OAUTH_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=token&scope=${scope}&prompt=select_account`;
|
||||
|
||||
const provider = new GoogleAuthProvider();
|
||||
// Workspace Scopes
|
||||
provider.addScope('https://www.googleapis.com/auth/spreadsheets');
|
||||
provider.addScope('https://www.googleapis.com/auth/drive');
|
||||
provider.addScope('https://www.googleapis.com/auth/documents');
|
||||
const width = 500;
|
||||
const height = 650;
|
||||
const left = window.screenX + (window.outerWidth - width) / 2;
|
||||
const top = window.screenY + (window.outerHeight - height) / 2;
|
||||
|
||||
let isSigningIn = false;
|
||||
let cachedAccessToken: string | null = null;
|
||||
const popup = window.open(
|
||||
authUrl,
|
||||
'Google Workspace OAuth Sign-In',
|
||||
`width=${width},height=${height},left=${left},top=${top},scrollbars=yes`
|
||||
);
|
||||
|
||||
export const initAuthListener = (
|
||||
onSuccess: (user: User, token: string) => void,
|
||||
onFailure: () => void
|
||||
) => {
|
||||
return onAuthStateChanged(auth, async (user) => {
|
||||
if (user) {
|
||||
if (cachedAccessToken) {
|
||||
onSuccess(user, cachedAccessToken);
|
||||
} else if (!isSigningIn) {
|
||||
// If logged in without token in memory (e.g., page refresh), trigger quick re-auth if needed
|
||||
onFailure();
|
||||
if (!popup) {
|
||||
throw new Error('Popup window was blocked by your browser. Please allow popups for http://localhost:3000.');
|
||||
}
|
||||
} else {
|
||||
cachedAccessToken = null;
|
||||
onFailure();
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
if (popup.closed) {
|
||||
clearInterval(interval);
|
||||
if (!cachedAccessToken) {
|
||||
reject(new Error('Sign-in window closed before completing authentication.'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUrl = popup.location.href;
|
||||
if (currentUrl && currentUrl.includes(window.location.origin)) {
|
||||
const hash = popup.location.hash;
|
||||
if (hash && hash.includes('access_token=')) {
|
||||
clearInterval(interval);
|
||||
popup.close();
|
||||
|
||||
const params = new URLSearchParams(hash.substring(1));
|
||||
const token = params.get('access_token');
|
||||
|
||||
if (token) {
|
||||
cachedAccessToken = token;
|
||||
localStorage.setItem('ai_job_portal_google_token', token);
|
||||
localStorage.setItem('ai_job_portal_google_connected', 'true');
|
||||
|
||||
try {
|
||||
const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (userRes.ok) {
|
||||
const userData = await userRes.json();
|
||||
resolve({
|
||||
email: userData.email || 'dkifer19@gmail.com',
|
||||
name: userData.name || 'David Kifer',
|
||||
accessToken: token
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
resolve({
|
||||
email: 'dkifer19@gmail.com',
|
||||
name: 'David Kifer',
|
||||
accessToken: token
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 400);
|
||||
|
||||
} catch (err: any) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const googleWorkspaceSignIn = async (): Promise<{ user: User; accessToken: string } | null> => {
|
||||
try {
|
||||
isSigningIn = true;
|
||||
const result = await signInWithPopup(auth, provider);
|
||||
const credential = GoogleAuthProvider.credentialFromResult(result);
|
||||
if (!credential?.accessToken) {
|
||||
throw new Error('Failed to obtain Google access token');
|
||||
}
|
||||
cachedAccessToken = credential.accessToken;
|
||||
return { user: result.user, accessToken: cachedAccessToken };
|
||||
} catch (error: any) {
|
||||
console.error('Google Workspace Sign-in Error:', error);
|
||||
|
||||
// Check if error is due to domain restriction or cross-origin popup block
|
||||
const errCode = error?.code || '';
|
||||
if (errCode.includes('unauthorized-domain') || errCode.includes('popup-closed') || errCode.includes('auth/invalid-api-key')) {
|
||||
throw new Error(`Google Auth Domain Restriction: Your local origin (localhost:3000) is not listed in the Firebase/Google Auth authorized domains. Error Code: ${errCode}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
isSigningIn = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCachedAccessToken = (): string | null => {
|
||||
return cachedAccessToken;
|
||||
};
|
||||
|
||||
export const googleSignOut = async (): Promise<void> => {
|
||||
await signOut(auth);
|
||||
export const googleSignOut = async () => {
|
||||
cachedAccessToken = null;
|
||||
localStorage.removeItem('ai_job_portal_google_token');
|
||||
localStorage.removeItem('ai_job_portal_google_connected');
|
||||
localStorage.removeItem('ai_job_portal_google_user');
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Workspace Action Handlers
|
||||
|
||||
export async function exportApplicationsToGoogleSheets(
|
||||
|
||||
Reference in New Issue
Block a user