: prototype of spotify token generation

This commit is contained in:
StarAppeal
2024-03-10 13:51:02 +01:00
parent c10f69fc1e
commit 4d697689ba
7 changed files with 254 additions and 1 deletions
+120
View File
@@ -13,6 +13,7 @@
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.11.19",
"@types/ws": "^8.5.10",
"axios": "^1.6.7",
"dotenv": "^16.4.4",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
@@ -212,6 +213,21 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -280,6 +296,17 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -349,6 +376,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -535,6 +570,25 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
@@ -550,6 +604,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1084,6 +1151,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -1799,6 +1871,21 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"requires": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1852,6 +1939,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1903,6 +1998,11 @@
"gopd": "^1.0.1"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -2053,6 +2153,11 @@
"unpipe": "~1.0.0"
}
},
"follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
},
"foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
@@ -2062,6 +2167,16 @@
"signal-exit": "^4.0.1"
}
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2427,6 +2542,11 @@
"ipaddr.js": "1.9.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+1
View File
@@ -18,6 +18,7 @@
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.11.19",
"@types/ws": "^8.5.10",
"axios": "^1.6.7",
"dotenv": "^16.4.4",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
+10 -1
View File
@@ -5,7 +5,8 @@ export default class User {
public name: string,
public uuid: string,
public id: ObjectId,
public config : UserConfig
public config : UserConfig,
public spotifyConfig: SpotifyConfig
) {}
}
@@ -16,3 +17,11 @@ export class UserConfig {
public isAdmin: boolean
) {}
}
export class SpotifyConfig {
constructor(
public accessToken: string,
public refreshToken: string,
public expirationDate: Date
) {}
}
+3
View File
@@ -5,6 +5,7 @@ import { UserService } from "./db/services/database.service";
import { RestUser } from "./rest/restUser";
import { authenticateJwt } from "./rest/middleware/authenticateJwt";
import { JwtTokenPropertiesExtractor } from "./rest/jwtTokenPropertiesExtractor";
import { SpotifyTokenGenerator } from "./rest/spotifyTokenGenerator";
const app = express();
const port = process.env.PORT || 3000;
@@ -17,10 +18,12 @@ app.use(express.json({ limit: "15mb" }));
const webSocketServer = new ExtendedWebSocketServer(server);
const restWebSocket = new RestWebSocket(webSocketServer);
const restUser = new RestUser(UserService.create);
const spotifyTokenGenerator = new SpotifyTokenGenerator(UserService.create);
const jwtTokenPropertiesExtractor = new JwtTokenPropertiesExtractor();
app.use("/api/websocket", authenticateJwt, restWebSocket.createRouter());
app.use("/api/user", authenticateJwt, restUser.createRouter());
app.use("/api/spotify", authenticateJwt, spotifyTokenGenerator.createRouter());
app.use(
"/api/jwt",
authenticateJwt,
+10
View File
@@ -0,0 +1,10 @@
export interface OAuthTokenResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scope: string;
expires_at?: number;
error?: string;
error_description?: string;
}
+59
View File
@@ -0,0 +1,59 @@
import { UserService } from "../db/services/database.service";
import express from "express";
import { SpotifyTokenService } from "../utils/spotifyTokenService";
export class SpotifyTokenGenerator {
constructor(private callback: () => Promise<UserService>) {}
public createRouter() {
const router = express.Router();
router.get("/token", async (req, res) => {
const userService = await this.callback();
const user = await userService.getUserById(req.payload._id);
const spotifyConfig = user.spotifyConfig;
const token = await new SpotifyTokenService().getToken(spotifyConfig);
if (token.expiresIn !== -1) {
console.log("Updating token");
spotifyConfig.accessToken = token.accessToken;
spotifyConfig.expirationDate = new Date(
Date.now() + token.expiresIn * 1000,
);
user.spotifyConfig = spotifyConfig;
await userService.updateUser(req.payload._id, user);
}
res.status(200).send({ accessToken: token.accessToken });
});
router.get(
"/token/generate/code/:auth_code/redirect-uri/:redirect_uri",
async (req, res) => {
const userService = await this.callback();
const authCode = req.params.auth_code;
const redirectUri = req.params.redirect_uri;
const user = await userService.getUserById(req.payload._id);
const token = await new SpotifyTokenService().generateToken(
authCode,
redirectUri,
);
console.log(token);
user.spotifyConfig = {
accessToken: token.access_token,
refreshToken: token.refresh_token,
expirationDate: new Date(Date.now() + token.expires_in * 1000),
};
await userService.updateUser(req.payload._id, user);
res.status(200).send({ tokenResponse: token });
},
);
return router;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { SpotifyConfig } from "../db/models/user";
import axios from "axios";
import { OAuthTokenResponse } from "../interfaces/OAuthTokenResponse";
const url = "https://accounts.spotify.com/api/token";
const clientId = process.env.SPOTIFY_CLIENT_ID;
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
export class SpotifyTokenService {
public async getToken(spotifyConfig: SpotifyConfig) {
if (spotifyConfig.expirationDate > new Date()) {
return { accessToken: spotifyConfig.accessToken, expiresIn: -1 };
}
console.log("Refreshing token");
const response = await axios.post(
url,
`grant_type=refresh_token&refresh_token=${spotifyConfig.refreshToken}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${clientId}:${clientSecret}`,
).toString("base64")}`,
},
},
);
const token = response.data as OAuthTokenResponse;
return { accessToken: token.access_token, expiresIn: token.expires_in };
}
public async generateToken(authorizationCode: string, redirectUri: string) {
const response = await axios.post(
url,
`grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${clientId}:${clientSecret}`,
).toString("base64")}`,
},
},
);
console.log(response.data);
return response.data as OAuthTokenResponse;
}
}