feat(#89): Add backup functionality for google drive and dropbox

This commit is contained in:
Josh Creek
2026-01-24 13:32:10 +00:00
parent 3f5c14a8bc
commit ab82f75dc6
22 changed files with 1582 additions and 7 deletions
@@ -0,0 +1,115 @@
import { json, redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import PokedexExportIntegrationRepository from '$lib/repositories/PokedexExportIntegrationRepository';
import { requireAuth } from '$lib/utils/auth';
import { clearOAuthStateCookie, readOAuthStateCookie } from '$lib/utils/oauthState';
import { getEnv } from '$lib/utils/env';
export const GET = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const url = new URL(event.request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const errorParam = url.searchParams.get('error');
const oauthState = readOAuthStateCookie(event, 'dropbox');
if (!oauthState || !state || oauthState.state !== state || oauthState.userId !== userId) {
clearOAuthStateCookie(event, 'dropbox');
return json({ error: 'Invalid OAuth state' }, { status: 400 });
}
clearOAuthStateCookie(event, 'dropbox');
const fallbackReturnTo = oauthState.pokedexId
? `/pokedex/${oauthState.pokedexId}`
: '/backup-settings';
const baseReturnTo =
oauthState.returnTo && oauthState.returnTo.startsWith('/')
? oauthState.returnTo
: fallbackReturnTo;
const returnTo = baseReturnTo.includes('?')
? `${baseReturnTo}&export=dropbox`
: `${baseReturnTo}?export=dropbox`;
if (errorParam) {
throw redirect(302, `${returnTo}-denied`);
}
if (!code) {
return json({ error: 'Missing OAuth code' }, { status: 400 });
}
const env = getEnv();
const clientId = env.DROPBOX_OAUTH_CLIENT_ID;
const clientSecret = env.DROPBOX_OAUTH_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return json({ error: 'Missing Dropbox OAuth credentials' }, { status: 500 });
}
const redirectUri = `${event.url.origin}/api/integrations/dropbox/callback`;
const tokenParams = new URLSearchParams({
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
grant_type: 'authorization_code'
});
const tokenResponse = await fetch('https://api.dropbox.com/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenParams.toString()
});
if (!tokenResponse.ok) {
const text = await tokenResponse.text();
return json({ error: `Dropbox token exchange failed: ${text}` }, { status: 500 });
}
const tokenData = (await tokenResponse.json()) as {
access_token: string;
expires_in?: number;
refresh_token?: string;
scope?: string;
};
const expiresAt = tokenData.expires_in
? new Date(Date.now() + tokenData.expires_in * 1000).toISOString()
: null;
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
const repo = new PokedexExportIntegrationRepository(event.locals.supabase, userId, null);
const fileName = oauthState.fileName?.trim() ? oauthState.fileName : null;
const path = oauthState.path?.trim() ? oauthState.path : null;
try {
await repo.upsert({
provider: 'dropbox',
enabled: true,
fileName,
path,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token ?? null,
accessTokenExpiresAt: expiresAt,
metadata: tokenData.scope ? { scope: tokenData.scope } : null
});
} catch (saveError) {
console.error('Dropbox integration save failed:', saveError);
throw redirect(302, `${returnTo}-error`);
}
throw redirect(302, `${returnTo}-connected`);
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};