238 lines
6.3 KiB
TypeScript
238 lines
6.3 KiB
TypeScript
import { browser } from '$app/environment';
|
|
|
|
// Client-side only functions
|
|
export const scopes = [
|
|
'https://www.googleapis.com/auth/gmail.send',
|
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
'https://www.googleapis.com/auth/drive.readonly',
|
|
'https://www.googleapis.com/auth/spreadsheets.readonly'
|
|
];
|
|
|
|
/**
|
|
* Initialize Google Auth (placeholder for client-side)
|
|
*/
|
|
export async function initGoogleAuth(): Promise<void> {
|
|
if (!browser) return;
|
|
// Google Auth initialization is handled by the OAuth flow
|
|
// No initialization needed for our server-side approach
|
|
}
|
|
|
|
/**
|
|
* Get the Google Auth URL
|
|
* @returns URL for Google OAuth
|
|
*/
|
|
export function getAuthUrl(): string {
|
|
if (!browser) return '';
|
|
// This should be obtained from the server
|
|
return '/auth/google';
|
|
}
|
|
|
|
/**
|
|
* Check if an access token is valid
|
|
* @param accessToken - Google access token to validate
|
|
* @returns True if the token is valid
|
|
*/
|
|
export async function isTokenValid(accessToken: string): Promise<boolean> {
|
|
if (!browser) return false;
|
|
|
|
try {
|
|
const response = await fetch(`https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${accessToken}`);
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.expires_in && data.expires_in > 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Error validating token:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh an access token using the refresh token
|
|
* @param refreshToken - Google refresh token
|
|
* @returns New access token or null if failed
|
|
*/
|
|
export async function refreshAccessToken(refreshToken: string): Promise<string | null> {
|
|
try {
|
|
const response = await fetch('/private/api/google/auth/refresh', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ refreshToken })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.accessToken;
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error refreshing token:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Google user information
|
|
* @param accessToken - Google access token
|
|
* @returns User info including email, name, and picture
|
|
*/
|
|
export async function getUserInfo(accessToken: string): Promise<{ email: string; name: string; picture: string } | null> {
|
|
try {
|
|
const response = await fetch('/private/api/google/auth/userinfo', {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error fetching user info:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authenticate with Google using OAuth popup flow
|
|
* @returns Authentication result with success status and tokens
|
|
*/
|
|
export async function authenticateWithGoogle(): Promise<{
|
|
success: boolean;
|
|
refreshToken?: string;
|
|
userEmail?: string;
|
|
error?: string;
|
|
}> {
|
|
if (!browser) {
|
|
return { success: false, error: 'Not in browser environment' };
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
try {
|
|
// Open popup window for OAuth
|
|
const popup = window.open(
|
|
'/auth/google',
|
|
'google-auth',
|
|
'width=500,height=600,scrollbars=yes,resizable=yes,left=' +
|
|
Math.round(window.screen.width / 2 - 250) + ',top=' +
|
|
Math.round(window.screen.height / 2 - 300)
|
|
);
|
|
|
|
if (!popup) {
|
|
resolve({ success: false, error: 'Failed to open popup window. Please allow popups for this site.' });
|
|
return;
|
|
}
|
|
|
|
let authCompleted = false;
|
|
let popupTimer: number | null = null;
|
|
|
|
// Store current timestamp to detect changes in localStorage
|
|
const startTimestamp = localStorage.getItem('google_auth_timestamp') ?? '0';
|
|
|
|
// Poll localStorage for auth completion
|
|
const pollInterval = setInterval(() => {
|
|
try {
|
|
const currentTimestamp = localStorage.getItem('google_auth_timestamp');
|
|
|
|
// If timestamp has changed, auth is complete
|
|
if (currentTimestamp && currentTimestamp !== startTimestamp) {
|
|
handleAuthSuccess();
|
|
}
|
|
} catch (e) {
|
|
console.error('Error checking auth timestamp:', e);
|
|
}
|
|
}, 500); // Poll every 500ms
|
|
|
|
// Common handler for authentication success
|
|
function handleAuthSuccess() {
|
|
if (authCompleted) return; // Prevent duplicate handling
|
|
|
|
authCompleted = true;
|
|
|
|
// Clean up timers
|
|
clearInterval(pollInterval);
|
|
if (popupTimer) clearTimeout(popupTimer);
|
|
|
|
// Get tokens from localStorage
|
|
const refreshToken = localStorage.getItem('google_refresh_token');
|
|
const userEmail = localStorage.getItem('google_user_email');
|
|
|
|
if (refreshToken) {
|
|
resolve({
|
|
success: true,
|
|
refreshToken,
|
|
userEmail: userEmail ?? undefined
|
|
});
|
|
} else {
|
|
resolve({ success: false, error: 'No refresh token found after authentication' });
|
|
}
|
|
}
|
|
|
|
// Clean up function to handle all cleanup in one place
|
|
const cleanUp = () => {
|
|
clearInterval(pollInterval);
|
|
if (popupTimer) clearTimeout(popupTimer);
|
|
};
|
|
|
|
// Set a timeout for initial auth check
|
|
popupTimer = setTimeout(() => {
|
|
if (!authCompleted) {
|
|
cleanUp();
|
|
// Check if tokens were stored by the popup before it was closed
|
|
const refreshToken = localStorage.getItem('google_refresh_token');
|
|
const userEmail = localStorage.getItem('google_user_email');
|
|
|
|
if (refreshToken) {
|
|
resolve({
|
|
success: true,
|
|
refreshToken,
|
|
userEmail: userEmail ?? undefined
|
|
});
|
|
} else {
|
|
resolve({ success: false, error: 'Authentication timeout or cancelled' });
|
|
}
|
|
}
|
|
}, 30 * 1000) as unknown as number;
|
|
|
|
// Final cleanup timeout
|
|
setTimeout(() => {
|
|
if (!authCompleted) {
|
|
cleanUp();
|
|
resolve({ success: false, error: 'Authentication timeout' });
|
|
}
|
|
}, 60 * 1000);
|
|
|
|
} catch (error) {
|
|
console.error('Error connecting to Google:', error);
|
|
resolve({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Revoke a Google access token
|
|
* @param accessToken - Google access token to revoke
|
|
* @returns True if revocation was successful
|
|
*/
|
|
export async function revokeToken(accessToken: string): Promise<boolean> {
|
|
try {
|
|
const response = await fetch('/private/api/google/auth/revoke', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ accessToken })
|
|
});
|
|
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error('Error revoking token:', error);
|
|
return false;
|
|
}
|
|
}
|