back again...

bit of validation, some fixes, some auth corrections

scrapped graph db stuff

some misc. stuff, check the diff if you want bruh
This commit is contained in:
Kaushik Narayan R 2024-07-24 13:38:07 +05:30
parent 2225f5db49
commit 5803c997b2
13 changed files with 370 additions and 1720 deletions

1
.gitignore vendored
View File

@ -71,6 +71,7 @@ typings/
# dotenv environment variables file # dotenv environment variables file
.env .env
.env.development .env.development
.env.production
.env.test .env.test
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)

View File

@ -68,7 +68,7 @@ const callback = async (req, res) => {
logger.info('New login.'); logger.info('New login.');
req.session.accessToken = response.data.access_token; req.session.accessToken = response.data.access_token;
req.session.refreshToken = response.data.refresh_token; req.session.refreshToken = response.data.refresh_token;
// note that session does not expire; so infinite refresh, just default access token expiration req.session.cookie.maxAge = 7776000000 // 90 days, arbitrary
req.session.save((err) => { req.session.save((err) => {
if (err) { if (err) {
@ -110,6 +110,7 @@ const refresh = async (req, res) => {
if (response.status === 200) { if (response.status === 200) {
req.session.accessToken = response.data.access_token; req.session.accessToken = response.data.access_token;
req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; // refresh token rotation req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; // refresh token rotation
req.session.cookie.maxAge = 7776000000 // 90 days, arbitrary
logger.info(`Access token refreshed${(response.data.refresh_token !== null) ? ' and refresh token updated' : ''}.`); logger.info(`Access token refreshed${(response.data.refresh_token !== null) ? ' and refresh token updated' : ''}.`);
return res.status(200).send({ return res.status(200).send({
@ -133,7 +134,7 @@ const refresh = async (req, res) => {
const logout = async (req, res) => { const logout = async (req, res) => {
try { try {
const delSession = req.session.destroy((err) => { const delSession = req.session.destroy((err) => {
if (Object.keys(err).length) { if (Object.keys(err).length) { // err is empty obj if no error
logger.error("Error while logging out", { err }); logger.error("Error while logging out", { err });
return res.sendStatus(500); return res.sendStatus(500);
} else { } else {

View File

@ -26,8 +26,9 @@ const getUserPlaylists = async (req, res) => {
} }
); );
if (response.status === 401) if (response.status === 401) {
return res.status(401).send(response.data); return res.status(401).send(response.data);
}
/** @type {typedefs.SimplifiedPlaylist[]} */ /** @type {typedefs.SimplifiedPlaylist[]} */
playlists.items = response.data.items.map((playlist) => { playlists.items = response.data.items.map((playlist) => {

View File

@ -1,5 +1,6 @@
require('dotenv-flow').config(); require('dotenv-flow').config();
const util = require('util');
const express = require('express'); const express = require('express');
const session = require("express-session"); const session = require("express-session");
@ -9,7 +10,6 @@ const helmet = require("helmet");
const redis = require('redis'); const redis = require('redis');
const RedisStore = require("connect-redis").default; const RedisStore = require("connect-redis").default;
const neo4j = require('neo4j-driver');
const logger = require("./utils/logger")(module); const logger = require("./utils/logger")(module);
@ -35,24 +35,6 @@ redisClient.connect()
const redisStore = new RedisStore({ client: redisClient }); const redisStore = new RedisStore({ client: redisClient });
// Configure Neo4j driver and connect
const neo4jDriver = neo4j.driver(
process.env.NEO4J_URL,
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWD)
);
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 });
cleanupFunc();
}
})();
// Configure session middleware // Configure session middleware
app.use(session({ app.use(session({
store: redisStore, store: redisStore,
@ -100,16 +82,18 @@ const server = app.listen(port, () => {
logger.info(`App Listening on port ${port}`); logger.info(`App Listening on port ${port}`);
}); });
const cleanupFunc = async (signal) => { const cleanupFunc = (signal) => {
if (neo4jSession) await neo4jSession.close(); Promise.allSettled([
if (neo4jDriver) await neo4jDriver.close(); redisClient.disconnect,
logger.info('Neo4j connection closed'); util.promisify(server.close),
if (redisClient) await redisClient.disconnect(); ]).then(() => {
logger.info('Redis client disconnected'); if (signal)
server.close(() => { logger.info(`Caught ${signal} signal`);
logger.info('HTTP server closed'); logger.info("Cleaned up, exiting...");
process.exit(0);
}); });
} }
process.on('SIGINT', cleanupFunc); ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach((signal) => {
process.on('SIGTERM', cleanupFunc); process.on(signal, () => cleanupFunc(signal));
});

1983
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,6 @@
"express-session": "^1.17.3", "express-session": "^1.17.3",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
"helmet": "^7.0.0", "helmet": "^7.0.0",
"neo4j-driver": "^5.15.0",
"redis": "^4.6.10", "redis": "^4.6.10",
"winston": "^3.10.0" "winston": "^3.10.0"
}, },

View File

@ -1,6 +1,7 @@
const router = require('express').Router(); const router = require('express').Router();
const { login, callback, refresh, logout } = require('../controllers/auth'); const { login, callback, refresh, logout } = require('../controllers/auth');
const { isAuthenticated } = require('../middleware/authCheck');
const validator = require("../validators"); const validator = require("../validators");
router.get( router.get(
@ -15,7 +16,7 @@ router.get(
router.get( router.get(
"/refresh", "/refresh",
validator.validate, isAuthenticated,
refresh refresh
) )

View File

@ -2,6 +2,7 @@ const router = require('express').Router();
const { getUserPlaylists, getPlaylistDetails, } = require('../controllers/playlists'); const { getUserPlaylists, getPlaylistDetails, } = require('../controllers/playlists');
const { isAuthenticated } = require('../middleware/authCheck'); const { isAuthenticated } = require('../middleware/authCheck');
const { getPlaylistDetailsValidator } = require('../validators/playlists');
const validator = require("../validators"); const validator = require("../validators");
router.get( router.get(
@ -13,6 +14,7 @@ router.get(
router.get( router.get(
"/details", "/details",
isAuthenticated, isAuthenticated,
getPlaylistDetailsValidator,
validator.validate, validator.validate,
getPlaylistDetails getPlaylistDetails
); );

View File

@ -7,8 +7,6 @@
* *
* @typedef {import('winston').Logger} Logger * @typedef {import('winston').Logger} Logger
* *
* @typedef {import('neo4j-driver').Session} Neo4jSession
*
* @typedef {{ * @typedef {{
* display_name: string, * display_name: string,
* uri: string, * uri: string,

View File

@ -34,7 +34,7 @@ axiosInstance.interceptors.response.use(
if (error.response) { if (error.response) {
// Server has responded // Server has responded
logger.error( logger.error(
"API: Error", { "Spotify API: Error", {
response: { response: {
status: error.response.status, status: error.response.status,
statusText: error.response.statusText, statusText: error.response.statusText,
@ -45,7 +45,7 @@ axiosInstance.interceptors.response.use(
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
logger.error( logger.error(
"API: No response", { "Spotify API: No response", {
request: { request: {
url: error.request?.url, url: error.request?.url,
params: error.request?.params, params: error.request?.params,
@ -54,7 +54,7 @@ axiosInstance.interceptors.response.use(
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
logger.error( logger.error(
"API: Request error", { "Spotify API: Request error", {
error: { error: {
message: error.message, message: error.message,
} }

View File

@ -1,7 +1,7 @@
const path = require("path"); const path = require("path");
const { createLogger, transports, config, format } = require('winston'); const { createLogger, transports, config, format } = require('winston');
const { combine, label, timestamp, printf } = format; const { combine, label, timestamp, printf, errors } = format;
const typedefs = require("../typedefs"); const typedefs = require("../typedefs");
@ -12,21 +12,26 @@ const getLabel = (callingModule) => {
const logMetaReplacer = (key, value) => { const logMetaReplacer = (key, value) => {
if (key === "error") { if (key === "error") {
return value.name + ': ' + value.message; return {
name: value.name,
message: value.message,
stack: value.stack
};
} }
return value; return value;
} }
const metaFormat = (meta) => { const metaFormat = (meta) => {
if (Object.keys(meta).length > 0) if (Object.keys(meta).length > 0)
return '\n' + JSON.stringify(meta, logMetaReplacer) + '\n'; return '\n' + JSON.stringify(meta, logMetaReplacer, "\t") + '\n';
return '\n'; return '\n';
} }
const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
if (meta.error) { if (meta.error) {
for (const key in meta.error) { for (const key in meta.error) {
if (typeof key !== "symbol" && key !== "message" && key !== "name") { const allowedErrorKeys = ["name", "message", "stack"]
if (typeof key !== "symbol" && !allowedErrorKeys.includes(key)) {
delete meta.error[key] delete meta.error[key]
} }
} }
@ -43,6 +48,7 @@ const logger = (callingModule) => {
return createLogger({ return createLogger({
levels: config.npm.levels, levels: config.npm.levels,
format: combine( format: combine(
errors({ stack: true }),
label({ label: getLabel(callingModule) }), label({ label: getLabel(callingModule) }),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
logFormat, logFormat,

View File

@ -2,6 +2,7 @@ const { validationResult } = require("express-validator");
const typedefs = require("../typedefs"); const typedefs = require("../typedefs");
const { getNestedValuesString } = require("../utils/jsonTransformer"); const { getNestedValuesString } = require("../utils/jsonTransformer");
const logger = require("../utils/logger")(module);
/** /**
* Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator * Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator
@ -17,7 +18,7 @@ const validate = (req, res, next) => {
} }
const extractedErrors = [] const extractedErrors = []
errors.array().map(err => extractedErrors.push({ errors.array().map(err => extractedErrors.push({
[err.param]: err.msg [err.path]: err.msg
})); }));
return res.status(400).json({ return res.status(400).json({

23
validators/playlists.js Normal file
View File

@ -0,0 +1,23 @@
const { body, header, param, query } = require("express-validator");
const typedefs = require("../typedefs");
/**
* @param {typedefs.Req} req
* @param {typedefs.Res} res
* @param {typedefs.Next} next
*/
const getPlaylistDetailsValidator = async (req, res, next) => {
await query("playlist_id")
.notEmpty()
.withMessage("playlist_id not defined in query")
.isAlphanumeric()
.withMessage("playlist_id must be alphanumeric (base-62)")
.run(req);
next();
}
module.exports = {
getPlaylistDetailsValidator,
}