Security handening
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { currentStep } from '$lib/stores.js';
|
import { currentStep } from '$lib/stores.js';
|
||||||
import { isSignedIn, handleSignIn, handleSignOut, isGoogleApiReady } from '$lib/google';
|
import { isSignedIn, handleSignOut, requestTokenFromUser } from '$lib/google';
|
||||||
import Navigator from './subcomponents/Navigator.svelte';
|
import Navigator from './subcomponents/Navigator.svelte';
|
||||||
|
|
||||||
|
function handleSignIn() {
|
||||||
|
requestTokenFromUser();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
@@ -79,34 +83,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onclick={handleSignIn}
|
onclick={handleSignIn}
|
||||||
disabled={!$isGoogleApiReady}
|
|
||||||
class="flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
class="flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
||||||
>
|
>
|
||||||
{#if $isGoogleApiReady}
|
Sign In with Google
|
||||||
Sign In with Google
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
class="mr-2 h-5 w-5 animate-spin text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
class="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
class="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
Loading Google API...
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { selectedSheet, currentStep, columnMapping } from '$lib/stores';
|
import { selectedSheet, currentStep, columnMapping } from '$lib/stores';
|
||||||
import type { ColumnMappingType, SheetInfoType } from '$lib/stores';
|
import type { ColumnMappingType, SheetInfoType } from '$lib/stores';
|
||||||
import { getSheetNames, getSheetData } from '$lib/google';
|
import { getSheetNames, getSheetData, ensureToken } from '$lib/google';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Navigator from './subcomponents/Navigator.svelte';
|
import Navigator from './subcomponents/Navigator.svelte';
|
||||||
|
|
||||||
@@ -35,13 +35,16 @@
|
|||||||
{ key: 'alreadyPrinted', label: 'Already Printed', required: false }
|
{ key: 'alreadyPrinted', label: 'Already Printed', required: false }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const RECENT_SHEETS_KEY = 'recentSheets';
|
||||||
|
|
||||||
// Load available sheets when component mounts
|
// Load available sheets when component mounts
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
ensureToken();
|
||||||
if ($selectedSheet) {
|
if ($selectedSheet) {
|
||||||
console.log('Selected sheet on mount:', $selectedSheet);
|
console.log('Selected sheet on mount:', $selectedSheet);
|
||||||
|
|
||||||
// Check if we already have saved mapping data
|
// Check if we already have saved mapping data
|
||||||
const recentSheetsData = localStorage.getItem('recent-sheets');
|
const recentSheetsData = localStorage.getItem(RECENT_SHEETS_KEY);
|
||||||
|
|
||||||
if (recentSheetsData) {
|
if (recentSheetsData) {
|
||||||
try {
|
try {
|
||||||
@@ -245,8 +248,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const recentSheetsKey = 'recent-sheets';
|
const existingData = localStorage.getItem(RECENT_SHEETS_KEY);
|
||||||
const existingData = localStorage.getItem(recentSheetsKey);
|
|
||||||
|
|
||||||
if (existingData) {
|
if (existingData) {
|
||||||
const recentSheets = JSON.parse(existingData);
|
const recentSheets = JSON.parse(existingData);
|
||||||
@@ -331,8 +333,7 @@
|
|||||||
|
|
||||||
// Save column mapping to localStorage for the selected sheet
|
// Save column mapping to localStorage for the selected sheet
|
||||||
try {
|
try {
|
||||||
const recentSheetsKey = 'recent-sheets';
|
const existingData = localStorage.getItem(RECENT_SHEETS_KEY);
|
||||||
const existingData = localStorage.getItem(recentSheetsKey);
|
|
||||||
let recentSheets = existingData ? JSON.parse(existingData) : [];
|
let recentSheets = existingData ? JSON.parse(existingData) : [];
|
||||||
|
|
||||||
// Find the current sheet in recent sheets and update its column mapping
|
// Find the current sheet in recent sheets and update its column mapping
|
||||||
@@ -374,7 +375,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(recentSheetsKey, JSON.stringify(recentSheets));
|
localStorage.setItem(RECENT_SHEETS_KEY, JSON.stringify(recentSheets));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save column mapping to localStorage:', err);
|
console.error('Failed to save column mapping to localStorage:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { columnMapping, sheetData, currentStep, pictures, cropRects } from '$lib/stores';
|
import { columnMapping, sheetData, currentStep, pictures, cropRects } from '$lib/stores';
|
||||||
import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl } from '$lib/google';
|
import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl, ensureToken } from '$lib/google';
|
||||||
import Navigator from './subcomponents/Navigator.svelte';
|
import Navigator from './subcomponents/Navigator.svelte';
|
||||||
import PhotoCard from './subcomponents/PhotoCard.svelte';
|
import PhotoCard from './subcomponents/PhotoCard.svelte';
|
||||||
import * as tf from '@tensorflow/tfjs';
|
import * as tf from '@tensorflow/tfjs';
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
|
|
||||||
// Initialize detector and process photos
|
// Initialize detector and process photos
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
ensureToken();
|
||||||
initializeDetector(); // Start loading model
|
initializeDetector(); // Start loading model
|
||||||
if ($sheetData.length > 0 && $columnMapping.pictureUrl !== undefined) {
|
if ($sheetData.length > 0 && $columnMapping.pictureUrl !== undefined) {
|
||||||
console.log('Processing photos for gallery step');
|
console.log('Processing photos for gallery step');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
currentStep,
|
currentStep,
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import type { RowData } from '$lib/stores';
|
import type { RowData } from '$lib/stores';
|
||||||
import { getSheetData } from '$lib/google';
|
import { getSheetData, ensureToken } from '$lib/google';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import Navigator from './subcomponents/Navigator.svelte';
|
import Navigator from './subcomponents/Navigator.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
@@ -87,6 +87,7 @@
|
|||||||
}
|
}
|
||||||
} // Run on component mount
|
} // Run on component mount
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
ensureToken();
|
||||||
fetchAndProcessData();
|
fetchAndProcessData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { selectedSheet, currentStep } from '$lib/stores';
|
import { selectedSheet, currentStep } from '$lib/stores';
|
||||||
import type { SheetInfoType } from '$lib/stores';
|
import type { SheetInfoType } from '$lib/stores';
|
||||||
import { searchSheets } from '$lib/google';
|
import { searchSheets, ensureToken } from '$lib/google';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Navigator from './subcomponents/Navigator.svelte';
|
import Navigator from './subcomponents/Navigator.svelte';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
const RECENT_SHEETS_KEY = 'recent-sheets';
|
const RECENT_SHEETS_KEY = 'recent-sheets';
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
ensureToken();
|
||||||
loadRecentSheets();
|
loadRecentSheets();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,117 +1,164 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable, get } from 'svelte/store';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
export const isGoogleApiReady = writable(false);
|
// Store state: undefined = not yet known, null = failed/logged out, string = token
|
||||||
|
export const accessToken = writable<string | null | undefined>(undefined);
|
||||||
export const isSignedIn = writable(false);
|
export const isSignedIn = writable(false);
|
||||||
|
export const isGoogleApiReady = writable(false); // To track GAPI client readiness
|
||||||
|
|
||||||
let tokenClient: google.accounts.oauth2.TokenClient;
|
let tokenClient: google.accounts.oauth2.TokenClient;
|
||||||
|
let gapiInited = false;
|
||||||
|
let gsiInited = false;
|
||||||
|
|
||||||
const TOKEN_KEY = 'google_oauth_token';
|
// This function ensures both GAPI (for Sheets/Drive APIs) and GSI (for auth) are loaded in the correct order.
|
||||||
export function initGoogleClient(callback: () => void) {
|
export function initGoogleClients(callback: () => void) {
|
||||||
const script = document.createElement('script');
|
// If everything is already initialized, just run the callback.
|
||||||
script.src = 'https://apis.google.com/js/api.js';
|
if (gapiInited && gsiInited) {
|
||||||
script.onload = () => {
|
callback();
|
||||||
gapi.load('client', async () => {
|
return;
|
||||||
await gapi.client.init({
|
|
||||||
discoveryDocs: [
|
|
||||||
'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest',
|
|
||||||
'https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
isGoogleApiReady.set(true);
|
|
||||||
// Restore token from storage if available
|
|
||||||
const saved = localStorage.getItem(TOKEN_KEY);
|
|
||||||
if (saved) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(saved);
|
|
||||||
if (data.access_token && data.expires_at && data.expires_at > Date.now()) {
|
|
||||||
gapi.client.setToken({ access_token: data.access_token });
|
|
||||||
isSignedIn.set(true);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
document.body.appendChild(script);
|
|
||||||
|
|
||||||
const scriptGsi = document.createElement('script');
|
|
||||||
scriptGsi.src = 'https://accounts.google.com/gsi/client';
|
|
||||||
scriptGsi.onload = () => {
|
|
||||||
const clientId = env.PUBLIC_GOOGLE_CLIENT_ID;
|
|
||||||
if (!clientId) {
|
|
||||||
console.error('PUBLIC_GOOGLE_CLIENT_ID is not set in the environment.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tokenClient = google.accounts.oauth2.initTokenClient({
|
|
||||||
client_id: clientId,
|
|
||||||
scope: 'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets.readonly',
|
|
||||||
callback: (tokenResponse) => {
|
|
||||||
if (tokenResponse?.access_token) {
|
|
||||||
// Set token in gapi client
|
|
||||||
gapi.client.setToken({ access_token: tokenResponse.access_token });
|
|
||||||
isSignedIn.set(true);
|
|
||||||
// Persist token with expiration
|
|
||||||
const expiresInSeconds = tokenResponse.expires_in
|
|
||||||
? Number(tokenResponse.expires_in)
|
|
||||||
: 0;
|
|
||||||
const expiresInMs = expiresInSeconds * 1000;
|
|
||||||
const record = {
|
|
||||||
access_token: tokenResponse.access_token,
|
|
||||||
expires_at: expiresInMs ? Date.now() + expiresInMs : Date.now() + 3600 * 1000
|
|
||||||
};
|
|
||||||
localStorage.setItem(TOKEN_KEY, JSON.stringify(record));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
document.body.appendChild(scriptGsi);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleSignIn() {
|
|
||||||
if (gapi.client.getToken() === null) {
|
|
||||||
tokenClient.requestAccessToken({ prompt: 'consent' });
|
|
||||||
} else {
|
|
||||||
tokenClient.requestAccessToken({ prompt: '' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleSignOut() {
|
|
||||||
const savedToken = localStorage.getItem(TOKEN_KEY);
|
|
||||||
if (savedToken) {
|
|
||||||
try {
|
|
||||||
const tokenData = JSON.parse(savedToken);
|
|
||||||
if (tokenData.access_token) {
|
|
||||||
google.accounts.oauth2.revoke(tokenData.access_token, () => {
|
|
||||||
console.log('User token revoked.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing token from localStorage', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disables automatic sign-in on the next page load.
|
// 1. Load GAPI script for Sheets/Drive APIs first.
|
||||||
google.accounts.id.disableAutoSelect();
|
if (!gapiInited) {
|
||||||
|
const gapiScript = document.createElement('script');
|
||||||
|
gapiScript.src = 'https://apis.google.com/js/api.js';
|
||||||
|
gapiScript.async = true;
|
||||||
|
gapiScript.defer = true;
|
||||||
|
document.head.appendChild(gapiScript);
|
||||||
|
gapiScript.onload = () => {
|
||||||
|
gapi.load('client', () => {
|
||||||
|
gapi.client
|
||||||
|
.init({
|
||||||
|
discoveryDocs: [
|
||||||
|
'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest',
|
||||||
|
'https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
gapiInited = true;
|
||||||
|
isGoogleApiReady.set(true);
|
||||||
|
// Now that GAPI is ready, initialize the GSI client.
|
||||||
|
initGsiClient(callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// GAPI is already ready, just ensure GSI is initialized.
|
||||||
|
initGsiClient(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear gapi client token
|
// 2. Load GSI script for Auth. This should only be called after GAPI is ready.
|
||||||
gapi.client.setToken(null);
|
function initGsiClient(callback: () => void) {
|
||||||
|
if (gsiInited) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gsiScript = document.createElement('script');
|
||||||
|
gsiScript.src = 'https://accounts.google.com/gsi/client';
|
||||||
|
gsiScript.async = true;
|
||||||
|
gsiScript.defer = true;
|
||||||
|
document.head.appendChild(gsiScript);
|
||||||
|
gsiScript.onload = () => {
|
||||||
|
gsiInited = true;
|
||||||
|
tokenClient = google.accounts.oauth2.initTokenClient({
|
||||||
|
client_id: env.PUBLIC_GOOGLE_CLIENT_ID,
|
||||||
|
scope:
|
||||||
|
'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets.readonly',
|
||||||
|
callback: (tokenResponse) => {
|
||||||
|
// This callback handles responses from all token requests.
|
||||||
|
if (tokenResponse.error) {
|
||||||
|
console.error('Google token error:', tokenResponse.error);
|
||||||
|
accessToken.set(null);
|
||||||
|
isSignedIn.set(false);
|
||||||
|
if (gapiInited) gapi.client.setToken(null);
|
||||||
|
} else if (tokenResponse.access_token) {
|
||||||
|
const token = tokenResponse.access_token;
|
||||||
|
accessToken.set(token);
|
||||||
|
isSignedIn.set(true);
|
||||||
|
// Also set the token for the GAPI client
|
||||||
|
if (gapiInited) gapi.client.setToken({ access_token: token });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Clear token from localStorage
|
/**
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
* Tries to get a token silently.
|
||||||
|
* This is for background tasks and on-load checks.
|
||||||
|
* It will not show a consent prompt to the user.
|
||||||
|
*/
|
||||||
|
export function ensureToken(): Promise<string> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
initGoogleClients(() => {
|
||||||
|
const currentToken = get(accessToken);
|
||||||
|
// If we already have a valid token, resolve immediately.
|
||||||
|
if (currentToken) {
|
||||||
|
res(currentToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update signed in state
|
let unsubscribe: () => void;
|
||||||
|
unsubscribe = accessToken.subscribe((t) => {
|
||||||
|
// undefined means we are still waiting for the initial token request.
|
||||||
|
if (t) { // Got a token.
|
||||||
|
if (unsubscribe) unsubscribe();
|
||||||
|
res(t);
|
||||||
|
} else if (t === null) { // Got an explicit null, meaning auth failed.
|
||||||
|
if (unsubscribe) unsubscribe();
|
||||||
|
rej(new Error('Failed to retrieve access token. The user may need to sign in.'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no token, request one silently.
|
||||||
|
// The result is handled by the callback in initGsiClient, which updates the store and resolves the promise.
|
||||||
|
if (get(accessToken) === undefined) {
|
||||||
|
tokenClient.requestAccessToken({ prompt: '' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user for consent to grant a token.
|
||||||
|
* This should be called when a user clicks a "Sign In" button.
|
||||||
|
*/
|
||||||
|
export function requestTokenFromUser() {
|
||||||
|
initGoogleClients(() => {
|
||||||
|
if (tokenClient) {
|
||||||
|
tokenClient.requestAccessToken({ prompt: 'consent' });
|
||||||
|
} else {
|
||||||
|
console.error("requestTokenFromUser called before Google client was initialized.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the user out, revokes the token, and clears all local state.
|
||||||
|
*/
|
||||||
|
export function handleSignOut() {
|
||||||
|
const token = get(accessToken);
|
||||||
|
if (token && gsiInited) {
|
||||||
|
google.accounts.oauth2.revoke(token, () => {
|
||||||
|
console.log('User token revoked.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all tokens and states
|
||||||
|
if (gapiInited) {
|
||||||
|
gapi.client.setToken(null);
|
||||||
|
}
|
||||||
|
accessToken.set(null);
|
||||||
isSignedIn.set(false);
|
isSignedIn.set(false);
|
||||||
|
|
||||||
console.log('User signed out.');
|
console.log('User signed out.');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchSheets(query: string) {
|
export async function searchSheets(query: string) {
|
||||||
if (!gapi.client.drive) {
|
await ensureToken(); // Ensure we are authenticated before making a call
|
||||||
|
if (!gapi.client || !gapi.client.drive) {
|
||||||
throw new Error('Google Drive API not loaded');
|
throw new Error('Google Drive API not loaded');
|
||||||
}
|
}
|
||||||
const response = await gapi.client.drive.files.list({
|
const response = await gapi.client.drive.files.list({
|
||||||
@@ -123,7 +170,8 @@ export async function searchSheets(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSheetNames(spreadsheetId: string) {
|
export async function getSheetNames(spreadsheetId: string) {
|
||||||
if (!gapi.client.sheets) {
|
await ensureToken();
|
||||||
|
if (!gapi.client || !gapi.client.sheets) {
|
||||||
throw new Error('Google Sheets API not loaded');
|
throw new Error('Google Sheets API not loaded');
|
||||||
}
|
}
|
||||||
const response = await gapi.client.sheets.spreadsheets.get({
|
const response = await gapi.client.sheets.spreadsheets.get({
|
||||||
@@ -139,7 +187,8 @@ export async function getSheetNames(spreadsheetId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSheetData(spreadsheetId: string, range: string) {
|
export async function getSheetData(spreadsheetId: string, range: string) {
|
||||||
if (!gapi.client.sheets) {
|
await ensureToken();
|
||||||
|
if (!gapi.client || !gapi.client.sheets) {
|
||||||
throw new Error('Google Sheets API not loaded');
|
throw new Error('Google Sheets API not loaded');
|
||||||
}
|
}
|
||||||
const response = await gapi.client.sheets.spreadsheets.values.get({
|
const response = await gapi.client.sheets.spreadsheets.values.get({
|
||||||
@@ -178,13 +227,14 @@ export function isGoogleDriveUrl(url: string): boolean {
|
|||||||
|
|
||||||
// Download image from Google Drive using the API
|
// Download image from Google Drive using the API
|
||||||
export async function downloadDriveImage(url: string): Promise<Blob> {
|
export async function downloadDriveImage(url: string): Promise<Blob> {
|
||||||
|
await ensureToken();
|
||||||
const fileId = extractDriveFileId(url);
|
const fileId = extractDriveFileId(url);
|
||||||
|
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
throw new Error('Could not extract file ID from Google Drive URL');
|
throw new Error('Could not extract file ID from Google Drive URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gapi.client.drive) {
|
if (!gapi.client || !gapi.client.drive) {
|
||||||
throw new Error('Google Drive API not loaded');
|
throw new Error('Google Drive API not loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { initGoogleClient } from '$lib/google';
|
import { initGoogleClients } from '$lib/google';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
initGoogleClient(() => {
|
initGoogleClients(() => {
|
||||||
// You can add any logic here to run after the client is initialized
|
// You can add any logic here to run after the client is initialized
|
||||||
console.log('Google API client initialized');
|
console.log('Google API client initialized');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,23 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
// Consult https://svelte.dev/docs/kit/integrations
|
|
||||||
// for more information about preprocessors
|
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
kit: { adapter: adapter() }
|
kit: { adapter: adapter() },
|
||||||
|
csp: {
|
||||||
|
mode: 'hash',
|
||||||
|
directives: {
|
||||||
|
'default-src': ["'self'"],
|
||||||
|
'script-src': ["'self'"],
|
||||||
|
'style-src': ["'self'"],
|
||||||
|
'img-src': ["'self'", 'data:'],
|
||||||
|
'connect-src': ["'self'", 'https://www.googleapis.com'],
|
||||||
|
'font-src': ["'self'"],
|
||||||
|
'object-src': ["'none'"],
|
||||||
|
'frame-ancestors': ["'none'"],
|
||||||
|
'base-uri': ["'self'"],
|
||||||
|
'form-action': ["'self'"]
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user