Search code examples
node.jsjsonoauth-2.0spotifyaccess-token

Nodejs: How to read and traverse ServerResponse? (or, How to read client URL parameters?)


User Flow:

  • User makes a post request to the server.
  • The request redirects the user to Spotify's authorization endpoint, where he grants certain access to our web application.
  • The user is redirected back to our website, and the URL in his address bar has now changed from http://localhost:3000 to http://localhost:3000/?code={code}

We need to access this code.

My current approach is to console log the response returned.

This is what the console logs:

https://docs.google.com/document/d/1FII3_xrjb6lmTkma20TDjoyRXN_yKZ-knmFgFyfALKc/edit?usp=sharing

Same console log, without numbering:

https://docs.google.com/document/d/12qeVkp82opa2ITk0wGs8Fv0_Zej38OzUkJGRh8TzFfE/edit?usp=sharing

Scroll to line 1259, you would find this:

params: {},
 query:
      { code:
         'AQC_G...nzDFS'
},

How can we access the value of this 'code'?

Parsing it to JSON online (hoping to get the path to 'query'), it says, "Failed to parse invalid JSON format."

Also, this is the app.js:

const express = require("express");
const https = require("https");
const bodyParser = require("body-parser");
const axios = require("axios");

const app = express();

app.listen(3000, function() {
})

// The page to load when the browser (client) makes request to GET something from the server on "/", i.e., from the homepage.
app.get("/", function(req, res) {
  res.sendFile(__dirname + "/index.html");
  console.log(res); // This is logging that long ServerResponse.
});

let authURL = "https://accounts.spotify.com/authorize?client_id={client_id}&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&scope=user-read-playback-state%20app-remote-control%20user-modify-playback-state%20user-read-currently-playing%20user-read-playback-position%20user-read-email%20streaming"

// Redirect user to Spotify's endpoint.
app.post("/", function(req, res) {
  res.redirect(authURL);
});

// The data that server should POST when the POST request is sent by the client, upon entering the search queryValue, in the search bar (form).
app.post("/", function(req, res) {

  // The user input query. We are using body-parser package here.
  const query = req.body.queryValue;

  let searchUrl = "https://api.spotify.com/v1/search?q=" + query + "&type=track%2Calbum%2Cartist&limit=4&market=IN";

  //Using Axios to fetch data. It gets parsed to JSON automatically.
  axios.get(searchUrl, {
      headers: {
        'Authorization': token,
      }
    })
    .then((resAxios) => {
      console.log(resAxios.data)



      //Extracting required data.



    })
    .catch((error) => {
      console.error(error)
    })
});

This is the index.js:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Title</title>
    <link rel="stylesheet" href="index.css">
  </head>
  <body>

    <div class="index-search-container">
      <form class="" action="/" method="post">
        <input id="queryId" type="text" name="queryValue" value="" placeholder="Search">
        <button type="submit" name="button">HIT ME</button>
      </form>
    </div>

  </body>
</html>

Notice the 'POST' method 'form' in index.js. This form is actually used to send queryValue to the server, to be returned with some data fetched from API. In app.js, there are two app.post now, and only the first one works. Please help me improve the code. :)


Solution

  • Here is an example you can use to ask for a user permissions to contact Spotify's API on their behalf:

    const express      = require("express")
    const bodyParser   = require("body-parser")
    const cookieParser = require("cookie-parser")
    const axios        = require("axios")
    const crypto       = require("crypto")
    /**
     * Environment Variables
     */
    const PORT       = process.env.PORT       || 3000
    const PROTOCOL   = process.env.PROTOCOL   || "http"
    const URL        = process.env.URL        || `${PROTOCOL}://localhost:${PORT}`
    const SEARCH_URL = process.env.SEARCH_URL || "https://api.spotify.com/v1/search"
    const AUTH_URL   = process.env.AUTH_URL   || "https://accounts.spotify.com/authorize"
    const TOKEN_URL  = process.env.TOKEN_URL  || "https://accounts.spotify.com/api/token"
    const SCOPE      = process.env.SCOPE      || "user-read-playback-state app-remote-control user-modify-playback-state user-read-currently-playing user-read-playback-position user-read-email streaming"
    /**
     * Spotify's Client Secrets
     */
    const CLIENT_ID     = process.env.CLIENT_ID
    const CLIENT_SECRET = process.env.CLIENT_SECRET
    /**
     * Memory Store
     */
    const DB = {}
    /**
     * Express App Configuration
     */
    const app = express()
    app.use(bodyParser.json())
    app.use(cookieParser())
    app.use((_, res, next) => {
      res.header('Access-Control-Allow-Origin', "*");
      res.header('Access-Control-Allow-Headers', "*");
      next()
    })
    // Home
    app.get("/", (req, res) => {
      res.status(200)
        .header("Content-Type", "text/html")
        .send(`
    <!doctype html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Spotify Example</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
    <form id="searchForm">
      <input id="queryId" type="text" name="query" value="" placeholder="Search">
      <button type="submit" name="button">HIT ME</button>
    </form>
    <pre id="results">
    <script>
    const form$    = document.getElementById("searchForm")
    const results$ = document.getElementById("results")
    form$.addEventListener("submit", (e) => {
      e.preventDefault()
      fetch("/", {
        method: "POST",
        credentials: "same-origin",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({query: e.target.elements.query.value})
      })
        .then((response) => {
          if (response.status === 301) {
            window.location.href = "/login"
            return
          }
          return response.json()
        })
        .then((data) => results$.textContent = JSON.stringify(data, null, 2))
        .catch((err) => console.error(err))
    })
    </script>
    </body>
    </html>
        `)
    })
    // Search
    app.post("/", (req, res) => {
      const id = req.cookies.id
      if (id === undefined || DB[id] === undefined) {
        res.status(301).send()
        return
      }
      axios({
        url: `${SEARCH_URL}?q=${encodeURIComponent(req.body.query)}&type=track%2Calbum%2Cartist&limit=4&market=IN`,
        method: "get",
        headers: {"Authorization": `Bearer ${DB[id].access_token}`},
      })
        .then((response) => {
          res.status(200).json(response.data)
        })
        .catch((err) => {
          console.error(err)
          res.status(400).send(err.message)
        })
    })
    // Login
    app.get("/login", (_, res) => {
      res.redirect(`${AUTH_URL}?client_id=${CLIENT_ID}&scope=${encodeURIComponent(SCOPE)}&response_type=code&redirect_uri=${encodeURIComponent(URL + '/callback')}`)
    })
    // Callback
    app.get("/callback", (req, res) => {
      const id   = crypto.randomBytes(9).toString("hex")
      const code = req.query.code
      axios({
        url: TOKEN_URL,
        method: "post",
        data: Object.entries({grant_type: "client_credentials", code, redirect_uri: URL})
          .map(([key, value]) => key + "=" + encodeURIComponent(value)).join("&"),
        headers: {
          "Accept": "application/json",
          "Content-Type": "application/x-www-form-urlencoded",
        },
        auth: {username: CLIENT_ID, password: CLIENT_SECRET},
      })
        .then((response) => {
          DB[id] = response.data
          res.cookie("id", id, {maxAge: response.data.expires_in * 1000, httpOnly: true, path: "/"}).redirect("/")
        })
        .catch((err) => {
          console.error(err)
          res.status(400).send(err.message)
        })
    })
    /**
     * Listen
     */
    app.listen(PORT, () => console.log(`- Listening on port ${PORT}`))
    

    You need to provide your CLIENT_ID, CLIENT_SECRET, and the redirect URL as environment variables. I suggest calling the index.js script like this:

    env CLIENT_ID=xxx \
    env CLIENT_SECRET=zzz \
    env URL=http://spotify.127.0.0.1.nip.io:3000 \
    node index.js
    

    The procedure is as follows:

    1. A user sends a query through the form. Then, the server checks to see if it has given the app permissions to use Spotify's API. If not, it sends a redirect message.
    2. The user authenticates with Spotify's credentials.
    3. Spotify redirects the client to the site reference by the redirect_uri parameter.
    4. We grab the code provided by Spotify, and we exchange it for the user's credentials by providing our CLIENT_ID and SECRET_ID.
    5. We store those credentials in memory.
    6. We redirect the client to the main site.

    The next time the user runs a query, we'll have valid credentials, so we can query Spotify's API and return the results.