diff --git a/.app.pid b/.app.pid index d58c2d5..cdfc4e6 100644 --- a/.app.pid +++ b/.app.pid @@ -1 +1 @@ -249378 +267794 diff --git a/src/lib/googleAuth.ts b/src/lib/googleAuth.ts index 9ddafa2..d5b8191 100644 --- a/src/lib/googleAuth.ts +++ b/src/lib/googleAuth.ts @@ -1,9 +1,33 @@ import { JobApplication } from '../types'; let cachedAccessToken: string | null = localStorage.getItem('ai_job_portal_google_token'); +let tokenExpiresAt: number = Number(localStorage.getItem('ai_job_portal_google_token_exp') || '0'); export const getCachedAccessToken = () => cachedAccessToken; +/** + * Silent / Interactive Token Refresh Mechanism + * Automatically checks token age and triggers silent OAuth popup or re-auth if token is expired. + */ +export const ensureValidAccessToken = async (): Promise => { + const now = Date.now(); + + // If token exists and is valid for at least another 3 minutes, reuse it + if (cachedAccessToken && tokenExpiresAt > now + 3 * 60 * 1000) { + return cachedAccessToken; + } + + // Token is expired or expiring soon -> Trigger silent refresh / prompt-less re-auth + console.log('[Google Auth] OAuth Token expired or expiring soon. Triggering automatic token refresh...'); + try { + const authResult = await googleWorkspaceSignIn(true); // silent = true + return authResult.accessToken; + } catch (err: any) { + console.warn('[Google Auth] Silent token refresh notice:', err.message); + return cachedAccessToken; + } +}; + const getEnvVar = (key: string) => { try { return (import.meta as any).env?.[key]; @@ -18,12 +42,13 @@ const OAUTH_CLIENT_ID = getEnvVar('VITE_GOOGLE_OAUTH_CLIENT_ID') || '48854802845 * 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 }> => { +export const googleWorkspaceSignIn = async (silentMode: boolean = false): 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 promptParam = silentMode ? 'none' : 'select_account'; + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${OAUTH_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=token&scope=${scope}&prompt=${promptParam}`; const width = 500; const height = 650; @@ -59,10 +84,14 @@ export const googleWorkspaceSignIn = async (): Promise<{ email: string; name: st const params = new URLSearchParams(hash.substring(1)); const token = params.get('access_token'); + const expiresInSeconds = Number(params.get('expires_in') || '3600'); + const expiresAt = Date.now() + expiresInSeconds * 1000; if (token) { cachedAccessToken = token; + tokenExpiresAt = expiresAt; localStorage.setItem('ai_job_portal_google_token', token); + localStorage.setItem('ai_job_portal_google_token_exp', String(expiresAt)); localStorage.setItem('ai_job_portal_google_connected', 'true'); try { @@ -99,20 +128,20 @@ export const googleWorkspaceSignIn = async (): Promise<{ email: string; name: st export const googleSignOut = async () => { cachedAccessToken = null; + tokenExpiresAt = 0; localStorage.removeItem('ai_job_portal_google_token'); + localStorage.removeItem('ai_job_portal_google_token_exp'); localStorage.removeItem('ai_job_portal_google_connected'); localStorage.removeItem('ai_job_portal_google_user'); }; - - // Workspace Action Handlers export async function exportApplicationsToGoogleSheets( applications: JobApplication[], accessToken?: string ): Promise<{ spreadsheetId: string; spreadsheetUrl: string; isLocalCsv?: boolean; message?: string }> { - const token = accessToken || cachedAccessToken; + const token = accessToken || (await ensureValidAccessToken()) || cachedAccessToken; const res = await fetch('/api/workspace/sheets/export', { method: 'POST', headers: { @@ -134,7 +163,7 @@ export async function createTailoredGoogleDoc( docType: 'resume' | 'coverLetter' | 'note', accessToken?: string ): Promise<{ documentId: string; documentUrl: string; isLocalDoc?: boolean; message?: string }> { - const token = accessToken || cachedAccessToken; + const token = accessToken || (await ensureValidAccessToken()) || cachedAccessToken; const res = await fetch('/api/workspace/docs/create', { method: 'POST', headers: { @@ -156,7 +185,7 @@ export async function uploadFileToGoogleDrive( mimeType: string = 'text/plain', accessToken?: string ): Promise<{ fileId: string; driveUrl: string; isLocalFile?: boolean; message?: string }> { - const token = accessToken || cachedAccessToken; + const token = accessToken || (await ensureValidAccessToken()) || cachedAccessToken; const res = await fetch('/api/workspace/drive/upload', { method: 'POST', headers: {