From 8355f1c651dcff4c5cab65686811949ed09feb81 Mon Sep 17 00:00:00 2001 From: Yashaswini K P Date: Sat, 15 Aug 2026 20:08:18 +0530 Subject: [PATCH 1/2] refactor: decouple username parsing from URL path using backend meta tag injection --- frontend/js/user/profile-controller.js | 17 +++++-------- frontend/js/user/utils.js | 10 ++++++++ frontend/user.html | 1 + server.js | 35 ++++++++++++++++---------- 4 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 frontend/js/user/utils.js diff --git a/frontend/js/user/profile-controller.js b/frontend/js/user/profile-controller.js index 8f229e4f..5b2e63bc 100644 --- a/frontend/js/user/profile-controller.js +++ b/frontend/js/user/profile-controller.js @@ -4,19 +4,14 @@ import { loadGoalSetter } from "./goal-setter.js"; import { fetchUserData } from "./historical-graphs.js"; import { loadLeaderboardRanks } from "./ranks.js"; import { loadStreakData } from "./streak.js"; - -function getUsername() { - const pathSegments = window.location.pathname.split("/"); - return ( - pathSegments[pathSegments.length - 1] || - pathSegments[pathSegments.length - 2] || - "" - ); -} +import { getCurrentUsername } from "./utils.js"; async function initProfile() { - const username = getUsername(); - if (!username) return; + const username = getCurrentUsername(); + if (!username) { + console.error("Critical error: Username meta tag is missing."); + return; + } // Set header display variables prior to fetch if elements exist const usernameHeading = document.getElementById("username-display"); diff --git a/frontend/js/user/utils.js b/frontend/js/user/utils.js new file mode 100644 index 00000000..1fe4b7ac --- /dev/null +++ b/frontend/js/user/utils.js @@ -0,0 +1,10 @@ +export function getCurrentUsername() { + const metaTag = document.querySelector('meta[name="current-user"]'); + + if (!metaTag || !metaTag.content) { + console.warn("Current user meta tag is missing or empty."); + return null; + } + + return metaTag.content.trim(); +} diff --git a/frontend/user.html b/frontend/user.html index fa8d49b9..dd642ceb 100644 --- a/frontend/user.html +++ b/frontend/user.html @@ -2,6 +2,7 @@ + CodePVG diff --git a/server.js b/server.js index 302324fa..4e5f6996 100644 --- a/server.js +++ b/server.js @@ -80,20 +80,26 @@ app.use((req, res, next) => { // 4. HTML page routes — inject per-request nonce into __NONCE__ placeholders const htmlCache = {}; -function serveHtml(res, filePath) { - if (htmlCache[filePath]) { - const html = htmlCache[filePath].replace(/__NONCE__/g, res.locals.nonce); - return res.type("html").send(html); - } - - fs.readFile(filePath, "utf8", (err, data) => { - if (err) { +function serveHtml(res, filePath, replacements = {}) { + let template = htmlCache[filePath]; + + if (!template) { + try { + template = fs.readFileSync(filePath, "utf8"); + htmlCache[filePath] = template; + } catch (err) { return res.status(500).send("Error loading page"); } - htmlCache[filePath] = data; - const html = data.replace(/__NONCE__/g, res.locals.nonce); - res.type("html").send(html); - }); + } + + let html = template.replace(/__NONCE__/g, res.locals.nonce); + + for (const [key, value] of Object.entries(replacements)) { + const regex = new RegExp(key, "g"); + html = html.replace(regex, value); + } + + res.type("html").send(html); } /* HOME ROUTES */ @@ -133,7 +139,10 @@ app.get("/uptime", (req, res) => { }); app.get("/user/:username", (req, res) => { - serveHtml(res, path.join(__dirname, "frontend", "user.html")); + const username = req.params.username; + serveHtml(res, path.join(__dirname, "frontend", "user.html"), { + __USERNAME__: username, + }); }); // ---- Rate limiter for API endpoint ---- From c1c91962ac23f5be6bfd25b1c0c7417252fe2dd8 Mon Sep 17 00:00:00 2001 From: Yashaswini K P Date: Sat, 22 Aug 2026 11:55:41 +0530 Subject: [PATCH 2/2] feat: add user profile page and make leaderboard names clickable --- frontend/js/leaderboard/render.js | 26 ++++++++++++++++++-------- frontend/styles/main.css | 12 ++++++++++++ server.js | 7 +++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/frontend/js/leaderboard/render.js b/frontend/js/leaderboard/render.js index 2dbbdf45..b56a8000 100644 --- a/frontend/js/leaderboard/render.js +++ b/frontend/js/leaderboard/render.js @@ -155,16 +155,17 @@ function renderLeaderboardRow(user, rank) { if (rankTagEl) { nameDiv.appendChild(rankTagEl); } - const nameTextWrapper = document.createElement("span"); - nameTextWrapper.className = "name-text"; - nameTextWrapper.appendChild(document.createTextNode(user.name)); + const nameLink = document.createElement("a"); + nameLink.href = `/user/${encodeURIComponent(user.id)}`; + nameLink.className = "name-text leaderboard-user-link"; + nameLink.textContent = user.name; if (rankChangeEl) { - nameTextWrapper.appendChild(document.createTextNode(" ")); - nameTextWrapper.appendChild(rankChangeEl); + nameLink.appendChild(document.createTextNode(" ")); + nameLink.appendChild(rankChangeEl); } - nameDiv.appendChild(nameTextWrapper); + nameDiv.appendChild(nameLink); row.appendChild(nameDiv); // Username with link and external icon — id is user-controlled (textContent) @@ -314,13 +315,22 @@ function renderMobileCard(user, rank) { const mobileRankTagEl = createRankTagElement(rank); const mobileRankChangeEl = user.score > 0 ? createRankChangeElement(user.rankChange) : null; + if (mobileRankTagEl) { mobileName.appendChild(mobileRankTagEl); } - mobileName.appendChild(document.createTextNode(user.name)); + + const mobileNameLink = document.createElement("a"); + mobileNameLink.href = `/user/${encodeURIComponent(user.id)}`; + mobileNameLink.className = "leaderboard-user-link"; + mobileNameLink.textContent = user.name; + if (mobileRankChangeEl) { - mobileName.appendChild(mobileRankChangeEl); + mobileNameLink.appendChild(document.createTextNode(" ")); + mobileNameLink.appendChild(mobileRankChangeEl); } + + mobileName.appendChild(mobileNameLink); card.appendChild(mobileName); // Username — id is user-controlled (textContent) diff --git a/frontend/styles/main.css b/frontend/styles/main.css index da05b7db..cbf51567 100644 --- a/frontend/styles/main.css +++ b/frontend/styles/main.css @@ -3226,6 +3226,18 @@ input[type="checkbox"].compare-checkbox:checked::after { pointer-events: auto; } +.leaderboard-user-link { + color: inherit; + text-decoration: none; +} + +.leaderboard-user-link:hover, +.leaderboard-user-link:visited, +.leaderboard-user-link:active { + color: inherit; + text-decoration: none; +} + @keyframes compareSlideInRight { from { transform: translateX(120%); diff --git a/server.js b/server.js index 4e5f6996..26f47659 100644 --- a/server.js +++ b/server.js @@ -251,6 +251,13 @@ app.get("/api/user/:username", async (req, res) => { } }); +app.get("/user/:username", (req, res) => { + const username = req.params.username; + serveHtml(res, path.join(__dirname, "frontend", "user.html"), { + __USERNAME__: username, + }); +}); + // 404 handler app.use((req, res) => { res.status(404);