diff --git a/boilerplates/controller.js b/boilerplates/controller.js index 9010406..512c9e3 100644 --- a/boilerplates/controller.js +++ b/boilerplates/controller.js @@ -10,7 +10,7 @@ const __controller_func = async (req, res) => { try { } catch (error) { - logger.error('Error', { error }); + logger.error('__controller_func', { error }); return res.status(500).send({ message: "Server Error. Try again." }); } } diff --git a/controllers/auth.js b/controllers/auth.js index 03a5c46..ec91b37 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -1,7 +1,7 @@ const { authInstance } = require("../utils/axios"); const typedefs = require("../typedefs"); -const { scopes, stateKey, accountsAPIURL } = require('../constants'); +const { scopes, stateKey, accountsAPIURL, sessionAgeInSeconds } = require('../constants'); const generateRandString = require('../utils/generateRandString'); const logger = require('../utils/logger')(module); @@ -28,7 +28,7 @@ const login = (_req, res) => { }).toString() ); } catch (error) { - logger.error('Error', { error }); + logger.error('login', { error }); return res.status(500).send({ message: "Server Error. Try again." }); } } @@ -68,7 +68,7 @@ const callback = async (req, res) => { logger.info('New login.'); req.session.accessToken = response.data.access_token; req.session.refreshToken = response.data.refresh_token; - req.session.cookie.maxAge = response.data.expires_in * 1000; + // note that session does not expire; so infinite refresh, just default access token expiration req.session.save((err) => { if (err) { @@ -86,7 +86,7 @@ const callback = async (req, res) => { } } } catch (error) { - logger.error('Error', { error }); + logger.error('callback', { error }); return res.status(500).send({ message: "Server Error. Try again." }); } } @@ -107,10 +107,9 @@ const refresh = async (req, res) => { const response = await authInstance.post('/api/token', authPayload); - if (response.statusCode === 200) { + if (response.status === 200) { req.session.accessToken = response.data.access_token; - req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; - req.session.cookie.maxAge = response.data.expires_in * 1000; + req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; // refresh token rotation logger.info(`Access token refreshed${(response.data.refresh_token !== null) ? ' and refresh token updated' : ''}.`); return res.status(200).send({ @@ -118,16 +117,38 @@ const refresh = async (req, res) => { }); } else { logger.error('refresh failed', { statusCode: response.status }); - res.status(response.status).send('Error: Refresh token flow failed.'); + return res.status(response.status).send('Error: Refresh token flow failed.'); } } catch (error) { - logger.error('Error', { error }); + logger.error('refresh', { error }); return res.status(500).send({ message: "Server Error. Try again." }); } }; +/** + * Clear session + * @param {typedefs.Req} req + * @param {typedefs.Res} res + */ +const logout = async (req, res) => { + try { + const delSession = req.session.destroy((err) => { + if (Object.keys(err).length) { + logger.error("Error while logging out", { err }); + } else { + logger.info("Logged out.", { sessionID: delSession.id }); + } + return res.sendStatus(200); + }) + } catch (error) { + logger.error('logout', { error }); + return res.status(500).send({ message: "Server Error. Try again." }); + } +} + module.exports = { login, callback, - refresh + refresh, + logout, }; \ No newline at end of file diff --git a/controllers/playlists.js b/controllers/playlists.js index 8cd1917..6c58224 100644 --- a/controllers/playlists.js +++ b/controllers/playlists.js @@ -26,12 +26,15 @@ const getUserPlaylists = async (req, res) => { } ); + if (response.status === 401) + return res.status(401).send(response.data); + /** @type {typedefs.SimplifiedPlaylist[]} */ playlists.items = response.data.items.map((playlist) => { return { name: playlist.name, description: playlist.description, - owner: playlist.owner.display_name, + owner_name: playlist.owner.display_name, id: playlist.id, } }); @@ -49,13 +52,15 @@ const getUserPlaylists = async (req, res) => { } } ); + if (response.status === 401) + return res.status(401).send(response.data); playlists.items.push( ...nextResponse.data.items.map((playlist) => { return { name: playlist.name, description: playlist.description, - owner: playlist.owner.display_name, + owner_name: playlist.owner.display_name, id: playlist.id, } }) @@ -76,7 +81,7 @@ const getUserPlaylists = async (req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const getUserPlaylist = async (req, res) => { +const getPlaylistDetails = async (req, res) => { try { /** @type {typedefs.Playlist} */ let playlist = {}; @@ -87,6 +92,8 @@ const getUserPlaylist = async (req, res) => { headers: { ...req.authHeader } } ); + if (response.status === 401) + return res.status(401).send(response.data); // TODO: this whole section needs to be DRYer // look into serializr @@ -95,7 +102,10 @@ const getUserPlaylist = async (req, res) => { playlist.description = response.data.description let { display_name, uri, id, ...rest } = response.data.owner playlist.owner = { display_name, uri, id } - playlist.followers = response.data.followers.total + playlist.followers = response.data.followers + playlist.total = response.data.tracks.total; + playlist.next = response.data.tracks.next; + playlist.tracks = response.data.tracks.items.map((playlist_track) => { return { added_at: playlist_track.added_at, @@ -109,13 +119,46 @@ const getUserPlaylist = async (req, res) => { } }); + + // keep getting batches of 50 till exhausted + while (playlist.next) { + const nextResponse = await axiosInstance.get( + playlist.next, // absolute URL from previous response which has offset and limit params + { + headers: { + ...req.authHeader + } + } + ); + if (nextResponse.status === 401) + return res.status(401).send(nextResponse.data); + + playlist.tracks.push( + ...nextResponse.data.items.map((playlist_track) => { + return { + added_at: playlist_track.added_at, + track: { + uri: playlist_track.track.uri, + name: playlist_track.track.name, + artists: playlist_track.track.artists.map((artist) => { return { name: artist.name } }), + album: { name: playlist_track.track.album.name }, + is_local: playlist_track.track.is_local, + } + } + }) + ); + + playlist.next = nextResponse.data.next; + } + return res.status(200).send(playlist); } catch (error) { - logger.error('getUserPlaylist', { error }); + logger.error('getPlaylistDetails', { error }); return res.status(500).send({ message: "Server Error. Try again." }); } } + module.exports = { getUserPlaylists, - getUserPlaylist, + getPlaylistDetails, }; \ No newline at end of file diff --git a/index.js b/index.js index a0211a6..68c236e 100644 --- a/index.js +++ b/index.js @@ -45,6 +45,7 @@ const neo4jSession = neo4jDriver.session(); (async () => { try { await neo4jSession.run('MATCH () RETURN 1 LIMIT 1'); + app.locals.neodb = neo4jSession; logger.info("Connected to Neo4j graph DB"); } catch (error) { logger.error("Neo4j connection error", { error }); @@ -99,16 +100,16 @@ const server = app.listen(port, () => { logger.info(`App Listening on port ${port}`); }); -const cleanupFunc = async () => { - logger.debug('SIGTERM signal received: closing server'); +const cleanupFunc = async (signal) => { if (neo4jSession) await neo4jSession.close(); if (neo4jDriver) await neo4jDriver.close(); - logger.debug('Neo4j connection closed'); + logger.info('Neo4j connection closed'); if (redisClient) await redisClient.disconnect(); - logger.debug('Redis client disconnected'); + logger.info('Redis client disconnected'); server.close(() => { - logger.debug('HTTP server closed'); + logger.info('HTTP server closed'); }); } +process.on('SIGINT', cleanupFunc); process.on('SIGTERM', cleanupFunc); \ No newline at end of file diff --git a/middleware/authCheck.js b/middleware/authCheck.js index e4525b4..4337550 100644 --- a/middleware/authCheck.js +++ b/middleware/authCheck.js @@ -2,22 +2,24 @@ const typedefs = require("../typedefs"); const logger = require("../utils/logger")(module); /** - * middleware to test if authenticated - * - * TODO: not checking if tokens are valid + * middleware to check if access token is present * @param {typedefs.Req} req * @param {typedefs.Res} res * @param {typedefs.Next} next */ const isAuthenticated = (req, res, next) => { - if (req.session.refreshToken && req.session.accessToken) { - // TODO: find a better way to set bearer token + if (req.session.accessToken) { req.authHeader = { 'Authorization': `Bearer ${req.session.accessToken}` }; next() } else { - const delSession = req.session.destroy(); - logger.info("Session destroyed.", { sessionID: delSession.id }); - res.status(401).redirect("/"); + const delSession = req.session.destroy((err) => { + if (err) { + logger.error("Error while destroying session.", { err }); + } else { + logger.info("Session destroyed.", { sessionID: delSession.id }); + } + return res.sendStatus(401); + }); } } diff --git a/routes/auth.js b/routes/auth.js index 2f0fa63..eff5144 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -1,6 +1,6 @@ const router = require('express').Router(); -const { login, callback, refresh } = require('../controllers/auth'); +const { login, callback, refresh, logout } = require('../controllers/auth'); const validator = require("../validators"); router.get( @@ -19,4 +19,8 @@ router.get( refresh ) +router.get( + "/logout", + logout, +) module.exports = router; diff --git a/routes/playlists.js b/routes/playlists.js index f92c6dc..ed18f44 100644 --- a/routes/playlists.js +++ b/routes/playlists.js @@ -1,6 +1,6 @@ const router = require('express').Router(); -const { getUserPlaylists, getUserPlaylist } = require('../controllers/playlists'); +const { getUserPlaylists, getPlaylistDetails, } = require('../controllers/playlists'); const { isAuthenticated } = require('../middleware/authCheck'); const validator = require("../validators"); @@ -14,7 +14,7 @@ router.get( "/details", isAuthenticated, validator.validate, - getUserPlaylist + getPlaylistDetails ); module.exports = router; diff --git a/typedefs.js b/typedefs.js index 743cd13..36920aa 100644 --- a/typedefs.js +++ b/typedefs.js @@ -7,6 +7,8 @@ * * @typedef {import('winston').Logger} Logger * + * @typedef {import('neo4j-driver').Session} Neo4jSession + * * @typedef {{ * display_name: string, * uri: string, diff --git a/utils/axios.js b/utils/axios.js index 26b5f2d..625b7d0 100644 --- a/utils/axios.js +++ b/utils/axios.js @@ -41,6 +41,7 @@ axiosInstance.interceptors.response.use( data: error.response.data } }); + return error.response; } else if (error.request) { // The request was made but no response was received logger.error(