From ca8fcc935300b6ed7341b8063e3d224439f13ecf Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 23 Jan 2023 23:27:52 +0000 Subject: [PATCH] added `SyncController` --- packages/server/src/api.js | 4 +- .../SyncController/classes/secureSyncEntry.js | 114 ++++++++++++++ .../src/controllers/SyncController/index.js | 149 ++++++++++++++++++ .../SyncController/subcontrollers/spotify.js | 0 packages/server/src/controllers/index.js | 3 +- 5 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/controllers/SyncController/classes/secureSyncEntry.js create mode 100644 packages/server/src/controllers/SyncController/index.js create mode 100644 packages/server/src/controllers/SyncController/subcontrollers/spotify.js diff --git a/packages/server/src/api.js b/packages/server/src/api.js index 66eebd32..2274d708 100755 --- a/packages/server/src/api.js +++ b/packages/server/src/api.js @@ -1,5 +1,6 @@ import path from "path" import { Server as LinebridgeServer } from "linebridge/dist/server" + import express from "express" import bcrypt from "bcrypt" import passport from "passport" @@ -45,11 +46,12 @@ export default class Server { controllers.FeaturedEventsController, controllers.PlaylistsController, controllers.FeedController, + controllers.SyncController, ] middlewares = middlewares - server = new LinebridgeServer({ + server = global.server = new LinebridgeServer({ port: process.env.MAIN_LISTEN_PORT || 3000, headers: { "Access-Control-Expose-Headers": "regenerated_token", diff --git a/packages/server/src/controllers/SyncController/classes/secureSyncEntry.js b/packages/server/src/controllers/SyncController/classes/secureSyncEntry.js new file mode 100644 index 00000000..1da19f3c --- /dev/null +++ b/packages/server/src/controllers/SyncController/classes/secureSyncEntry.js @@ -0,0 +1,114 @@ +import { SyncEntry } from "../../../models" + +import crypto from "crypto" + +export default class SecureSyncEntry { + static get encrytionAlgorithm() { + return "aes-256-cbc" + } + + static async set(user_id, key, value) { + if (!user_id) { + throw new Error("Missing user_id") + } + + if (!key) { + throw new Error("Missing key") + } + + if (!value) { + throw new Error("Missing value") + } + + let entry = await SyncEntry.findOne({ key }).catch(() => null) + + const encryptionKey = Buffer.from(process.env.SYNC_ENCRIPT_SECRET, "hex") + const iv = crypto.randomBytes(16) + + const cipher = crypto.createCipheriv(SecureSyncEntry.encrytionAlgorithm, encryptionKey, iv) + + let encrypted + + try { + encrypted = cipher.update(value) + } + catch (error) { + console.error(error) + } + + encrypted = Buffer.concat([encrypted, cipher.final()]) + + if (entry) { + entry.value = iv.toString("hex") + ":" + encrypted.toString("hex") + + await entry.save() + + return entry + } + + entry = new SyncEntry({ + user_id, + key, + value: iv.toString("hex") + ":" + encrypted.toString("hex"), + }) + + await entry.save() + + return entry + } + + static async get(user_id, key) { + if (!user_id) { + throw new Error("Missing user_id") + } + + if (!key) { + throw new Error("Missing key") + } + + const entry = await SyncEntry.findOne({ + user_id, + key, + }).catch(() => null) + + if (!entry) { + return null + } + + const encryptionKey = Buffer.from(process.env.SYNC_ENCRIPT_SECRET, "hex") + + const iv = Buffer.from(entry.value.split(":")[0], "hex") + const encryptedText = Buffer.from(entry.value.split(":")[1], "hex") + + const decipher = crypto.createDecipheriv(SecureSyncEntry.encrytionAlgorithm, encryptionKey, iv) + + let decrypted = decipher.update(encryptedText) + + decrypted = Buffer.concat([decrypted, decipher.final()]) + + return decrypted.toString() + } + + static async delete(user_id, key) { + if (!user_id) { + throw new Error("Missing user_id") + } + + if (!key) { + throw new Error("Missing key") + } + + const entry = await SyncEntry.findOne({ + user_id, + key, + }).catch(() => null) + + if (!entry) { + return null + } + + await entry.delete() + + return entry + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/SyncController/index.js b/packages/server/src/controllers/SyncController/index.js new file mode 100644 index 00000000..8e281e3a --- /dev/null +++ b/packages/server/src/controllers/SyncController/index.js @@ -0,0 +1,149 @@ +import { Controller } from "linebridge/dist/server" +import SecureSyncEntry from "./classes/secureSyncEntry" + +import axios from "axios" + +export default class SyncController extends Controller { + static useRoute = "/sync" + + post = { + "/spotify/auth": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const { code, redirect_uri } = req.body + + if (!code) { + return res.status(400).json({ + message: "Missing code", + }) + } + + if (!redirect_uri) { + return res.status(400).json({ + message: "Missing redirect_uri", + }) + } + + const response = await axios({ + url: "https://accounts.spotify.com/api/token", + method: "post", + params: { + grant_type: "authorization_code", + code: code, + redirect_uri: redirect_uri + }, + headers: { + 'Authorization': `Basic ${(Buffer.from(process.env.SPOTIFY_CLIENT_ID + ":" + process.env.SPOTIFY_CLIENT_SECRET).toString("base64"))}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + + if (!response) { + return res.status(400).json({ + message: "Missing data", + }) + } + + await SecureSyncEntry.set(req.user._id.toString(), "spotify_access_token", response.data.access_token) + await SecureSyncEntry.set(req.user._id.toString(), "spotify_refresh_token", response.data.refresh_token) + + return res.json({ + message: "ok" + }) + } + }, + "/spotify/unlink": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + await SecureSyncEntry.delete(req.user._id.toString(), "spotify_access_token", "") + await SecureSyncEntry.delete(req.user._id.toString(), "spotify_refresh_token", "") + + return res.json({ + message: "ok" + }) + } + } + } + + get = { + "/spotify/client_id": async (req, res) => { + return res.json({ + client_id: process.env.SPOTIFY_CLIENT_ID, + }) + }, + "/spotify/is_authorized": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") + + if (!authToken) { + return res.json({ + is_authorized: false, + }) + } + + return res.json({ + is_authorized: true, + }) + } + }, + "/spotify/data": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") + + if (!authToken) { + return res.status(400).json({ + message: "Missing auth token", + }) + } + + const { data } = await axios.get("https://api.spotify.com/v1/me", { + headers: { + "Authorization": `Bearer ${authToken}` + }, + }).catch((error) => { + console.error(error.response.data) + + res.status(error.response.status).json(error.response.data) + + return null + }) + + + return res.json(data) + } + }, + "/spotify/currently_playing": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") + + if (!authToken) { + return res.status(400).json({ + message: "Missing auth token", + }) + } + + const response = await axios.get("https://api.spotify.com/v1/me/player", { + headers: { + "Authorization": `Bearer ${authToken}` + }, + }).catch((error) => { + console.error(error.response.data) + + res.status(error.response.status).json(error.response.data) + + return null + }) + + if (response) { + return res.json(response.data) + } + } + } + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/SyncController/subcontrollers/spotify.js b/packages/server/src/controllers/SyncController/subcontrollers/spotify.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/controllers/index.js b/packages/server/src/controllers/index.js index bb156989..a6948a6e 100755 --- a/packages/server/src/controllers/index.js +++ b/packages/server/src/controllers/index.js @@ -12,4 +12,5 @@ export { default as CommentsController } from "./CommentsController" export { default as SearchController } from "./SearchController" export { default as FeaturedEventsController } from "./FeaturedEventsController" export { default as PlaylistsController } from "./PlaylistsController" -export { default as FeedController } from "./FeedController" \ No newline at end of file +export { default as FeedController } from "./FeedController" +export { default as SyncController } from "./SyncController" \ No newline at end of file