mirror of
https://github.com/20kaushik02/spotify-manager.git
synced 2025-12-06 07:54:07 +00:00
package checks, healthcheck, lot of cookie fiddling, some logging, bug fixing
This commit is contained in:
parent
d999db53ae
commit
79060661a6
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
})
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
37
index.js
37
index.js
@ -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
|
||||
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(),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
542
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user