diff --git a/.env b/.env new file mode 100644 index 0000000..c3b28c2 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +CLIENT_ID = your_client_id_here +CLIENT_SECRET = your_client_secret_here +SESSION_SECRET = 'your_session_secret_string_here' +PORT = 9001 diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..971685d --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +REDIRECT_URI = http://localhost:9001/api/auth/callback +TRUST_PROXY=1 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..c2892a6 --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +REDIRECT_URI = https://domain.for.this.app/api/auth/callback +TRUST_PROXY=1 diff --git a/.gitignore b/.gitignore index 9a3f0f9..f20e8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -69,10 +69,10 @@ typings/ .yarn-integrity # dotenv environment variables file -.env -.env.development -.env.production -.env.test +.env.local +.env.development.local +.env.production.local +.env.test.local # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/README.md b/README.md index dbda3bc..d91b3fe 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # spotify-manager + Personal Spotify playlist manager. Features inbound! diff --git a/boilerplates/controller.js b/boilerplates/controller.js index 7eaddd9..e6578c4 100644 --- a/boilerplates/controller.js +++ b/boilerplates/controller.js @@ -8,7 +8,7 @@ const typedefs = require("../typedefs"); */ const __controller_func = async (req, res) => { try { - + } catch (error) { logger.error('__controller_func', { error }); return res.sendStatus(500); @@ -17,4 +17,4 @@ const __controller_func = async (req, res) => { module.exports = { __controller_func -}; \ No newline at end of file +}; diff --git a/boilerplates/validator.js b/boilerplates/validator.js index ff86c7a..cbbd621 100644 --- a/boilerplates/validator.js +++ b/boilerplates/validator.js @@ -17,6 +17,5 @@ const __validator_func = async (req, res, next) => { } module.exports = { - __validator_func, + __validator_func } - diff --git a/constants.js b/constants.js index 0126b49..98bbaf4 100644 --- a/constants.js +++ b/constants.js @@ -19,7 +19,7 @@ const scopes = { ModifyPrivatePlaylists: 'playlist-modify-private', ControlRemotePlayback: 'app-remote-control', ModifyLibrary: 'user-library-modify', - ViewLibrary: 'user-library-read', + ViewLibrary: 'user-library-read' }; module.exports = { diff --git a/controllers/auth.js b/controllers/auth.js index 9f8d225..97285ec 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -1,4 +1,4 @@ -const { authInstance } = require("../utils/axios"); +const { authInstance, axiosInstance } = require("../utils/axios"); const typedefs = require("../typedefs"); const { scopes, stateKey, accountsAPIURL, sessionAgeInSeconds } = require('../constants'); @@ -62,28 +62,36 @@ const callback = async (req, res) => { const authPayload = (new URLSearchParams(authForm)).toString(); - const response = await authInstance.post('/api/token', authPayload); + const tokenResponse = await authInstance.post('/api/token', authPayload); - if (response.status === 200) { + if (tokenResponse.status === 200) { logger.info('New login.'); - req.session.accessToken = response.data.access_token; - req.session.refreshToken = response.data.refresh_token; + req.session.accessToken = tokenResponse.data.access_token; + req.session.refreshToken = tokenResponse.data.refresh_token; req.session.cookie.maxAge = 7 * 24 * 60 * 60 * 1000 // 1 week - - req.session.save((err) => { - if (err) { - logger.error("redis session save error", { sessionError: err }) - throw err; - } - }); - - return res.status(200).send({ - message: "Login successful", - }); } else { - logger.error('login failed', { statusCode: response.status }); - res.status(response.status).send('Error: Login failed'); + logger.error('login failed', { statusCode: tokenResponse.status }); + res.status(tokenResponse.status).send('Error: Login failed'); } + + const userResponse = await axiosInstance.get( + "/me", + { + headers: { + 'Authorization': `Bearer ${req.session.accessToken}` + } + } + ); + + /** @type {typedefs.User} */ + req.session.user = { + username: userResponse.data.display_name, + id: userResponse.data.id, + }; + + return res.status(200).send({ + message: "Login successful", + }); } } catch (error) { logger.error('callback', { error }); @@ -153,5 +161,5 @@ module.exports = { login, callback, refresh, - logout, -}; \ No newline at end of file + logout +}; diff --git a/controllers/playlists.js b/controllers/playlists.js index 5386ca2..b210c63 100644 --- a/controllers/playlists.js +++ b/controllers/playlists.js @@ -14,7 +14,7 @@ const getUserPlaylists = async (req, res) => { // get first 50 const response = await axiosInstance.get( - "/me/playlists", + `/users/${req.session.user.id}/playlists`, { params: { offset: 0, @@ -26,7 +26,7 @@ const getUserPlaylists = async (req, res) => { } ); - if (response.status === 401) { + if (response.status === 401) { return res.status(401).send(response.data); } @@ -34,9 +34,7 @@ const getUserPlaylists = async (req, res) => { playlists.items = response.data.items.map((playlist) => { return { name: playlist.name, - description: playlist.description, - owner_name: playlist.owner.display_name, - id: playlist.id, + id: playlist.id } }); @@ -60,9 +58,7 @@ const getUserPlaylists = async (req, res) => { ...nextResponse.data.items.map((playlist) => { return { name: playlist.name, - description: playlist.description, - owner_name: playlist.owner.display_name, - id: playlist.id, + id: playlist.id } }) ); @@ -70,6 +66,8 @@ const getUserPlaylists = async (req, res) => { playlists.next = nextResponse.data.next; } + delete playlists.next; + return res.status(200).send(playlists); } catch (error) { logger.error('getUserPlaylists', { error }); @@ -161,5 +159,5 @@ const getPlaylistDetails = async (req, res) => { module.exports = { getUserPlaylists, - getPlaylistDetails, -}; \ No newline at end of file + getPlaylistDetails +}; diff --git a/index.js b/index.js index a546eeb..4440c5f 100644 --- a/index.js +++ b/index.js @@ -35,12 +35,7 @@ app.use(session({ } })); -// Configure CORS options -const corsOptions = { - origin: [process.env.CORS_ORIGIN], -} - -app.use(cors(corsOptions)); +app.use(cors()); app.use(cookieParser()); // Configure helmet diff --git a/middleware/authCheck.js b/middleware/authCheck.js index 1184931..cb4c612 100644 --- a/middleware/authCheck.js +++ b/middleware/authCheck.js @@ -26,5 +26,5 @@ const isAuthenticated = (req, res, next) => { } module.exports = { - isAuthenticated, -} \ No newline at end of file + isAuthenticated +} diff --git a/routes/auth.js b/routes/auth.js index 035365e..8579109 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -18,10 +18,11 @@ router.get( "/refresh", isAuthenticated, refresh -) +); router.get( "/logout", - logout, -) + logout +); + module.exports = router; diff --git a/routes/playlists.js b/routes/playlists.js index e6eaf6d..f0255e0 100644 --- a/routes/playlists.js +++ b/routes/playlists.js @@ -1,6 +1,6 @@ const router = require('express').Router(); -const { getUserPlaylists, getPlaylistDetails, } = require('../controllers/playlists'); +const { getUserPlaylists, getPlaylistDetails } = require('../controllers/playlists'); const { isAuthenticated } = require('../middleware/authCheck'); const { getPlaylistDetailsValidator } = require('../validators/playlists'); const validator = require("../validators"); @@ -11,6 +11,7 @@ router.get( validator.validate, getUserPlaylists ); + router.get( "/details", isAuthenticated, diff --git a/typedefs.js b/typedefs.js index 743cd13..35867b2 100644 --- a/typedefs.js +++ b/typedefs.js @@ -9,14 +9,11 @@ * * @typedef {{ * display_name: string, - * uri: string, * id: string - * }} PlaylistOwner + * }} User * * @typedef {{ * name: string, - * description: string, - * owner: PlaylistOwner, * id: string, * }} SimplifiedPlaylist * @@ -45,7 +42,7 @@ * uri: string, * name: string, * description: string, - * owner: PlaylistOwner, + * owner: User, * followers: { * total: number * }, diff --git a/utils/axios.js b/utils/axios.js index 0c659e8..5132786 100644 --- a/utils/axios.js +++ b/utils/axios.js @@ -23,10 +23,11 @@ const axiosInstance = axios.default.create({ axiosInstance.interceptors.request.use(request => { logger.info("API call", { url: request.url, - params: request.params, + method: request.method, + params: request.params ?? {}, }); return request; -}) +}); axiosInstance.interceptors.response.use( (response) => response, @@ -62,9 +63,9 @@ axiosInstance.interceptors.response.use( } return Promise.reject(error); } -) +); module.exports = { authInstance, - axiosInstance, -}; \ No newline at end of file + axiosInstance +}; diff --git a/utils/generateRandString.js b/utils/generateRandString.js index a4faabe..e33412b 100644 --- a/utils/generateRandString.js +++ b/utils/generateRandString.js @@ -11,4 +11,4 @@ module.exports = (length) => { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; -}; \ No newline at end of file +}; diff --git a/utils/jsonTransformer.js b/utils/jsonTransformer.js index 47d43df..b7389ba 100644 --- a/utils/jsonTransformer.js +++ b/utils/jsonTransformer.js @@ -19,4 +19,4 @@ const getNestedValuesString = (obj) => { module.exports = { getNestedValuesString -} \ No newline at end of file +} diff --git a/utils/logger.js b/utils/logger.js index d8a0614..daca4bf 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -6,37 +6,37 @@ const { combine, label, timestamp, printf, errors } = format; const typedefs = require("../typedefs"); const getLabel = (callingModule) => { - const parts = callingModule.filename.split(path.sep); - return path.join(parts[parts.length - 2], parts.pop()); + const parts = callingModule.filename.split(path.sep); + return path.join(parts[parts.length - 2], parts.pop()); }; const logMetaReplacer = (key, value) => { - if (key === "error") { - return { - name: value.name, - message: value.message, - stack: value.stack - }; - } - return value; + if (key === "error") { + return { + name: value.name, + message: value.message, + stack: value.stack + }; + } + return value; } const metaFormat = (meta) => { - if (Object.keys(meta).length > 0) - return '\n' + JSON.stringify(meta, logMetaReplacer, "\t") + '\n'; - return '\n'; + if (Object.keys(meta).length > 0) + return '\n' + JSON.stringify(meta, logMetaReplacer, "\t") + '\n'; + return '\n'; } const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { - if (meta.error) { - for (const key in meta.error) { - const allowedErrorKeys = ["name", "message", "stack"] - if (typeof key !== "symbol" && !allowedErrorKeys.includes(key)) { - delete meta.error[key] - } + if (meta.error) { + for (const key in meta.error) { + const allowedErrorKeys = ["name", "message", "stack"] + if (typeof key !== "symbol" && !allowedErrorKeys.includes(key)) { + delete meta.error[key] + } + } } - } - return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`; + return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`; }); /** @@ -45,22 +45,22 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { * @returns {typedefs.Logger} */ const logger = (callingModule) => { - return createLogger({ - levels: config.npm.levels, - format: combine( - errors({ stack: true }), - label({ label: getLabel(callingModule) }), - timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), - logFormat, - ), - transports: [ - process.env.NODE_ENV !== 'production' ? - new transports.Console() : - new transports.Console(), - new transports.File({ filename: __dirname + '/../logs/common.log' }), - new transports.File({ filename: __dirname + '/../logs/error.log', level: 'error' }), - ] - }); + return createLogger({ + levels: config.npm.levels, + format: combine( + errors({ stack: true }), + label({ label: getLabel(callingModule) }), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), + logFormat, + ), + transports: [ + process.env.NODE_ENV !== 'production' ? + new transports.Console() : + new transports.Console(), + new transports.File({ filename: __dirname + '/../logs/common.log' }), + new transports.File({ filename: __dirname + '/../logs/error.log', level: 'error' }), + ] + }); } module.exports = logger; \ No newline at end of file diff --git a/validators/index.js b/validators/index.js index 86d8052..8a2f904 100644 --- a/validators/index.js +++ b/validators/index.js @@ -24,9 +24,9 @@ const validate = (req, res, next) => { return res.status(400).json({ message: getNestedValuesString(extractedErrors), errors: extractedErrors - }) + }); } module.exports = { - validate, + validate } \ No newline at end of file diff --git a/validators/playlists.js b/validators/playlists.js index afcba5d..8299d08 100644 --- a/validators/playlists.js +++ b/validators/playlists.js @@ -18,6 +18,5 @@ const getPlaylistDetailsValidator = async (req, res, next) => { } module.exports = { - getPlaylistDetailsValidator, + getPlaylistDetailsValidator } -