Implement automatic silent OAuth token refresh and expiration tracking for Google Workspace API calls
This commit is contained in:
+36
-7
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user