import { error, redirect } from '@sveltejs/kit'; import { client } from '#lib/server/db/index.js'; import { requireOwner } from '#lib/server/owner.js'; import { exchangeCode, spotifyGet } from '#lib/music/collector.server.js'; import { CALLBACK_PATH, publicOrigin, safeEqual, STATE_COOKIE } from '#lib/server/music/oauth.js'; import { pollNow, spotifyCredentials } from '#lib/server/music/poller.js'; import type { RequestHandler } from './$types'; // Spotify redirects the owner here after /api/spotify/connect. Stores the refresh token in // music.spotify_auth (app role only) and kicks off the first poll. export const GET: RequestHandler = async (event) => { requireOwner(event); const { url, cookies, fetch } = event; const expected = cookies.get(STATE_COOKIE) ?? ''; cookies.delete(STATE_COOKIE, { path: '/api/spotify' }); const state = url.searchParams.get('state') ?? ''; if (!expected || !safeEqual(state, expected)) error(400, 'Invalid or expired state, try again.'); if (url.searchParams.get('error')) redirect(303, `/trackers/music?spotify=${encodeURIComponent(url.searchParams.get('error')!)}`); const code = url.searchParams.get('code'); if (!code) error(400, 'Missing code'); const creds = spotifyCredentials(); if (!creds.clientId || !creds.clientSecret) error(503, 'Spotify is not configured on this server.'); const tokens = await exchangeCode(fetch, creds, code, `${publicOrigin(url)}${CALLBACK_PATH}`); if (!tokens.refresh_token) error(502, 'Spotify did not return a refresh token.'); const me = (await spotifyGet(fetch, tokens.access_token, 'me')) as Record | null; const userId = typeof me?.id === 'string' ? me.id : 'unknown'; const name = typeof me?.display_name === 'string' ? me.display_name : null; await client` insert into music.spotify_auth (id, spotify_user_id, display_name, refresh_token, access_token, access_expires_at, scope, needs_reauth, last_error, connected_at, updated_at) values (1, ${userId}, ${name}, ${tokens.refresh_token}, ${tokens.access_token}, now() + make_interval(secs => ${tokens.expires_in}), ${tokens.scope ?? null}, false, null, now(), now()) on conflict (id) do update set spotify_user_id = excluded.spotify_user_id, display_name = excluded.display_name, refresh_token = excluded.refresh_token, access_token = excluded.access_token, access_expires_at = excluded.access_expires_at, scope = excluded.scope, needs_reauth = false, last_error = null, lease_until = null, -- another account starts a fresh recently-played cursor recent_cursor = case when music.spotify_auth.spotify_user_id = excluded.spotify_user_id then music.spotify_auth.recent_cursor end, connected_at = now(), updated_at = now()`; void pollNow(); redirect(303, '/trackers/music?spotify=connected'); };