import { createFileRoute } from "@tanstack/react-router"; const SPOTIFY_CLIENT_ID = process.env.VITE_SPOTIFY_CLIENT_ID!; const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!; const SPOTIFY_REDIRECT_URI = process.env.VITE_SPOTIFY_REDIRECT_URI!; export const Route = createFileRoute("/api/spotify/callback")({ server: { handlers: { GET: async ({ request }: { request: Request }) => { const getReturnPath = (state: string | null): string => { if (!state) return "/"; try { const decodedState = JSON.parse(atob(state)); return decodedState.returnPath || "/"; } catch { return "/"; } }; try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const error = url.searchParams.get("error"); const returnPath = getReturnPath(state); if (error) { console.error("Spotify OAuth error:", error); return new Response(null, { status: 302, headers: { Location: returnPath + "?spotify_error=" + encodeURIComponent(error), }, }); } if (!code || !state) { console.error("Missing code or state:", { code: !!code, state: !!state, }); return new Response(null, { status: 302, headers: { Location: returnPath + "?spotify_error=missing_code_or_state", }, }); } console.log("Token exchange attempt:", { client_id: SPOTIFY_CLIENT_ID, redirect_uri: SPOTIFY_REDIRECT_URI, has_code: !!code, has_state: !!state, }); const tokenResponse = await fetch( "https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64")}`, }, body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: SPOTIFY_REDIRECT_URI, }), } ); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error("Token exchange error:", { status: tokenResponse.status, statusText: tokenResponse.statusText, body: errorText, redirect_uri: SPOTIFY_REDIRECT_URI, }); const errorParam = encodeURIComponent( `${tokenResponse.status}: ${errorText}` ); return new Response(null, { status: 302, headers: { Location: `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`, }, }); } const tokens = await tokenResponse.json(); console.log("Token exchange successful:", { has_access_token: !!tokens.access_token, has_refresh_token: !!tokens.refresh_token, expires_in: tokens.expires_in, }); console.log("Decoded return path:", returnPath); const response = new Response(null, { status: 302, headers: { Location: returnPath + "?spotify_auth=success", }, }); const isSecure = import.meta.env.NODE_ENV === "production"; const cookieOptions = `HttpOnly; ${isSecure ? 'Secure; ' : ''}SameSite=Lax; Path=/; Max-Age=${tokens.expires_in}`; response.headers.append( "Set-Cookie", `spotify_access_token=${tokens.access_token}; ${cookieOptions}` ); if (tokens.refresh_token) { const refreshCookieOptions = `HttpOnly; ${isSecure ? 'Secure; ' : ''}SameSite=Lax; Path=/; Max-Age=${60 * 60 * 24 * 30}`; // 30 days response.headers.append( "Set-Cookie", `spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}` ); } return response; } catch (error) { console.error("Spotify callback error:", error); const url = new URL(request.url); const state = url.searchParams.get("state"); const returnPath = getReturnPath(state); return new Response(null, { status: 302, headers: { Location: returnPath + "?spotify_error=callback_failed", }, }); } }, }, }, });