From c79ee2b203aa5b9a3e91fddde31da8273033a276 Mon Sep 17 00:00:00 2001 From: creations Date: Sun, 6 Apr 2025 20:59:38 -0400 Subject: [PATCH] fix xss issue aka: https://git.creations.works/creations/profilePage/issues/3, update depends change how activities display, remove readme title, --- package.json | 9 +-- public/css/index.css | 97 ++++++++++++++++++------- public/js/index.js | 157 +++++++++++++++++++++++++++-------------- src/helpers/lanyard.ts | 6 +- src/server.ts | 10 +-- src/views/index.ejs | 118 +++++++++++++++++++++---------- 6 files changed, 270 insertions(+), 127 deletions(-) diff --git a/package.json b/package.json index 7d18b41..8d6e543 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-promise": "^7.2.1", "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-unicorn": "^58.0.0", "eslint-plugin-unused-imports": "^4.1.4", - "globals": "^15.15.0", + "globals": "^16.0.0", "prettier": "^3.5.3" }, "peerDependencies": { @@ -29,7 +29,8 @@ }, "dependencies": { "ejs": "^3.1.10", - "node-vibrant": "^4.0.3", - "marked": "^15.0.7" + "isomorphic-dompurify": "^2.23.0", + "marked": "^15.0.7", + "node-vibrant": "^4.0.3" } } diff --git a/public/css/index.css b/public/css/index.css index da470b9..e4acc03 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -142,18 +142,28 @@ ul { display: flex; flex-direction: row; gap: 1rem; - background: #1a1a1d; - padding: 1rem; - border-radius: 6px; - box-shadow: 0 0 0 1px #2e2e30; - transition: background 0.2s ease; - align-items: flex-start; + background-color: #1e1f22; + padding: 0.75rem 1rem; + border-radius: 10px; + box-shadow: 0 1px 0 0 #2e2e30; } .activity:hover { background: #2a2a2d; } +.activity-wrapper { + display: flex; + flex-direction: column; + width: 100%; +} + +.activity-wrapper-inner { + display: flex; + flex-direction: row; + gap: 1rem; +} + .activity-art { width: 80px; height: 80px; @@ -165,7 +175,15 @@ ul { .activity-content { display: flex; flex-direction: column; + justify-content: space-between; flex: 1; + gap: 0.5rem; + position: relative; +} + +.activity-top { + display: flex; + flex-direction: column; gap: 0.25rem; } @@ -175,36 +193,49 @@ ul { align-items: flex-start; } +.activity-bottom { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + .activity-name { - font-weight: bold; - font-size: 1.1rem; - color: #ffffff; + font-weight: 600; + font-size: 1rem; + color: #fff; } .activity-detail { - font-size: 0.95rem; - color: #ccc; + font-size: 0.875rem; + color: #b5bac1; } .activity-timestamp { - font-size: 0.8rem; - color: #777; + font-size: 0.75rem; + color: #b5bac1; text-align: right; + margin-left: auto; + white-space: nowrap; } .progress-bar { - height: 6px; - background-color: #333; - border-radius: 3px; - overflow: hidden; - width: 100%; + height: 4px; + background-color: #2e2e30; + border-radius: 2px; margin-top: 0.5rem; + overflow: hidden; } .progress-fill { + background-color: #5865f2; + transition: width 0.4s ease; height: 100%; - background-color: #00b0f4; - transition: width 0.5s ease; +} + +.progress-bar, +.progress-time-labels { + width: 100%; + margin-top: 0.5rem; } .progress-time-labels { @@ -215,6 +246,21 @@ ul { margin-top: 0.25rem; } +.activity-type-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.activity-type-label { + font-size: 0.75rem; + text-transform: uppercase; + font-weight: 600; + color: #aaa; + margin-bottom: 0.50rem; + display: block; +} + .activity-header.no-timestamp { justify-content: flex-start; } @@ -226,9 +272,9 @@ ul { .activity-buttons { display: flex; - flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; + justify-content: flex-end; } .activity-button { @@ -249,10 +295,9 @@ ul { text-decoration: none; } -.activity-button.disabled { - background-color: #4e5058; - cursor: default; - pointer-events: none; +.activity-button:disabled { + background-color: #2d2e31; + cursor: not-allowed; opacity: 0.8; } @@ -392,6 +437,8 @@ ul { border-radius: 8px; box-shadow: 0 0 0 1px #2e2e30; + margin-top: 2rem; + box-sizing: border-box; overflow: hidden; overflow-y: auto; diff --git a/public/js/index.js b/public/js/index.js index 6feec74..8b40b41 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -4,9 +4,20 @@ const activityProgressMap = new Map(); function formatTime(ms) { const totalSecs = Math.floor(ms / 1000); - const mins = Math.floor(totalSecs / 60); + const hours = Math.floor(totalSecs / 3600); + const mins = Math.floor((totalSecs % 3600) / 60); const secs = totalSecs % 60; - return `${mins}:${secs.toString().padStart(2, "0")}`; + + return `${String(hours).padStart(1, "0")}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; +} + +function formatVerbose(ms) { + const totalSecs = Math.floor(ms / 1000); + const hours = Math.floor(totalSecs / 3600); + const mins = Math.floor((totalSecs % 3600) / 60); + const secs = totalSecs % 60; + + return `${hours}h ${mins}m ${secs}s`; } function updateElapsedAndProgress() { @@ -17,17 +28,14 @@ function updateElapsedAndProgress() { if (!start) return; const elapsed = now - start; - const mins = Math.floor(elapsed / 60000); - const secs = Math.floor((elapsed % 60000) / 1000); const display = el.querySelector(".elapsed"); - if (display) - display.textContent = `(${mins}m ${secs.toString().padStart(2, "0")}s ago)`; + if (display) display.textContent = `(${formatVerbose(elapsed)} ago)`; }); document.querySelectorAll(".progress-bar").forEach((bar) => { const start = Number(bar.dataset.start); const end = Number(bar.dataset.end); - if (!start || !end || end <= start) return; + if (!start || !end || end <= start || now > end) return; const duration = end - start; const elapsed = now - start; @@ -43,7 +51,7 @@ function updateElapsedAndProgress() { document.querySelectorAll(".progress-time-labels").forEach((label) => { const start = Number(label.dataset.start); const end = Number(label.dataset.end); - if (!start || !end || end <= start) return; + if (!start || !end || end <= start || now > end) return; const current = Math.max(0, now - start); const total = end - start; @@ -102,7 +110,7 @@ if (userId && instanceUri) { if (payload.t === "INIT_STATE" || payload.t === "PRESENCE_UPDATE") { updatePresence(payload.d); - updateElapsedAndProgress(); + requestAnimationFrame(() => updateElapsedAndProgress()); } }); } @@ -129,64 +137,98 @@ function buildActivityHTML(activity) { art = `https://cdn.discordapp.com/app-assets/${activity.application_id}/${img}.png`; } + const activityTypeMap = { + 0: "Playing", + 1: "Streaming", + 2: "Listening", + 3: "Watching", + 4: "Custom Status", + 5: "Competing", + }; + + const activityType = + activity.name === "Spotify" + ? "Listening to Spotify" + : activity.name === "TIDAL" + ? "Listening to TIDAL" + : activityTypeMap[activity.type] || "Playing"; + const activityTimestamp = - !total && start - ? ` -
- - Since: ${new Date(start).toLocaleTimeString("en-GB", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - })} - -
` + start && progress === null + ? `
+ Since: ${new Date(start).toLocaleTimeString("en-GB", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + })} +
` + : ""; + + const activityButtons = + activity.buttons && activity.buttons.length > 0 + ? `
+ ${activity.buttons + .map((button, index) => { + const label = + typeof button === "string" + ? button + : button.label; + let url = null; + if (typeof button === "object" && button.url) { + url = button.url; + } else if (index === 0 && activity.url) { + url = activity.url; + } + return url + ? `${label}` + : null; + }) + .filter(Boolean) + .join("")} +
` : ""; const progressBar = progress !== null - ? ` -
+ ? `
${formatTime(elapsed)} ${formatTime(total)} -
- ` +
` : ""; - const activityButtons = activity.buttons && activity.buttons.length > 0 - ? `
- ${activity.buttons.map((button, index) => { - const buttonLabel = typeof button === 'string' ? button : button.label; - let buttonUrl = null; - if (typeof button === 'object' && button.url) { - buttonUrl = button.url; - } - else if (index === 0 && activity.url) { - buttonUrl = activity.url; - } - if (buttonUrl) { - return `${buttonLabel}`; - } else { - return `${buttonLabel}`; - } - }).join('')} -
` - : ''; + const isMusic = activity.type === 2 || activity.type === 3; + + const primaryLine = isMusic ? activity.details : activity.name; + const secondaryLine = isMusic ? activity.state : activity.details; + const tertiaryLine = isMusic ? activity.assets?.large_text : activity.state; return `
  • - ${art ? `Art` : ""} -
    -
    - ${activity.name} +
    +
    + ${activityType} ${activityTimestamp}
    - ${activity.details ? `
    ${activity.details}
    ` : ""} - ${activity.state ? `
    ${activity.state}
    ` : ""} - ${activityButtons} +
    + ${art ? `Art` : ""} +
    +
    +
    +
    + ${primaryLine} +
    + ${secondaryLine ? `
    ${secondaryLine}
    ` : ""} + ${tertiaryLine ? `
    ${tertiaryLine}
    ` : ""} +
    +
    + ${activityButtons} +
    +
    +
    +
    ${progressBar}
  • @@ -234,10 +276,21 @@ function updatePresence(data) { } else if (emoji?.name) { emojiHTML = `${emoji.name} `; } - customStatus.innerHTML = `${emojiHTML}${custom.state}`; + customStatus.innerHTML = ` + ${emojiHTML} + ${custom.state ? `${custom.state}` : ""} + `; } - const filtered = data.activities?.filter((a) => a.type !== 4); + const filtered = data.activities + ?.filter((a) => a.type !== 4) + ?.sort((a, b) => { + const priority = { 2: 0, 1: 1, 3: 2 }; // Listening, Streaming, Watching ? should i keep this + const aPriority = priority[a.type] ?? 99; + const bPriority = priority[b.type] ?? 99; + return aPriority - bPriority; + }); + const activityList = document.querySelector(".activities"); if (activityList) { diff --git a/src/helpers/lanyard.ts b/src/helpers/lanyard.ts index f226f91..258b980 100644 --- a/src/helpers/lanyard.ts +++ b/src/helpers/lanyard.ts @@ -1,5 +1,6 @@ import { lanyardConfig } from "@config/environment"; import { fetch } from "bun"; +import DOMPurify from "isomorphic-dompurify"; import { marked } from "marked"; export async function getLanyardData(id?: string): Promise { @@ -85,7 +86,10 @@ export async function handleReadMe(data: LanyardData): Promise { const text: string = await res.text(); if (!text || text.length < 10) return null; - return marked.parse(text); + const html: string | null = await marked.parse(text); + const safe: string | null = DOMPurify.sanitize(html); + + return safe; } catch { return null; } diff --git a/src/server.ts b/src/server.ts index 13ae2ba..2298034 100644 --- a/src/server.ts +++ b/src/server.ts @@ -34,22 +34,16 @@ class ServerHandler { open: webSocketHandler.handleOpen.bind(webSocketHandler), message: webSocketHandler.handleMessage.bind(webSocketHandler), close: webSocketHandler.handleClose.bind(webSocketHandler), - error(error) { - logger.error(`Server error: ${error.message}`); - return new Response(`Server Error: ${error.message}`, { - status: 500, - }); - }, }, }); - const accessUrls = [ + const accessUrls: string[] = [ `http://${server.hostname}:${server.port}`, `http://localhost:${server.port}`, `http://127.0.0.1:${server.port}`, ]; - logger.info(`Server running at ${accessUrls[0]}`, true); + logger.info(`Server running at ${accessUrls[0]}`); logger.info(`Access via: ${accessUrls[1]} or ${accessUrls[2]}`, true); this.logRoutes(); diff --git a/src/views/index.ejs b/src/views/index.ejs index 6790bb5..9b8dc31 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -46,14 +46,25 @@ <% } else if (emoji?.name) { %> <%= emoji.name %> <% } %> - <%= activities[0].state %> + <% if (activities[0].state) { %> + <%= activities[0].state %> + <% } %>

    <% } %> - <% const filtered = activities.filter(a => a.type !== 4); %> + <% + let filtered = activities + .filter(a => a.type !== 4) + .sort((a, b) => { + const priority = { 2: 0, 1: 1, 3: 2 }; // Listening, Streaming, Watching ? should i keep this + const aPriority = priority[a.type] ?? 99; + const bPriority = priority[b.type] ?? 99; + return aPriority - bPriority; + }); + %> <% if (filtered.length > 0) { %>

    Activities

      @@ -75,16 +86,28 @@ } else if (img) { art = `https://cdn.discordapp.com/app-assets/${activity.application_id}/${img}.png`; } + + const activityTypeMap = { + 0: "Playing", + 1: "Streaming", + 2: "Listening", + 3: "Watching", + 4: "Custom Status", + 5: "Competing", + }; + + const activityType = activity.name === "Spotify" + ? "Listening to Spotify" + : activity.name === "TIDAL" + ? "Listening to TIDAL" + : activityTypeMap[activity.type] || "Playing"; %>
    • - <% if (art) { %> - Art - <% } %> - -
      -
      - <%= activity.name %> - +
      +
      + + <%= activityType %> + <% if (start && progress === null) { %>
      <% const started = new Date(start); %> @@ -95,33 +118,54 @@ <% } %>
      - <% if (activity.details) { %> -
      <%= activity.details %>
      - <% } %> - <% if (activity.state) { %> -
      <%= activity.state %>
      - <% } %> +
      + <% if (art) { %> + Art + <% } %> - <% if (activity.buttons && activity.buttons.length > 0) { %> -
      - <% activity.buttons.forEach((button, index) => { - const buttonLabel = typeof button === 'string' ? button : button.label; - let buttonUrl = null; - if (typeof button === 'object' && button.url) { - buttonUrl = button.url; - } - else if (index === 0 && activity.url) { - buttonUrl = activity.url; - } - %> - <% if (buttonUrl) { %> - <%= buttonLabel %> - <% } else { %> - <%= buttonLabel %> - <% } %> - <% }); %> +
      +
      + <% + const isMusic = activity.type === 2 || activity.type === 3; + const primaryLine = isMusic ? activity.details : activity.name; + const secondaryLine = isMusic ? activity.state : activity.details; + const tertiaryLine = isMusic ? activity.assets?.large_text : activity.state; + %> +
      +
      + <%= primaryLine %> +
      + + <% if (secondaryLine) { %> +
      <%= secondaryLine %>
      + <% } %> + <% if (tertiaryLine) { %> +
      <%= tertiaryLine %>
      + <% } %> +
      +
      + <% if (activity.buttons && activity.buttons.length > 0) { %> +
      + <% activity.buttons.forEach((button, index) => { + const buttonLabel = typeof button === 'string' ? button : button.label; + let buttonUrl = null; + if (typeof button === 'object' && button.url) { + buttonUrl = button.url; + } + else if (index === 0 && activity.url) { + buttonUrl = activity.url; + } + %> + <% if (buttonUrl) { %> + <%= buttonLabel %> + <% } %> + <% }); %> +
      + <% } %> +
      +
      - <% } %> +
      <% if (progress !== null) { %>
      @@ -130,7 +174,7 @@ <% if (start && end) { %>
      - --:-- + <%= Math.floor((end - start) / 60000) %>:<%= String(Math.floor(((end - start) % 60000) / 1000)).padStart(2, "0") %>
      <% } %> @@ -140,8 +184,8 @@ <% }) %>
    <% } %> + <% if (readme) { %> -

    Readme

    <%- readme %>