Security handening
This commit is contained in:
@@ -1,117 +1,164 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { writable, get } from 'svelte/store';
|
||||
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 isGoogleApiReady = writable(false); // To track GAPI client readiness
|
||||
|
||||
let tokenClient: google.accounts.oauth2.TokenClient;
|
||||
let gapiInited = false;
|
||||
let gsiInited = false;
|
||||
|
||||
const TOKEN_KEY = 'google_oauth_token';
|
||||
export function initGoogleClient(callback: () => void) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://apis.google.com/js/api.js';
|
||||
script.onload = () => {
|
||||
gapi.load('client', async () => {
|
||||
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);
|
||||
}
|
||||
// This function ensures both GAPI (for Sheets/Drive APIs) and GSI (for auth) are loaded in the correct order.
|
||||
export function initGoogleClients(callback: () => void) {
|
||||
// If everything is already initialized, just run the callback.
|
||||
if (gapiInited && gsiInited) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Disables automatic sign-in on the next page load.
|
||||
google.accounts.id.disableAutoSelect();
|
||||
// 1. Load GAPI script for Sheets/Drive APIs first.
|
||||
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
|
||||
gapi.client.setToken(null);
|
||||
// 2. Load GSI script for Auth. This should only be called after GAPI is ready.
|
||||
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);
|
||||
|
||||
console.log('User signed out.');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
const response = await gapi.client.drive.files.list({
|
||||
@@ -123,7 +170,8 @@ export async function searchSheets(query: 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');
|
||||
}
|
||||
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) {
|
||||
if (!gapi.client.sheets) {
|
||||
await ensureToken();
|
||||
if (!gapi.client || !gapi.client.sheets) {
|
||||
throw new Error('Google Sheets API not loaded');
|
||||
}
|
||||
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
|
||||
export async function downloadDriveImage(url: string): Promise<Blob> {
|
||||
await ensureToken();
|
||||
const fileId = extractDriveFileId(url);
|
||||
|
||||
if (!fileId) {
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user