Búsqueda de sitios web

node.js - Implementar el flujo de autorización de Spotify usando las rutas de API de NextJS genera error de CORS


Estoy tratando de crear el flujo de inicio de sesión en NextJS como se menciona aquí: Tutorial de la API del SDK de Spotify, usando las rutas api de NextJS

He creado dos manejadores: api/login.ts y api/callback.ts.

login.ts maneja la construcción de los parámetros requeridos para llamar al endpoint de autorización de Spotify. callback.ts maneja la URL redirigida desde el servidor de autorización. Resuelve la solicitud y devuelve los datos.

Cuando visito la ruta: localhost:3000/api/login se resuelve correctamente con la respuesta y me envía el access_token y el refresh_token requeridos en la respuesta. La respuesta aparece como JSON en el navegador.

Ahora surge el problema cuando llamo a este endpoint dentro de un componente. Allí consulto el endpoint '/api/login' (ver: Login.tsx) pero lanza el siguiente error de cors:

Access to fetch at 'https://accounts.spotify.com/authorize/?response_type=code&client_id=woops&scope=streaming+user-read-email+user-read-private&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fcallback&state=woops' (redirected from 'http://localhost:3000/api/login') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

login.ts:

import type { NextApiRequest, NextApiResponse } from 'next'

const generateRandomString = (length: number): string => {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

  for (var i = 0; i < length; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
};

const scope = "streaming \
user-read-email \
user-read-private";

export default async function login(
  req: NextApiRequest,
  res: NextApiResponse
) {
  
  const state = generateRandomString(16);

  const input = {
    response_type: "code",
    client_id: process.env.SPOTIFY_CLIENT_ID,
    scope,
    redirect_uri: "http://localhost:3000",
    state,
  } as Record<string, string>;

  const auth_query_parameters = new URLSearchParams(input)
  res.redirect(`https://accounts.spotify.com/authorize/?${auth_query_parameters.toString()}`);
}

callback.ts:

import type { NextApiRequest, NextApiResponse } from 'next'

const formBody = (details: Record<string, string>): string => {
  return Object.keys(details)
    .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(details[key]))
    .join('&')
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { code } = req.query;
  
  const spotifyURL = 'https://accounts.spotify.com/api/token';

  const formData = {
    code: code,
    redirect_uri: "http://localhost:3000/api/callback",
    grant_type: 'authorization_code'
  } as Record<string, string>;

  const config = {
    method: 'POST',
    headers: {
      'Authorization': 'Basic ' + (Buffer.from(process.env.SPOTIFY_CLIENT_ID + ':' + process.env.SPOTIFY_CLIENT_SECRET).toString('base64')),
      'Content-Type' : 'application/x-www-form-urlencoded'
    },
    body: formBody(formData),
    JSON: true
  };

  fetch(spotifyURL, config as RequestInit)
    .then(response => response.json())
    .then(data => {
      res
        .send(data)
    })
}

Login.tsx

import { useEffect, useState } from "react";
import Login from '../components/Login';
import WebPlayback from '../components/WebPlayback';

const useLogin = () => {
  const [token, setToken] = useState('');
  
  useEffect(() => {
    async function getToken() {
      const response = await fetch('/api/login', { mode: 'cors' });
      const json = await response.json();
      setToken(json.access_token)
    }
    getToken();
  }, []);

  return token;
}

const App = () => {
  const token = useLogin();
  return (
    <>
      {
        token === ''
        ? <Login />
        : <WebPlayback token={token} />
      }
    </>
  )
}

export default App;

Respuesta #1

establece tu modo en no-cors

const response = await fetch('/api/login', { mode: 'no-cors' });