diff --git a/.gitignore b/.gitignore index e15d412..f01129e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ .env.development.local .env.test.local .env.production.local +/config npm-debug.log* yarn-debug.log* diff --git a/package-lock.json b/package-lock.json index 4c03004..04e3ea5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3801,6 +3801,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -3848,6 +3864,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", diff --git a/package.json b/package.json index b53520e..5a60eca 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,14 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.3", "@testing-library/user-event": "^7.1.2", + "cookie-parser": "^1.4.4", + "cors": "^2.8.5", + "express": "^4.17.1", + "querystring": "^0.2.0", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-scripts": "3.3.0" + "react-scripts": "3.3.0", + "request": "^2.88.0" }, "scripts": { "start": "react-scripts start", diff --git a/server.js b/server.js new file mode 100644 index 0000000..61da32f --- /dev/null +++ b/server.js @@ -0,0 +1,149 @@ +/** + * This is an example of a basic node.js script that performs + * the Authorization Code oAuth2 flow to authenticate against + * the Spotify Accounts. + * + * For more information, read + * https://developer.spotify.com/web-api/authorization-guide/#authorization_code_flow + */ + +const express = require('express'); // Express web server framework +const request = require('request'); // "Request" library +const cors = require('cors'); +const querystring = require('querystring'); +const cookieParser = require('cookie-parser'); +const env = require('./config/config.json') + +const client_id = env.client_id +const client_secret = env.client_secret +const redirect_uri = env.redirect_uri + +/** + * Generates a random string containing numbers and letters + * @param {number} length The length of the string + * @return {string} The generated string + */ +var generateRandomString = function(length) { + var text = ''; + var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (var i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +}; + +var stateKey = 'spotify_auth_state'; + +var app = express(); + +app.use(express.static(__dirname + '/public')) + .use(cors()) + .use(cookieParser()); + +app.get('/login', function(req, res) { + console.log('Login') + var state = generateRandomString(16); + res.cookie(stateKey, state); + + // your application requests authorization + var scope = 'user-read-private user-read-email'; + + res.redirect('https://accounts.spotify.com/authorize?' + + querystring.stringify({ + response_type: 'code', + client_id: client_id, + scope: scope, + redirect_uri: redirect_uri, + state: state + })); +}); + +app.get('/callback', function(req, res) { + + // your application requests refresh and access tokens + // after checking the state parameter + + var code = req.query.code || null; + var state = req.query.state || null; + var storedState = req.cookies ? req.cookies[stateKey] : null; + + if (state === null || state !== storedState) { + res.redirect('http://localhost:3000/#' + + querystring.stringify({ + error: 'state_mismatch' + })); + } else { + res.clearCookie(stateKey); + var authOptions = { + url: 'https://accounts.spotify.com/api/token', + form: { + code: code, + redirect_uri: redirect_uri, + grant_type: 'authorization_code' + }, + headers: { + 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) + }, + json: true + }; + + request.post(authOptions, function(error, response, body) { + if (!error && response.statusCode === 200) { + + var access_token = body.access_token, + refresh_token = body.refresh_token; + + var options = { + url: 'https://api.spotify.com/v1/me', + headers: { 'Authorization': 'Bearer ' + access_token }, + json: true + }; + + // use the access token to access the Spotify Web API + request.get(options, function(error, response, body) { + console.log(body); + }); + + // we can also pass the token to the browser to make requests from there + res.redirect('http://localhost:3000?' + + querystring.stringify({ + access_token: access_token, + refresh_token: refresh_token + })); + } else { + res.redirect('http://localhost:3000/#' + + querystring.stringify({ + error: 'invalid_token' + })); + } + }); + } +}); + +app.get('/refresh_token', function(req, res) { + + // requesting access token from refresh token + var refresh_token = req.query.refresh_token; + var authOptions = { + url: 'https://accounts.spotify.com/api/token', + headers: { 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) }, + form: { + grant_type: 'refresh_token', + refresh_token: refresh_token + }, + json: true + }; + + request.post(authOptions, function(error, response, body) { + if (!error && response.statusCode === 200) { + var access_token = body.access_token; + res.send({ + 'access_token': access_token + }); + } + }); +}); + +console.log('Listening on 8888'); +app.listen(8888); diff --git a/src/App.js b/src/App.js index ce9cbd2..805806c 100644 --- a/src/App.js +++ b/src/App.js @@ -1,26 +1,31 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; +import React, { useState, useEffect } from 'react' +import logo from './logo.svg' +import './App.css' function App() { + let [token, setToken] = useState(null) + + useEffect(() => { + const search = window.location.search + const params = new URLSearchParams(search) + setToken(params.get('access_token')) + }, []) + return (
logo +

Edit src/App.js and save to reload.

- - Learn React - + + Login to Spotify + +

{token}

- ); + ) } -export default App; +export default App diff --git a/src/views/ArtistSearch.jsx b/src/views/ArtistSearch.jsx new file mode 100644 index 0000000..7fadf32 --- /dev/null +++ b/src/views/ArtistSearch.jsx @@ -0,0 +1,21 @@ +import React, { useState, useEffect } from 'react' + +function ArtistSearch() { + let [token, setToken] = useState(null) + + useEffect(() => { + const search = window.location.search + const params = new URLSearchParams(search) + setToken(params.get('access_token')) + }, []) + + return ( +
+

Artist search

+ +

{token}

+
+ ) +} + +export default ArtistSearch