Implement automatic silent OAuth token refresh and expiration tracking for Google Workspace API calls

This commit is contained in:
2026-07-23 12:07:27 -04:00
parent 65ec2ab580
commit 186779fbbd
2 changed files with 37 additions and 8 deletions
+1 -1
View File
@@ -1 +1 @@
249378
267794
+36 -7
View File
@@ -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<string | null> => {
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: {