Compare commits

...

2 Commits

Author SHA1 Message Date
Roman Krček
03eeef5c69 Updated google button 2025-09-02 19:16:52 +02:00
Roman Krček
fa685e6ba9 Added loading indicator 2025-09-02 18:21:18 +02:00
5 changed files with 100 additions and 223 deletions

View File

@@ -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 @@
};
</script>
{#if authState.isConnected}
{#if authState.checking}
<div class="flex items-center gap-3">
<div class="flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 border border-gray-300 whitespace-nowrap">
<div class="w-4 h-4 animate-spin rounded-full border-2 border-current border-t-transparent text-gray-600"></div>
<span class="text-sm font-medium text-gray-800">Checking connection...</span>
</div>
</div>
{:else if authState.isConnected}
<div class="flex flex-wrap items-center gap-3">
<div class="flex items-center gap-2 rounded-full bg-green-100 px-3 py-1 border border-green-300 whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-green-600" viewBox="0 0 20 20" fill="currentColor">

View File

@@ -30,7 +30,7 @@ export class GoogleAuthManager {
this.state = state;
}
checkConnection(): void {
async checkConnection(): Promise<void> {
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;
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;
}

View File

@@ -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<Response> {
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 });
}
}

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { isTokenValid, getUserInfo, revokeToken } from '$lib/google/auth/client.js';
import type { GoogleSheet } from '$lib/google/sheets/types.ts';
import { goto } from '$app/navigation';
import { toast } from '$lib/stores/toast.js';
@@ -16,19 +15,11 @@
let { data } = $props();
// Step management
let currentStep = $state(0); // Start at step 0 for Google auth check
const totalSteps = 4; // Increased to include auth step
let currentStep = $state(0);
const totalSteps = 4;
// Step 0: Google Auth
let authData = $state({
isConnected: false,
checking: true,
connecting: false,
showCancelOption: false,
token: null as string | null,
error: null as string | null,
userEmail: null as string | null
});
let isGoogleConnected = $state(false);
// Step 1: Event Details
let eventData = $state({
@@ -42,13 +33,13 @@
selectedSheet: null as GoogleSheet | null,
sheetData: [] as string[][],
columnMapping: {
name: 0, // Initialize to 0 (no column selected)
name: 0,
surname: 0,
email: 0,
confirmation: 0
},
loading: false,
expandedSheetList: true // Add this flag to control sheet list expansion
expandedSheetList: true
});
// Step 3: Email
@@ -62,189 +53,11 @@
let errors = $state<Record<string, string>>({});
onMount(async () => {
// Check Google auth status on mount
await checkGoogleAuth();
if (currentStep === 2) {
await loadRecentSheets();
}
});
// Google Auth functions
async function checkGoogleAuth() {
authData.checking = true;
try {
const accessToken = localStorage.getItem('google_access_token');
const refreshToken = localStorage.getItem('google_refresh_token');
if (accessToken && refreshToken) {
// Check if token is still valid
const isValid = await isTokenValid(accessToken);
authData.isConnected = isValid;
authData.token = accessToken;
if (isValid) {
// Fetch user info
await fetchUserInfo(accessToken);
}
} else {
authData.isConnected = false;
authData.userEmail = null;
}
} catch (error) {
console.error('Error checking Google auth:', error);
authData.isConnected = false;
authData.error = 'Error checking Google connection';
authData.userEmail = null;
} finally {
authData.checking = false;
}
}
async function connectToGoogle() {
authData.error = '';
authData.connecting = true;
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) {
authData.error = 'Failed to open popup window. Please allow popups for this site.';
authData.connecting = false;
return;
}
let authCompleted = false;
let popupTimer: number | null = null;
let cancelTimeout: 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;
authData.connecting = false;
authData.showCancelOption = false;
// Clean up timers
clearInterval(pollInterval);
if (popupTimer) clearTimeout(popupTimer);
if (cancelTimeout) clearTimeout(cancelTimeout);
// Update auth state
setTimeout(checkGoogleAuth, 100);
}
// Clean up function to handle all cleanup in one place
const cleanUp = () => {
clearInterval(pollInterval);
if (popupTimer) clearTimeout(popupTimer);
if (cancelTimeout) clearTimeout(cancelTimeout);
authData.connecting = false;
};
// Set a timeout for initial auth check
popupTimer = setTimeout(() => {
// Only check if auth isn't already completed
if (!authCompleted) {
cleanUp();
// Check if tokens were stored by the popup before it was closed
setTimeout(checkGoogleAuth, 100);
}
}, 30 * 1000) as unknown as number; // Reduced from 60s to 30s
// Show cancel option sooner
cancelTimeout = setTimeout(() => {
if (!authCompleted) {
authData.showCancelOption = true;
}
}, 10 * 1000) as unknown as number; // Reduced from 20s to 10s
// Final cleanup timeout
setTimeout(() => {
if (!authCompleted) {
cleanUp();
}
}, 60 * 1000); // Reduced from 3min to 1min
} catch (error) {
console.error('Error connecting to Google:', error);
authData.error = 'Failed to connect to Google';
authData.connecting = false;
}
}
function cancelGoogleAuth() {
authData.connecting = false;
authData.showCancelOption = false;
}
async function fetchUserInfo(accessToken: string) {
try {
// Use the new getUserInfo function from our lib
const userData = await getUserInfo(accessToken);
if (userData) {
authData.userEmail = userData.email;
} else {
authData.userEmail = null;
}
} catch (error) {
console.error('Error fetching user info:', error);
authData.userEmail = null;
}
}
async function disconnectGoogle() {
try {
// First revoke the token at Google using our API
const accessToken = localStorage.getItem('google_access_token');
if (accessToken) {
await revokeToken(accessToken);
}
// Remove tokens from local storage
localStorage.removeItem('google_access_token');
localStorage.removeItem('google_refresh_token');
// Update auth state
authData.isConnected = false;
authData.token = null;
authData.userEmail = null;
// Clear any selected sheets data
sheetsData.availableSheets = [];
sheetsData.selectedSheet = null;
sheetsData.sheetData = [];
} catch (error) {
console.error('Error disconnecting from Google:', error);
authData.error = 'Failed to disconnect from Google';
}
}
// Step navigation
function nextStep() {
if (validateCurrentStep()) {
@@ -265,7 +78,7 @@
let isValid = true;
if (currentStep === 0) {
if (!authData.isConnected) {
if (!isGoogleConnected) {
toast.error('Please connect your Google account to continue');
errors.auth = 'Please connect your Google account to continue';
return false;
@@ -437,13 +250,13 @@
// Computed values
let canProceed = $derived(() => {
if (currentStep === 0) return authData.isConnected;
if (currentStep === 1) return eventData.name && eventData.date;
if (currentStep === 0) return isGoogleConnected;
if (currentStep === 1) return !!(eventData.name && eventData.date);
if (currentStep === 2) {
const { name, surname, email, confirmation } = sheetsData.columnMapping;
return sheetsData.selectedSheet && name && surname && email && confirmation;
return !!(sheetsData.selectedSheet && name && surname && email && confirmation);
}
if (currentStep === 3) return emailData.subject && emailData.body;
if (currentStep === 3) return !!(emailData.subject && emailData.body);
return false;
});
</script>
@@ -455,16 +268,9 @@
<div class="mb-4 rounded border border-gray-300 bg-white p-6">
{#if currentStep === 0}
<GoogleAuthStep
onSuccess={(token) => {
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}
<EventDetailsStep bind:eventData />
@@ -485,7 +291,7 @@
<StepNavigation
{currentStep}
{totalSteps}
{canProceed}
canProceed={canProceed()}
{loading}
{prevStep}
{nextStep}

View File

@@ -2,9 +2,10 @@
import GoogleAuthButton from '$lib/components/GoogleAuthButton.svelte';
// Props
let { onSuccess, onError } = $props<{
let { onSuccess, onError, onDisconnect } = $props<{
onSuccess?: (token: string) => void;
onError?: (error: string) => void;
onDisconnect?: () => void;
}>();
</script>
@@ -18,8 +19,9 @@
<GoogleAuthButton
size="large"
variant="primary"
onSuccess={onSuccess}
onError={onError}
{onSuccess}
{onError}
{onDisconnect}
/>
</div>
</div>