diff --git a/src/lib/components/GoogleAuthButton.svelte b/src/lib/components/GoogleAuthButton.svelte index 928fcbd..b84361e 100644 --- a/src/lib/components/GoogleAuthButton.svelte +++ b/src/lib/components/GoogleAuthButton.svelte @@ -6,12 +6,14 @@ let { onSuccess, onError, + onDisconnect, disabled = false, size = 'default', variant = 'primary' } = $props<{ onSuccess?: (token: string) => void; onError?: (error: string) => void; + onDisconnect?: () => void; disabled?: boolean; size?: 'small' | 'default' | 'large'; variant?: 'primary' | 'secondary'; @@ -21,8 +23,8 @@ let authState = $state(createGoogleAuthState()); let authManager = new GoogleAuthManager(authState); - onMount(() => { - authManager.checkConnection(); + onMount(async () => { + await authManager.checkConnection(); }); async function handleConnect() { @@ -41,6 +43,7 @@ async function handleDisconnect() { await authManager.disconnectGoogle(); + onDisconnect?.(); } // Size classes @@ -57,7 +60,14 @@ }; -{#if authState.isConnected} +{#if authState.checking} +
+
+
+ Checking connection... +
+
+{:else if authState.isConnected}
diff --git a/src/lib/google/auth/manager.ts b/src/lib/google/auth/manager.ts index f87901e..54d8a39 100644 --- a/src/lib/google/auth/manager.ts +++ b/src/lib/google/auth/manager.ts @@ -30,7 +30,7 @@ export class GoogleAuthManager { this.state = state; } - checkConnection(): void { + async checkConnection(): Promise { this.state.checking = true; this.state.error = null; @@ -38,12 +38,39 @@ export class GoogleAuthManager { const token = localStorage.getItem('google_refresh_token'); const email = localStorage.getItem('google_user_email'); - this.state.isConnected = !!token; - this.state.token = token; - this.state.userEmail = email; + if (!token) { + this.state.isConnected = false; + this.state.token = null; + this.state.userEmail = null; + return; + } + + // Verify the token by calling our backend endpoint + const response = await fetch('/private/api/google/auth/check', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ refreshToken: token }) + }); + + if (response.ok) { + this.state.isConnected = true; + this.state.token = token; + this.state.userEmail = email; + } else { + // Token is invalid or expired + await this.disconnectGoogle(); + if (response.status === 401) { + this.state.error = 'Google session expired. Please reconnect.'; + } else { + this.state.error = 'Failed to verify connection.'; + } + } } catch (error) { console.error('Error checking connection:', error); - this.state.error = 'Failed to check connection status'; + this.state.error = 'Failed to verify connection status'; + this.state.isConnected = false; } finally { this.state.checking = false; } diff --git a/src/lib/google/sheets/server.ts b/src/lib/google/sheets/server.ts index 1323fa9..331a2c6 100644 --- a/src/lib/google/sheets/server.ts +++ b/src/lib/google/sheets/server.ts @@ -51,25 +51,48 @@ export async function getRecentSpreadsheets( * Get data from a Google Sheet * @param refreshToken - Google refresh token * @param spreadsheetId - ID of the spreadsheet - * @param range - Cell range to retrieve (default: A1:Z10) + * @param range - Optional cell range. If not provided, it will fetch the entire first sheet. * @returns Sheet data as a 2D array */ export async function getSpreadsheetData( - refreshToken: string, - spreadsheetId: string, - range: string = 'A1:Z10' + refreshToken: string, + spreadsheetId: string, + range?: string ): Promise { - const oauth = getAuthenticatedClient(refreshToken); - const sheets = google.sheets({ version: 'v4', auth: oauth }); + const oauth = getAuthenticatedClient(refreshToken); + const sheets = google.sheets({ version: 'v4', auth: oauth }); - const response = await sheets.spreadsheets.values.get({ - spreadsheetId, - range - }); + let effectiveRange = range; - return { - values: response.data.values || [] - }; + // If no range is provided, get the name of the first sheet and use that as the range + // to fetch all its content. + if (!effectiveRange) { + try { + const info = await getSpreadsheetInfo(refreshToken, spreadsheetId); + const firstSheetName = info.sheets?.[0]?.properties?.title; + + if (firstSheetName) { + // To use a sheet name as a range, it must be quoted if it contains spaces or special characters. + effectiveRange = `'${firstSheetName}'`; + } else { + // Fallback if sheet name can't be determined. + effectiveRange = 'A1:Z1000'; // A sensible default for a large preview + } + } catch (error) { + console.error(`Failed to get sheet info for spreadsheet ${spreadsheetId}`, error); + // Fallback if the info call fails + effectiveRange = 'A1:Z1000'; + } + } + + const response = await sheets.spreadsheets.values.get({ + spreadsheetId, + range: effectiveRange + }); + + return { + values: response.data.values || [] + }; } /** diff --git a/src/routes/private/api/google/auth/check/+server.ts b/src/routes/private/api/google/auth/check/+server.ts new file mode 100644 index 0000000..9b832fe --- /dev/null +++ b/src/routes/private/api/google/auth/check/+server.ts @@ -0,0 +1,32 @@ +import { json } from '@sveltejs/kit'; +import { getAuthenticatedClient } from '$lib/google/auth/server'; + +/** + * @description Verify the validity of a Google refresh token + * @method POST + * @param {Request} request + * @returns {Response} + */ +export async function POST({ request }: { request: Request }): Promise { + try { + const { refreshToken } = await request.json(); + + if (!refreshToken) { + return json({ error: 'Refresh token is required' }, { status: 400 }); + } + + // Get an authenticated client. This will attempt to get a new access token, + // which effectively validates the refresh token. + const oauth2Client = getAuthenticatedClient(refreshToken); + + // Attempt to get a new access token + await oauth2Client.getAccessToken(); + + // If no error is thrown, the token is valid + return json({ success: true }); + } catch (error) { + console.error('Failed to verify Google refresh token:', error); + // The token is likely invalid or revoked + return json({ error: 'Invalid or expired refresh token' }, { status: 401 }); + } +} diff --git a/src/routes/private/api/google/sheets/[sheetId]/data/+server.ts b/src/routes/private/api/google/sheets/[sheetId]/data/+server.ts index e0ca677..086bfdd 100644 --- a/src/routes/private/api/google/sheets/[sheetId]/data/+server.ts +++ b/src/routes/private/api/google/sheets/[sheetId]/data/+server.ts @@ -2,17 +2,18 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { googleSheetsServer } from '$lib/google/sheets/server.js'; -export const GET: RequestHandler = async ({ params, request }) => { +export const GET: RequestHandler = async ({ params, request, url }) => { try { const { sheetId } = params; const authHeader = request.headers.get('authorization'); - + const range = url.searchParams.get('range') || undefined; + if (!authHeader?.startsWith('Bearer ')) { return json({ error: 'Missing or invalid authorization header' }, { status: 401 }); } const refreshToken = authHeader.slice(7); - const sheetData = await googleSheetsServer.getSpreadsheetData(refreshToken, sheetId, 'A1:Z10'); + const sheetData = await googleSheetsServer.getSpreadsheetData(refreshToken, sheetId, range); return json(sheetData); } catch (error) { diff --git a/src/routes/private/events/event/new/+page.svelte b/src/routes/private/events/event/new/+page.svelte index dcaf5df..5642b85 100644 --- a/src/routes/private/events/event/new/+page.svelte +++ b/src/routes/private/events/event/new/+page.svelte @@ -1,6 +1,5 @@ @@ -455,16 +268,9 @@
{#if currentStep === 0} { - authData.error = null; - authData.token = token; - authData.isConnected = true; - setTimeout(checkGoogleAuth, 100); - }} - onError={(error) => { - authData.error = error; - authData.isConnected = false; - }} + onSuccess={() => (isGoogleConnected = true)} + onDisconnect={() => (isGoogleConnected = false)} + onError={(err) => toast.error(err)} /> {:else if currentStep === 1} @@ -485,7 +291,7 @@ void; onError?: (error: string) => void; + onDisconnect?: () => void; }>(); @@ -15,11 +16,12 @@ To create events and import participants from Google Sheets, you need to connect your Google account.

- +
diff --git a/src/routes/private/events/event/view/+page.svelte b/src/routes/private/events/event/view/+page.svelte index 482450c..e1578ac 100644 --- a/src/routes/private/events/event/view/+page.svelte +++ b/src/routes/private/events/event/view/+page.svelte @@ -115,6 +115,8 @@ } syncingParticipants = true; + const previousCount = participants.length; // Capture count before sync + try { // Fetch sheet data const response = await fetch(`/private/api/google/sheets/${event.sheet_id}/data`, { @@ -177,16 +179,23 @@ // Reload participants await loadParticipants(); - - // Show success message with count of synced participants - const previousCount = participants.length; - const newCount = names.length; - const addedCount = Math.max(0, participants.length - previousCount); - - toast.success( - `Successfully synced participants. ${newCount} entries processed, ${addedCount} new participants added.`, - 5000 - ); + + // Show success message with accurate count of changes + const newCount = participants.length; + const diff = newCount - previousCount; + const processedCount = names.length; + + let message = `Sync complete. ${processedCount} confirmed entries processed from the sheet.`; + + if (diff > 0) { + message += ` ${diff} new participants added.`; + } else if (diff < 0) { + message += ` ${-diff} participants removed.`; + } else { + message += ` No changes to the participant list.`; + } + + toast.success(message, 6000); } catch (err) { console.error('Error syncing participants:', err); toast.error(`Failed to sync participants: ${err instanceof Error ? err.message : 'Unknown error'}`);