From d56048455a2c4a0ff828845070c5d7dfde251b1c Mon Sep 17 00:00:00 2001 From: Alyssa Tadeo Date: Thu, 16 Nov 2023 00:06:41 -0800 Subject: [PATCH] working auth and current album art --- README.md | 2 +- app.js | 173 ++++++++++++++---------- helpers.js | 18 +++ package-lock.json | 41 ++++++ package.json | 1 + {public => static}/images/COIN.jpg | Bin {public => static}/images/HAIM.jpg | Bin views/dashboard.liquid | 9 ++ views/home.liquid | 5 + public/index.html => views/index.liquid | 6 +- 10 files changed, 179 insertions(+), 76 deletions(-) create mode 100644 helpers.js rename {public => static}/images/COIN.jpg (100%) rename {public => static}/images/HAIM.jpg (100%) create mode 100644 views/dashboard.liquid create mode 100644 views/home.liquid rename public/index.html => views/index.liquid (87%) diff --git a/README.md b/README.md index 39418c5..a26de4a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ SPOTIFY_REDIRECT_URI /* http://[HOSTNAME]:[PORT]/callback/ */ Note: HOSTNAME should not contain "http://" -3. `pm2 start app.js` +3. `npm start app.js` ## Running the App 1. Open `[HOSTNAME]:[PORT]` in a web browser diff --git a/app.js b/app.js index 5e187fc..f118e97 100644 --- a/app.js +++ b/app.js @@ -1,70 +1,99 @@ -const { assert } = require("console"); +// const { assert, log } = require("console"); const express = require("express"); const http = require("http"); const { Server } = require("socket.io"); const dotenv = require("dotenv") -var cookieParser = require('cookie-parser'); -var request = require('request'); -const { access } = require("fs"); - -// https://stackoverflow.com/questions/68329418/in-javascript-how-can-i-throw-an-error-if-an-environmental-variable-is-missing -function getEnv(name){ - let val = process.env[name]; - if ((val === undefined) || (val === null)) { - throw ("Spotipi: Error: Missing "+ name +" in ./.env"); - } - return val; -} +const cookieParser = require('cookie-parser'); +const request = require('request'); +// const { access } = require("fs"); +const { Liquid } = require('liquidjs'); +const { generateRandomString, getEnv } = require("./helpers"); /* dotenv setup */ const result = dotenv.config(); -if (result.error){ +if (result.error) { console.log("Spotipi: Error: dotenv not configured correctly.") throw result.error; } -const port = getEnv("PORT"); -const hostname = getEnv("HOSTNAME"); +const port = 8888; +const hostname = "localhost"; const client_id = getEnv("SPOTIFY_CLIENT_ID"); const redirect_uri = getEnv("SPOTIFY_REDIRECT_URI"); const client_secret = getEnv("SPOTIFY_CLIENT_SECRET"); +var stateKey = 'spotify_auth_state'; -/* Express.js and socket.io setup */ +/* Express.js setup with LiquidJS */ const app = express(); +const engine = new Liquid(); +app.engine("liquid", engine.express()); +app.set("views", __dirname + "/views"); +app.set("view engine", "liquid"); +app.use(express.static(__dirname + "/static")) +app.use(cookieParser()); + +/* Socket.io setup */ const server = http.createServer(app); const io = new Server(server); -/* Cookie methods */ -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'; - -/* Routing methods */ -app.use(cookieParser()); - +// home page app.get("/", (req, res) => { - res.sendFile(__dirname + "/public/index.html"); + res.render('home'); }); +// dashboard - login required +app.get("/dashboard", (req, res) => { + if (req.cookies['access_token']) { + var currently_playing_params = { + url: 'https://api.spotify.com/v1/me/player/currently-playing', + headers: { 'Authorization': 'Bearer ' + req.cookies["access_token"] }, + json: true + }; + + request.get(currently_playing_params, (error, response, body) => { + if (response.statusCode == 200) { + // music currently playing + var current_image_url = body.item.album.images[0].url; + var current_progress = body.progress_ms; + var current_duration = body.item.duration_ms; + var next_refresh = current_duration - current_progress; + console.log(current_image_url, next_refresh) + res.render('dashboard', { + current_image_url: current_image_url + }); + } else if (response.statusCode == 204) { + // no music currently playing + res.render('dashboard', { + error: "Play some music to start!" + }) + } else if (response.statusCode == 401) { + // bad or expired token + res.redirect("/refresh_token?next=/dashboard") + } else { + // invalid HTTP code received + res.render('dashboard', { + error: response.statusCode + ": " + response.body + }); + } + }); + } else { + res.redirect('/') + } +}) + // request authorization from Spotify app.get("/login", (req, res) => { var state = generateRandomString(16); res.cookie(stateKey, state); var scope = "user-read-currently-playing"; - var params = new URLSearchParams([ - ['response_type', 'code'], - ['client_id', client_id], - ['scope', scope], - ['redirect_uri', redirect_uri], - ['state', state] - ]); + var params = new URLSearchParams({ + 'response_type': 'code', + 'client_id': client_id, + 'scope': scope, + 'redirect_uri': redirect_uri, + 'state': state, + 'show_dialog': true + }); res.redirect("https://accounts.spotify.com/authorize?" + params.toString()); }); @@ -75,9 +104,9 @@ app.get("/callback", (req, res) => { var storedState = req.cookies ? req.cookies[stateKey] : null; if (state == null || state !== storedState) { var params = new URLSearchParams({ 'error': 'state_mismatch' }); - res.redirect('/#' + params.toString()); + res.redirect('/#?' + params.toString()); } else { - // get api token + // generate token request res.clearCookie(stateKey); var authOptions = { url: 'https://accounts.spotify.com/api/token', @@ -92,33 +121,22 @@ app.get("/callback", (req, res) => { json: true }; - // post response - var status = ""; + // post token request request.post(authOptions, (error, response, body) => { if (!error && response.statusCode === 200) { - var access_token = body.access_token; - var refresh_token = body.refresh_token; - var options = { - url: 'https://api.spotify.com/v1/me', - headers: { 'Authorization': 'Bearer ' + access_token }, - json: true - }; - - request.get(options, (error, response, body) => { - console.log(body); - }); - - // pass token to the browser - var params = new URLSearchParams([ - ["access_token", access_token], - ["refresh_token", refresh_token] - ]); - status = "Authorized" - res.redirect('/#' + params.toString()); + // store access token and refresh token in session cookie + const cookieAttributes = { + httpOnly: true, + secure: true, + } + res.cookie("access_token", body.access_token, cookieAttributes) + res.cookie("refresh_token", body.refresh_token, cookieAttributes); + + // redirect to dashboard page + res.redirect('/dashboard'); } else { var params = new URLSearchParams({ "error": "invalid_token" }); - status = "Invalid Token" - res.redirect('/#', params.toString()); + res.redirect('/#?' + params.toString()); } }); } @@ -126,10 +144,15 @@ app.get("/callback", (req, res) => { // refresh access token app.get("/refresh_token", (req, res) => { - var refresh_token = req.query.refresh_token; + var next = req.query.next + console.log(next) + var refresh_token = req.cookies['refresh_token'] var authOptions = { url: 'https://accounts.spotify.com/api/token', - headers: { 'Authorization': 'Basic ' + Buffer.from(client_id + ':' + client_secret).toString('base64') }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic ' + Buffer.from(client_id + ':' + client_secret).toString('base64') + }, form: { grant_type: 'refresh_token', refresh_token: refresh_token @@ -137,12 +160,16 @@ app.get("/refresh_token", (req, res) => { json: true }; - request.post(authOptions, (error, response, body) =>{ - if (!error && response.statusCode == 200){ - var access_token = body.access_token; - res.send({ - "access_token": access_token - }); + request.post(authOptions, (error, response, body) => { + if (!error && response.statusCode == 200) { + res.cookie('access_token', body.access_token) + res.cookie('refresh_token', body.refresh_token) + res.redirect(next) + } else { + console.error('Token refresh error:', error); + console.log('Response:', response.statusCode, response.body); + console.log('Request:', authOptions); + res.redirect("/"); } }); }); diff --git a/helpers.js b/helpers.js new file mode 100644 index 0000000..995acc5 --- /dev/null +++ b/helpers.js @@ -0,0 +1,18 @@ +function generateRandomString(length) { + var text = ''; + var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (var i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +}; + +function getEnv(name) { + if (!process.env[name]) { + throw ("Spotipi: Error: Missing " + name + " in ./.env"); + } + return process.env[name]; +} + +module.exports = { generateRandomString, getEnv } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3742a4a..f251d33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "is-extglob": "^2.1.1", "is-glob": "^4.0.3", "is-number": "^7.0.0", + "liquidjs": "^10.9.4", "minimatch": "^3.1.2", "ms": "^2.1.3", "nopt": "^1.0.10", @@ -302,6 +303,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -916,6 +925,25 @@ "node": ">=0.6.0" } }, + "node_modules/liquidjs": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.9.4.tgz", + "integrity": "sha512-E7SmGMwhv0Pa1Yau6odd2EgNPAmrx1OOjzvpm9AFxBGVtCX2Bx4fOCDtDCML13L7g6zjLPN7Kb/kakyAl2HTPQ==", + "dependencies": { + "commander": "^10.0.0" + }, + "bin": { + "liquid": "bin/liquid.js", + "liquidjs": "bin/liquid.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/liquidjs" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1815,6 +1843,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2296,6 +2329,14 @@ "verror": "1.10.0" } }, + "liquidjs": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.9.4.tgz", + "integrity": "sha512-E7SmGMwhv0Pa1Yau6odd2EgNPAmrx1OOjzvpm9AFxBGVtCX2Bx4fOCDtDCML13L7g6zjLPN7Kb/kakyAl2HTPQ==", + "requires": { + "commander": "^10.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/package.json b/package.json index 16aaeba..c6bdd95 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "is-extglob": "^2.1.1", "is-glob": "^4.0.3", "is-number": "^7.0.0", + "liquidjs": "^10.9.4", "minimatch": "^3.1.2", "ms": "^2.1.3", "nopt": "^1.0.10", diff --git a/public/images/COIN.jpg b/static/images/COIN.jpg similarity index 100% rename from public/images/COIN.jpg rename to static/images/COIN.jpg diff --git a/public/images/HAIM.jpg b/static/images/HAIM.jpg similarity index 100% rename from public/images/HAIM.jpg rename to static/images/HAIM.jpg diff --git a/views/dashboard.liquid b/views/dashboard.liquid new file mode 100644 index 0000000..7314a41 --- /dev/null +++ b/views/dashboard.liquid @@ -0,0 +1,9 @@ +{% layout "index.liquid" %} +{% block content %} +

Dashboard

+ {% if error %} +

{{error}}

+ {% else %} + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/views/home.liquid b/views/home.liquid new file mode 100644 index 0000000..3c212ec --- /dev/null +++ b/views/home.liquid @@ -0,0 +1,5 @@ +{% layout "index.liquid" %} +{% block content %} +

Login

+ +{% endblock %} \ No newline at end of file diff --git a/public/index.html b/views/index.liquid similarity index 87% rename from public/index.html rename to views/index.liquid index 58c4f3e..a0de9e9 100644 --- a/public/index.html +++ b/views/index.liquid @@ -11,8 +11,10 @@ + + {% block content %}Default{% endblock %} -

Status:

+ \ No newline at end of file