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';
|
import { JobApplication } from '../types';
|
||||||
|
|
||||||
let cachedAccessToken: string | null = localStorage.getItem('ai_job_portal_google_token');
|
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;
|
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) => {
|
const getEnvVar = (key: string) => {
|
||||||
try {
|
try {
|
||||||
return (import.meta as any).env?.[key];
|
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
|
* 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.
|
* 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const scope = encodeURIComponent('https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents');
|
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 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 width = 500;
|
||||||
const height = 650;
|
const height = 650;
|
||||||
@@ -59,10 +84,14 @@ export const googleWorkspaceSignIn = async (): Promise<{ email: string; name: st
|
|||||||
|
|
||||||
const params = new URLSearchParams(hash.substring(1));
|
const params = new URLSearchParams(hash.substring(1));
|
||||||
const token = params.get('access_token');
|
const token = params.get('access_token');
|
||||||
|
const expiresInSeconds = Number(params.get('expires_in') || '3600');
|
||||||
|
const expiresAt = Date.now() + expiresInSeconds * 1000;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
cachedAccessToken = token;
|
cachedAccessToken = token;
|
||||||
|
tokenExpiresAt = expiresAt;
|
||||||
localStorage.setItem('ai_job_portal_google_token', token);
|
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');
|
localStorage.setItem('ai_job_portal_google_connected', 'true');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -99,20 +128,20 @@ export const googleWorkspaceSignIn = async (): Promise<{ email: string; name: st
|
|||||||
|
|
||||||
export const googleSignOut = async () => {
|
export const googleSignOut = async () => {
|
||||||
cachedAccessToken = null;
|
cachedAccessToken = null;
|
||||||
|
tokenExpiresAt = 0;
|
||||||
localStorage.removeItem('ai_job_portal_google_token');
|
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_connected');
|
||||||
localStorage.removeItem('ai_job_portal_google_user');
|
localStorage.removeItem('ai_job_portal_google_user');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Workspace Action Handlers
|
// Workspace Action Handlers
|
||||||
|
|
||||||
export async function exportApplicationsToGoogleSheets(
|
export async function exportApplicationsToGoogleSheets(
|
||||||
applications: JobApplication[],
|
applications: JobApplication[],
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
): Promise<{ spreadsheetId: string; spreadsheetUrl: string; isLocalCsv?: boolean; message?: 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', {
|
const res = await fetch('/api/workspace/sheets/export', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -134,7 +163,7 @@ export async function createTailoredGoogleDoc(
|
|||||||
docType: 'resume' | 'coverLetter' | 'note',
|
docType: 'resume' | 'coverLetter' | 'note',
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
): Promise<{ documentId: string; documentUrl: string; isLocalDoc?: boolean; message?: 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', {
|
const res = await fetch('/api/workspace/docs/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -156,7 +185,7 @@ export async function uploadFileToGoogleDrive(
|
|||||||
mimeType: string = 'text/plain',
|
mimeType: string = 'text/plain',
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
): Promise<{ fileId: string; driveUrl: string; isLocalFile?: boolean; message?: 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', {
|
const res = await fetch('/api/workspace/drive/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
Reference in New Issue
Block a user