diff --git a/client/src/components/windows/settings/IntegrationSettingsEditor.tsx b/client/src/components/windows/settings/IntegrationSettingsEditor.tsx index ccfb08a..7bd4615 100644 --- a/client/src/components/windows/settings/IntegrationSettingsEditor.tsx +++ b/client/src/components/windows/settings/IntegrationSettingsEditor.tsx @@ -9,7 +9,7 @@ import DeleteIcon from '@material-ui/icons/Delete'; import * as serverApi from '../../../api'; import StoreLinkIcon, { ExternalStore } from '../../common/StoreLinkIcon'; import { v4 as genUuid } from 'uuid'; -import { getAuthToken } from '../../../lib/integration/spotify/spotify'; +import { testSpotify } from '../../../lib/integration/spotify/spotifyClientCreds'; let _ = require('lodash') interface EditIntegrationProps { @@ -113,7 +113,7 @@ function EditIntegration(props: EditIntegrationProps) { onClick={() => { props.onDelete(); }} >} {!props.submitting && } {props.submitting && } diff --git a/client/src/lib/integration/spotify/spotify.tsx b/client/src/lib/integration/spotify/spotify.tsx deleted file mode 100644 index 02aa5ca..0000000 --- a/client/src/lib/integration/spotify/spotify.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export async function getAuthToken(clientId: string, clientSecret: string) { - let requestOpts = { - method: "POST", - headers: { "Authorization": "Basic " + clientId + ":" + clientSecret }, - } - - const response = await fetch("https://accounts.spotify.com/api/token?grant_type=client_credentials", requestOpts) - return await response.json(); -} - -export default {} \ No newline at end of file diff --git a/client/src/lib/integration/spotify/spotifyClientCreds.tsx b/client/src/lib/integration/spotify/spotifyClientCreds.tsx new file mode 100644 index 0000000..641c106 --- /dev/null +++ b/client/src/lib/integration/spotify/spotifyClientCreds.tsx @@ -0,0 +1,14 @@ +export async function testSpotify() { + const requestOpts = { + method: 'GET', + }; + + const response = await fetch( + (process.env.REACT_APP_BACKEND || "") + '/spotifycc/v1/search?q=queens&type=artist', + requestOpts + ); + if (!response.ok) { + throw new Error("Response to tag merge not OK: " + JSON.stringify(response)); + } + console.log("Spotify response: ", response); +} \ No newline at end of file diff --git a/server/app.ts b/server/app.ts index d0d2682..09d0388 100644 --- a/server/app.ts +++ b/server/app.ts @@ -14,6 +14,7 @@ import { RegisterUser } from './endpoints/RegisterUser'; import * as endpointTypes from './endpoints/types'; import { sha512 } from 'js-sha512'; +import { useSpotifyClientCreds } from './integrations/spotifyClientCreds'; // For authentication var passport = require('passport'); @@ -100,6 +101,9 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => { } } + // Set up integration proxies + useSpotifyClientCreds(app); + // Set up REST API endpoints app.post(apiBaseUrl + api.CreateSongEndpoint, checkLogin(), _invoke(PostSong)); app.put(apiBaseUrl + api.ModifySongEndpoint, checkLogin(), _invoke(PutSong)); diff --git a/server/integrations/spotifyClientCreds.ts b/server/integrations/spotifyClientCreds.ts index 7bcd887..5ebfd70 100644 --- a/server/integrations/spotifyClientCreds.ts +++ b/server/integrations/spotifyClientCreds.ts @@ -1,15 +1,65 @@ +const { createProxyMiddleware } = require('http-proxy-middleware'); +let axios = require('axios') +let qs = require('querystring') + // The authorization token to use with the Spotify API. // Will need to be refreshed once in a while. -let authToken: string | null = null; +var authToken: string | null = null; + +async function updateToken(clientId: string, clientSecret: string) { + if (authToken) { return; } + + let buf = Buffer.from(clientId + ':' + clientSecret) + let encoded = buf.toString('base64'); + + let response = await axios.post( + 'https://accounts.spotify.com/api/token', + qs.stringify({ 'grant_type': 'client_credentials' }), + { + 'headers': { + 'Authorization': 'Basic ' + encoded, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + authToken = (await response).data.access_token; +} -export async function getAuthToken(clientId: string, clientSecret: string) { - let requestOpts = { - method: "POST", - headers: { "Authorization": "Basic " + clientId + ":" + clientSecret }, - } +let onProxyReq = (proxyReq: any, req: any, res: any) => { + proxyReq.setHeader("Authorization", "Bearer " + req._access_token) - const response = await fetch("https://accounts.spotify.com/api/token?grant_type=client_credentials", requestOpts) - return await response.json(); + console.log("Proxying request", + { + 'path': req.path, + 'originalUrl': req.originalUrl, + 'baseUrl': req.baseUrl, + }, + { + 'path': proxyReq.path, + 'originalUrl': proxyReq.originalUrl, + 'baseUrl': req.baseUrl, + }, + ); } -export async function \ No newline at end of file +export function useSpotifyClientCreds(app: any) { + // First add a layer which creates a token and saves it in the request. + app.use((req: any, res: any, next: any) => { + updateToken('c3e5e605e7814cdf94cd86eeba6f4c4f', '5d870c84a3c34aa3a4cf803aa95cb96a') + .then(() => { + req._access_token = authToken; + next(); + }) + }) + app.use( + '/spotifycc', + createProxyMiddleware({ + target: 'https://api.spotify.com/', + changeOrigin: true, + onProxyReq: onProxyReq, + logLevel: 'debug', + pathRewrite: { '^/spotifycc': '' }, + }) + ) +} \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 8b45277..7d49ca9 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,6 +24,14 @@ "xml2js": "^0.4.19" }, "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -69,6 +77,14 @@ "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==" }, + "@types/http-proxy": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz", + "integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "14.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.2.tgz", @@ -304,11 +320,18 @@ "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" }, "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", "requires": { - "follow-redirects": "1.5.10" + "follow-redirects": "^1.10.0" + }, + "dependencies": { + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + } } }, "balanced-match": { @@ -1034,6 +1057,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1654,6 +1682,68 @@ "toidentifier": "1.0.0" } }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz", + "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==", + "requires": { + "@types/http-proxy": "^1.17.4", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.20", + "micromatch": "^4.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -2474,6 +2564,11 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz", "integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==" }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-gyp": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", @@ -3052,6 +3147,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -3206,6 +3306,11 @@ } } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", diff --git a/server/package.json b/server/package.json index 762d412..a05b69b 100644 --- a/server/package.json +++ b/server/package.json @@ -8,11 +8,13 @@ "test": "ts-node node_modules/jasmine/bin/jasmine --config=test/jasmine.json" }, "dependencies": { + "axios": "^0.21.0", "body-parser": "^1.18.3", "chai": "^4.2.0", "chai-http": "^4.3.0", "express": "^4.16.4", "express-session": "^1.17.1", + "http-proxy-middleware": "^1.0.6", "jasmine": "^3.5.0", "js-sha512": "^0.8.0", "knex": "^0.21.5", @@ -20,11 +22,13 @@ "mssql": "^6.2.1", "mysql": "^2.18.1", "mysql2": "^2.1.0", + "node-fetch": "^2.6.1", "nodemon": "^2.0.4", "oracledb": "^5.0.0", "passport": "^0.4.1", "passport-local": "^1.0.0", "pg": "^8.3.3", + "querystring": "^0.2.0", "sqlite3": "^5.0.0", "ts-node": "^8.10.2", "typescript": "~3.7.2"