callback.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. export const prerender = false;
  2. const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
  3. function requiredEnv(name: string): string {
  4. const v = import.meta.env[name];
  5. if (!v) throw new Error(`Missing env var: ${name}`);
  6. return v;
  7. }
  8. export async function GET({ request, url }: { request: Request; url: URL }) {
  9. try {
  10. const reqUrl = new URL(request.url);
  11. const code =
  12. reqUrl.searchParams.get("code") ?? url.searchParams.get("code");
  13. if (!code) {
  14. return new Response(
  15. [
  16. "Missing ?code",
  17. "",
  18. `request.url: ${request.url}`,
  19. `astro.url: ${url.toString()}`,
  20. ].join("\n"),
  21. {
  22. status: 400,
  23. headers: { "Content-Type": "text/plain; charset=utf-8" },
  24. },
  25. );
  26. }
  27. const client_id = requiredEnv("SPOTIFY_CLIENT_ID");
  28. const client_secret = requiredEnv("SPOTIFY_CLIENT_SECRET");
  29. const redirect_uri =
  30. import.meta.env.SPOTIFY_REDIRECT_URI ??
  31. `${reqUrl.origin}/api/spotify/callback`;
  32. const basic = Buffer.from(`${client_id}:${client_secret}`).toString(
  33. "base64",
  34. );
  35. const res = await fetch(TOKEN_ENDPOINT, {
  36. method: "POST",
  37. headers: {
  38. Authorization: `Basic ${basic}`,
  39. "Content-Type": "application/x-www-form-urlencoded",
  40. },
  41. body: new URLSearchParams({
  42. grant_type: "authorization_code",
  43. code,
  44. redirect_uri,
  45. }),
  46. });
  47. const text = await res.text();
  48. if (!res.ok) {
  49. return new Response(
  50. `Spotify token exchange failed (${res.status}).\n\n${text}`,
  51. {
  52. status: 500,
  53. headers: { "Content-Type": "text/plain; charset=utf-8" },
  54. },
  55. );
  56. }
  57. const data = JSON.parse(text) as {
  58. access_token?: string;
  59. refresh_token?: string;
  60. scope?: string;
  61. expires_in?: number;
  62. token_type?: string;
  63. };
  64. const refresh = data.refresh_token;
  65. if (!refresh) {
  66. return new Response(
  67. `No refresh_token returned.\n\nThis usually means you already authorized this app before.\nGo to https://www.spotify.com/account/apps/ and remove access, then try again.\n\nRaw response:\n${text}`,
  68. {
  69. status: 500,
  70. headers: { "Content-Type": "text/plain; charset=utf-8" },
  71. },
  72. );
  73. }
  74. return new Response(
  75. [
  76. "Refresh token generated successfully.",
  77. "",
  78. `SPOTIFY_REFRESH_TOKEN=${refresh}`,
  79. "",
  80. "Put that into your .env (server-side).",
  81. ].join("\n"),
  82. { status: 200, headers: { "Content-Type": "text/plain; charset=utf-8" } },
  83. );
  84. } catch (err) {
  85. const message = err instanceof Error ? err.message : String(err);
  86. return new Response(message, {
  87. status: 500,
  88. headers: { "Content-Type": "text/plain; charset=utf-8" },
  89. });
  90. }
  91. }