Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions frontend/leaderboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ <h1 class="page-title">Leaderboard</h1>
<button
id="load-more-btn"
class="btn btn-secondary"
onclick="loadMoreNodes()"
style="
font-family: &quot;Space Mono&quot;, monospace;
font-size: 0.9rem;
Expand All @@ -141,7 +140,6 @@ <h1 class="page-title">Leaderboard</h1>
<button
id="scroll-top-btn"
class="btn btn-primary"
onclick="window.scrollTo({ top: 0, behavior: 'smooth' })"
style="
font-family: &quot;Space Mono&quot;, monospace;
font-size: 0.9rem;
Expand All @@ -155,7 +153,7 @@ <h1 class="page-title">Leaderboard</h1>
</div>
</main>

<script>
<script nonce="__NONCE__">
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".tab").forEach((tab) => {
tab.addEventListener("click", () => {
Expand Down Expand Up @@ -195,6 +193,18 @@ <h1 class="page-title">Leaderboard</h1>
}
});

// Bind pagination buttons (replaces inline onclick handlers)
document
.getElementById("load-more-btn")
.addEventListener("click", () => {
loadMoreNodes();
});
document
.getElementById("scroll-top-btn")
.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});

fetchLeaderboardData();
});

Expand Down
14 changes: 11 additions & 3 deletions frontend/registration.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ <h1 class="page-title">Join the Leaderboard</h1>
<a href="/leaderboard" class="btn btn-secondary"
>view_leaderboard</a
>
<button class="btn btn-primary" onclick="fullResetForm()">
<button class="btn btn-primary" id="register-another-btn">
register_another
</button>
</div>
Expand Down Expand Up @@ -175,15 +175,15 @@ <h1 class="page-title">Join the Leaderboard</h1>
"
></div>
<div style="display: flex; justify-content: center">
<button class="btn btn-error" onclick="dismissError()">
<button class="btn btn-error" id="dismiss-error-btn">
retry_input
</button>
</div>
</div>
</div>
</main>

<script>
<script nonce="__NONCE__">
function showSuccess(name) {
document.getElementById("registration-form").classList.add("hidden");
document.getElementById("success-message").classList.remove("hidden");
Expand Down Expand Up @@ -520,6 +520,14 @@ <h1 class="page-title">Join the Leaderboard</h1>
registerBtn.style.display = "inline-block";
}
});

// Bind buttons (replaces inline onclick handlers)
document
.getElementById("register-another-btn")
.addEventListener("click", fullResetForm);
document
.getElementById("dismiss-error-btn")
.addEventListener("click", dismissError);
});
</script>
<script src="js/footer.js"></script>
Expand Down
34 changes: 33 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"dependencies": {
"axios": "^1.10.0",
"cors": "^2.8.5",
"express": "^5.1.0"
"express": "^5.1.0",
"helmet": "^8.2.0"
},
"devDependencies": {
"prettier": "^3.8.3"
}
}
90 changes: 85 additions & 5 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,113 @@
const express = require("express");
const helmet = require("helmet");
const cors = require("cors");
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");

const app = express();
const PORT = process.env.PORT || 3000;

// CORS — allow cross-origin requests (used by uptime monitor, etc.)
app.use(cors());
app.use(express.static(path.join(__dirname, "frontend")));

// ---------------------------------------------------------------------------
// 1. Per-request nonce generator (used by CSP and HTML nonce injection)
// ---------------------------------------------------------------------------
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString("base64url");
next();
});

// ---------------------------------------------------------------------------
// 2. Security headers via Helmet
// Sets: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options,
// Referrer-Policy, Strict-Transport-Security, and more.
// ---------------------------------------------------------------------------
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
// Inline scripts need a per-request nonce; external scripts from 'self'
// are allowed automatically.
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
// Allow inline styles (style attributes) + Google Fonts stylesheet
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
// Google Fonts
fontSrc: ["'self'", "https://fonts.gstatic.com"],
// Images: self + data: URIs (used by matrix canvas)
imgSrc: ["'self'", "data:"],
// No plugins
objectSrc: ["'none'"],
// No framing (clickjacking protection)
frameAncestors: ["'none'"],
// Upgrade HTTP to HTTPS when possible
upgradeInsecureRequests: [],
},
},
crossOriginEmbedderPolicy: false, // keep false so Google Fonts load
}),
);

// ---------------------------------------------------------------------------
// 3. Static assets (JS, CSS, images) — served normally
// HTML files are excluded — they go through nonce injection via routes.
// ---------------------------------------------------------------------------
const staticMiddleware = express.static(path.join(__dirname, "frontend"), {
index: false,
});

app.use((req, res, next) => {
if (req.path.endsWith(".html")) return next();
staticMiddleware(req, res, next);
});

// ---------------------------------------------------------------------------
// 4. HTML page routes — inject per-request nonce into __NONCE__ placeholders
// ---------------------------------------------------------------------------
function serveHtml(res, filePath) {
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
return res.status(500).send("Error loading page");
}
const html = data.replace(/__NONCE__/g, res.locals.nonce);
res.type("html").send(html);
});
}

app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "frontend", "index.html"));
serveHtml(res, path.join(__dirname, "frontend", "index.html"));
});

app.get("/leaderboard", (req, res) => {
res.sendFile(path.join(__dirname, "frontend", "leaderboard.html"));
serveHtml(res, path.join(__dirname, "frontend", "leaderboard.html"));
});

app.get("/about", (req, res) => {
res.sendFile(path.join(__dirname, "frontend", "about.html"));
serveHtml(res, path.join(__dirname, "frontend", "about.html"));
});

app.get("/registration", (req, res) => {
res.sendFile(path.join(__dirname, "frontend", "registration.html"));
serveHtml(res, path.join(__dirname, "frontend", "registration.html"));
});

// Redirect direct .html file access so nonce injection still applies
app.get(/\.html$/, (req, res) => {
const cleanPath = req.path.replace(/\.html$/, "");
res.redirect(301, cleanPath || "/");
});

// ---------------------------------------------------------------------------
// 5. Utility endpoints
// ---------------------------------------------------------------------------
app.get("/uptime", (req, res) => {
res.json({ status: "Website is running ✅" });
});

// ---------------------------------------------------------------------------
// 6. 404
// ---------------------------------------------------------------------------
app.use((req, res) => {
res.status(404).send("Page not found");
});
Expand Down