146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
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",
|
|
},
|
|
});
|
|
}
|
|
},
|
|
},
|
|
},
|
|
});
|