package checks, healthcheck, lot of cookie fiddling, some logging, bug fixing

This commit is contained in:
Kaushik Narayan R 2024-12-28 12:17:39 -07:00
parent d999db53ae
commit 79060661a6
11 changed files with 368 additions and 351 deletions

View File

@ -1,4 +1,6 @@
BASE_DOMAIN = localhost
REDIRECT_URI = http://localhost:9001/api/auth/callback
APP_URI = http://localhost:3000
DB_USER = your_database_username
DB_PASSWD = your_database_password
DB_NAME = your_database_name

View File

@ -1 +1,3 @@
REDIRECT_URI = https://domain.for.this.app/api/auth/callback
BASE_DOMAIN = domain.app
REDIRECT_URI = https://backend.app/api/auth/callback
APP_URI = https://frontend.app

View File

@ -35,6 +35,11 @@ const singleRequest = async (req, res, method, path, config = {}, data = null, i
// Non 2XX response received
let logMsg;
if (error.response.status >= 400 && error.response.status < 600) {
if (error.response.status === 401) {
logMsg="reauth, attempting...";
logger.info(logPrefix + logMsg);
// reauth here?
}
res.status(error.response.status).send(error.response.data);
logMsg = "" + error.response.status
}
@ -42,7 +47,7 @@ const singleRequest = async (req, res, method, path, config = {}, data = null, i
res.sendStatus(error.response.status);
logMsg = "???";
}
logger.error(logPrefix + logMsg, {
logger.warn(logPrefix + logMsg, {
response: {
data: error.response.data,
status: error.response.status,
@ -51,7 +56,7 @@ const singleRequest = async (req, res, method, path, config = {}, data = null, i
} else if (error.request) {
// No response received
res.sendStatus(504);
logger.warn(logPrefix + "No response", { error });
logger.error(logPrefix + "No response", { error });
} else {
// Something happened in setting up the request that triggered an Error
res.sendStatus(500);
@ -144,7 +149,7 @@ const checkPlaylistEditable = async (req, res, playlistID, userID) => {
message: "You cannot edit this playlist, you must be the owner/the playlist must be collaborative",
playlistID: playlistID
});
logger.warn("user cannot edit target playlist", { playlistID: playlistID });
logger.info("user cannot edit target playlist", { playlistID: playlistID });
return false;
} else {
return true;

View File

@ -49,10 +49,10 @@ const callback = async (req, res) => {
// check state
if (state === null || state !== storedState) {
res.redirect(409, "/");
logger.error("state mismatch");
logger.warn("state mismatch");
return;
} else if (error) {
res.status(401).send("Auth callback error");
res.status(401).send({ message: "Auth callback error" });
logger.error("callback error", { error });
return;
} else {
@ -73,10 +73,9 @@ const callback = async (req, res) => {
logger.debug("Tokens obtained.");
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
} else {
logger.error("login failed", { statusCode: tokenResponse.status });
res.status(tokenResponse.status).send("Error: Login failed");
res.status(tokenResponse.status).send({ message: "Error: Login failed" });
}
const userData = await getUserProfile(req, res);
@ -88,8 +87,9 @@ const callback = async (req, res) => {
id: userData.id,
};
res.sendStatus(200);
logger.info("New login.", { username: userData.display_name });
// res.sendStatus(200);
res.redirect(process.env.APP_URI + "?login=success");
logger.debug("New login.", { username: userData.display_name });
return;
}
} catch (error) {
@ -120,10 +120,10 @@ const refresh = async (req, res) => {
req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; // refresh token rotation
res.sendStatus(200);
logger.info(`Access token refreshed${(response.data.refresh_token !== null) ? " and refresh token updated" : ""}.`);
logger.debug(`Access token refreshed${(response.data.refresh_token !== null) ? " and refresh token updated" : ""}.`);
return;
} else {
res.status(response.status).send("Error: Refresh token flow failed.");
res.status(response.status).send({ message: "Error: Refresh token flow failed." });
logger.error("refresh failed", { statusCode: response.status });
return;
}
@ -141,15 +141,15 @@ const refresh = async (req, res) => {
*/
const logout = async (req, res) => {
try {
const delSession = req.session.destroy((err) => {
if (err) {
const delSession = req.session.destroy((error) => {
if (error) {
res.sendStatus(500);
logger.error("Error while logging out", { err });
logger.error("Error while logging out", { error });
return;
} else {
res.clearCookie(sessionName);
res.sendStatus(200);
logger.info("Logged out.", { sessionID: delSession.id });
logger.debug("Logged out.", { sessionID: delSession.id });
return;
}
})

View File

@ -98,7 +98,7 @@ const updateUser = async (req, res) => {
});
if (cleanedUser !== toRemovePls.length) {
res.sendStatus(500);
logger.error("Could not remove all old playlists", { error: new Error("Playlists.destroy failed?") });
logger.warn("Could not remove all old playlists", { error: new Error("Playlists.destroy failed?") });
return;
}
}
@ -116,7 +116,7 @@ const updateUser = async (req, res) => {
}
res.status(200).send({ removedLinks: removedLinks > 0 });
logger.info("Updated user data", { delLinks: removedLinks, delPls: cleanedUser, addPls: updatedUser.length });
logger.debug("Updated user data", { delLinks: removedLinks, delPls: cleanedUser, addPls: updatedUser.length });
return;
} catch (error) {
res.sendStatus(500);
@ -154,7 +154,7 @@ const fetchUser = async (req, res) => {
playlists: currentPlaylists,
links: currentLinks
});
logger.info("Fetched user data", { pls: currentPlaylists.length, links: currentLinks.length });
logger.debug("Fetched user data", { pls: currentPlaylists.length, links: currentLinks.length });
return;
} catch (error) {
res.sendStatus(500);
@ -174,17 +174,16 @@ const createLink = async (req, res) => {
let fromPl, toPl;
try {
fromPl = parseSpotifyLink(req.body["from"]);
toPl = parseSpotifyLink(req.body["to"]);
fromPl = parseSpotifyLink(req.body.from);
toPl = parseSpotifyLink(req.body.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" });
logger.warn("non-playlist link provided", { from: fromPl, to: toPl });
logger.info("non-playlist link provided", { from: fromPl, to: toPl });
return;
}
} catch (error) {
res.status(400).send({ message: error.message });
logger.error("parseSpotifyLink", { error });
res.status(400).send({ message: "Could not parse link" });
logger.warn("parseSpotifyLink", { error });
return;
}
@ -197,8 +196,8 @@ const createLink = async (req, res) => {
// if playlists are unknown
if (![fromPl, toPl].every(pl => playlists.includes(pl.id))) {
res.sendStatus(404);
logger.error("unknown playlists, resync");
res.status(404).send({ message: "Playlists out of sync "});
logger.warn("unknown playlists, resync");
return;
}
@ -214,7 +213,7 @@ const createLink = async (req, res) => {
});
if (existingLink) {
res.sendStatus(409);
logger.error("link already exists");
logger.info("link already exists");
return;
}
@ -228,7 +227,7 @@ const createLink = async (req, res) => {
if (newGraph.detectCycle()) {
res.status(400).send({ message: "Proposed link cannot cause a cycle in the graph" });
logger.error("potential cycle detected");
logger.warn("potential cycle detected");
return;
}
@ -244,7 +243,7 @@ const createLink = async (req, res) => {
}
res.sendStatus(201);
logger.info("Created link");
logger.debug("Created link");
return;
} catch (error) {
res.sendStatus(500);
@ -265,16 +264,16 @@ const removeLink = async (req, res) => {
let fromPl, toPl;
try {
fromPl = parseSpotifyLink(req.body["from"]);
toPl = parseSpotifyLink(req.body["to"]);
fromPl = parseSpotifyLink(req.body.from);
toPl = parseSpotifyLink(req.body.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" });
logger.warn("non-playlist link provided", { from: fromPl, to: toPl });
logger.info("non-playlist link provided", { from: fromPl, to: toPl });
return;
}
} catch (error) {
res.status(400).send({ message: error.message });
logger.error("parseSpotifyLink", { error });
res.status(400).send({ message: "Could not parse link" });
logger.warn("parseSpotifyLink", { error });
return;
}
@ -290,7 +289,7 @@ const removeLink = async (req, res) => {
});
if (!existingLink) {
res.sendStatus(409);
logger.error("link does not exist");
logger.warn("link does not exist");
return;
}
@ -310,7 +309,7 @@ const removeLink = async (req, res) => {
}
res.sendStatus(200);
logger.info("Deleted link");
logger.debug("Deleted link");
return;
} catch (error) {
res.sendStatus(500);
@ -350,12 +349,12 @@ const populateSingleLink = async (req, res) => {
toPl = parseSpotifyLink(req.body.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" });
logger.warn("non-playlist link provided", link);
logger.info("non-playlist link provided", link);
return;
}
} catch (error) {
res.status(400).send({ message: error.message });
logger.error("parseSpotifyLink", { error });
res.status(400).send({ message: "Could not parse link" });
logger.warn("parseSpotifyLink", { error });
return;
}
@ -400,7 +399,7 @@ const populateSingleLink = async (req, res) => {
// keep getting batches of 50 till exhausted
for (let i = 1; "next" in fromPlaylist; i++) {
while (fromPlaylist.next) {
const nextData = await getPlaylistDetailsNextPage(req, res, fromPlaylist.next);
if (res.headersSent) return;
@ -436,7 +435,7 @@ const populateSingleLink = async (req, res) => {
});
// keep getting batches of 50 till exhausted
for (let i = 1; "next" in toPlaylist; i++) {
while (toPlaylist.next) {
const nextData = await getPlaylistDetailsNextPage(req, res, toPlaylist.next);
if (res.headersSent) return;
@ -475,7 +474,7 @@ const populateSingleLink = async (req, res) => {
added: toAddNum,
local: localNum,
});
logger.info(`Backfilled ${toAddNum} tracks, could not add ${localNum} local files.`);
logger.debug(`Backfilled ${toAddNum} tracks, could not add ${localNum} local files.`);
return;
} catch (error) {
res.sendStatus(500);
@ -507,16 +506,16 @@ const pruneSingleLink = async (req, res) => {
let fromPl, toPl;
try {
fromPl = parseSpotifyLink(req.body["from"]);
toPl = parseSpotifyLink(req.body["to"]);
fromPl = parseSpotifyLink(req.body.from);
toPl = parseSpotifyLink(req.body.to);
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
res.status(400).send({ message: "Link is not a playlist" });
logger.warn("non-playlist link provided", { from: fromPl, to: toPl });
return
logger.info("non-playlist link provided", { from: fromPl, to: toPl });
return;
}
} catch (error) {
res.status(400).send({ message: error.message });
logger.error("parseSpotifyLink", { error });
logger.warn("parseSpotifyLink", { error });
return;
}
@ -532,7 +531,7 @@ const pruneSingleLink = async (req, res) => {
});
if (!existingLink) {
res.sendStatus(409);
logger.error("link does not exist");
logger.warn("link does not exist");
return
}
@ -561,7 +560,7 @@ const pruneSingleLink = async (req, res) => {
});
// keep getting batches of 50 till exhausted
for (let i = 1; "next" in fromPlaylist; i++) {
while (fromPlaylist.next) {
const nextData = await getPlaylistDetailsNextPage(req, res, fromPlaylist.next);
if (res.headersSent) return;
@ -598,7 +597,7 @@ const pruneSingleLink = async (req, res) => {
});
// keep getting batches of 50 till exhausted
for (let i = 1; "next" in toPlaylist; i++) {
while (toPlaylist.next) {
const nextData = await getPlaylistDetailsNextPage(req, res, toPlaylist.next);
if (res.headersSent) return;
@ -638,7 +637,7 @@ const pruneSingleLink = async (req, res) => {
}
res.status(200).send({ message: `Removed ${toDelNum} tracks.` });
logger.info(`Pruned ${toDelNum} tracks`);
logger.debug(`Pruned ${toDelNum} tracks`);
return;
} catch (error) {
res.sendStatus(500);

View File

@ -51,7 +51,7 @@ const fetchUserPlaylists = async (req, res) => {
delete userPlaylists.next;
res.status(200).send(userPlaylists);
logger.info("Fetched user playlists", { num: userPlaylists.total });
logger.debug("Fetched user playlists", { num: userPlaylists.total });
return;
} catch (error) {
res.sendStatus(500);
@ -83,7 +83,7 @@ const fetchPlaylistDetails = async (req, res) => {
}
} catch (error) {
res.status(400).send({ message: error.message });
logger.error("parseSpotifyLink", { error });
logger.warn("parseSpotifyLink", { error });
return;
}
@ -144,7 +144,7 @@ const fetchPlaylistDetails = async (req, res) => {
delete playlist.next;
res.status(200).send(playlist);
logger.info("Fetched playlist tracks", { num: playlist.tracks.length });
logger.debug("Fetched playlist tracks", { num: playlist.tracks.length });
return;
} catch (error) {
res.sendStatus(500);

View File

@ -13,6 +13,7 @@ const { sessionName } = require("./constants");
const db = require("./models");
const { isAuthenticated } = require("./middleware/authCheck");
const { getUserProfile } = require("./api/spotify");
const logger = require("./utils/logger")(module);
@ -35,13 +36,21 @@ app.use(session({
resave: false,
saveUninitialized: false,
cookie: {
secure: "auto", // if true only transmit cookie over https
httpOnly: true, // if true prevent client side JS from reading the cookie
domain: process.env.BASE_DOMAIN,
httpOnly: true, // if true prevent client side JS from reading the cookie
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
sameSite: process.env.NODE_ENV === "development" ? "lax" : "none", // cross-site for production
secure: process.env.NODE_ENV === "development" ? false : true, // if true only transmit cookie over https
}
}));
app.use(cors());
app.use(helmet());
app.use(cors({
origin: process.env.APP_URI,
credentials: true
}));
app.use(helmet({
crossOriginOpenerPolicy: { policy: process.env.NODE_ENV === "development" ? "unsafe-none" : "same-origin" }
}));
app.disable("x-powered-by");
app.use(cookieParser());
@ -51,6 +60,24 @@ app.use(express.urlencoded({ extended: true }));
// Static
app.use(express.static(__dirname + "/static"));
// Healthcheck
app.use("/health", (req, res) => {
res.sendStatus(200);
return;
});
app.use("/auth-health", isAuthenticated, async (req, res) => {
try {
await getUserProfile(req, res);
if (res.headersSent) return;
res.sendStatus(200);
return;
} catch (error) {
res.sendStatus(500);
logger.error("authHealthCheck", { error });
return;
}
});
// Routes
app.use("/api/auth/", require("./routes/auth"));
app.use("/api/playlists", isAuthenticated, require("./routes/playlists"));
@ -61,7 +88,7 @@ app.use((req, res) => {
res.status(404).send(
"Guess the <a href=\"https://github.com/20kaushik02/spotify-manager\">cat's</a> out of the bag!"
);
logger.info("Unrecognized URL", { url: req.url });
logger.info("404", { url: req.url });
return;
});
@ -73,7 +100,7 @@ const server = app.listen(port, () => {
const cleanupFunc = (signal) => {
if (signal)
logger.info(`${signal} signal received, shutting down now...`);
logger.debug(`${signal} signal received, shutting down now...`);
Promise.allSettled([
db.sequelize.close(),

View File

@ -19,7 +19,7 @@ const isAuthenticated = (req, res, next) => {
const delSession = req.session.destroy((err) => {
if (err) {
res.sendStatus(500);
logger.error("Error while destroying session.", { err });
logger.error("session.destroy", { err });
return;
} else {
res.clearCookie(sessionName);

View File

@ -18,7 +18,7 @@ if (config.use_env_variable) {
(async () => {
try {
await sequelize.authenticate();
logger.info("Sequelize auth success");
logger.debug("Sequelize auth success");
} catch (error) {
logger.error("Sequelize auth error", { error });
throw error;

542
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,10 @@
"description": "Personal Spotify playlist manager",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "cross-env NODE_ENV=development nodemon --exitcrash index.js"
"dev": "cross-env NODE_ENV=development nodemon --delay 2 --exitcrash index.js",
"test_setup": "npm i && cross-env NODE_ENV=test npx sequelize-cli db:migrate",
"test": "cross-env NODE_ENV=test node index.js",
"prod": "NODE_ENV=production node index.js"
},
"repository": {
"type": "git",
@ -18,7 +20,7 @@
},
"homepage": "https://github.com/20kaushik02/spotify-manager#readme",
"dependencies": {
"axios": "^1.7.8",
"axios": "^1.7.9",
"connect-sqlite3": "^0.9.15",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
@ -26,7 +28,7 @@
"express": "^4.21.2",
"express-session": "^1.18.1",
"express-validator": "^7.2.0",
"helmet": "^7.2.0",
"helmet": "^8.0.0",
"sequelize": "^6.37.5",
"pg": "^8.13.1",
"serializr": "^3.0.3",
@ -34,10 +36,10 @@
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.2.0",
"@types/node": "^22.10.2",
"cross-env": "^7.0.3",
"nodemon": "^3.1.4",
"nodemon": "^3.1.9",
"sequelize-cli": "^6.6.2",
"typescript": "^5.5.4"
"typescript": "^5.7.2"
}
}