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
46 changes: 46 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { GAME_SERVER_PORT } from "./game/udp";
import { sscRouter } from "./ssc/routes";
import { getCurrentCRC, LoadConfig, MATCHMAKING_CRC } from "./data/config";
import { PlayerTester, PlayerTesterModel } from "./database/PlayerTester";
import { PlayerStatsModel } from "./database/PlayerStats";
import { Types } from "mongoose";
import { RegExpMatcher, TextCensor, englishDataset, englishRecommendedTransformers, asteriskCensorStrategy } from "obscenity";
import env from "./env/env";
Expand Down Expand Up @@ -239,6 +240,10 @@ const homeFilePath = path.join(__dirname, "static/home.html");
const homeSource = fs.readFileSync(homeFilePath, "utf8");
const homeTemplate = handlebars.compile(homeSource);

const myStatsFilePath = path.join(__dirname, "static/my_stats.html");
const myStatsSource = fs.readFileSync(myStatsFilePath, "utf8");
const myStatsTemplate = handlebars.compile(myStatsSource);

const adminBannerFilePath = path.join(__dirname, "static/admin.html");
const adminBannerSource = fs.readFileSync(adminBannerFilePath, "utf8");
const adminBannerTemplate = handlebars.compile(adminBannerSource);
Expand All @@ -253,6 +258,47 @@ app.get("/home", async (req, res) => {
}
});

// ── /stats — career-stats page for the caller's IP-resolved account ──
// Mirrors the /namechange flow: caller's IP → resolvePlayerForWeb → PlayerStats.
// If multiple accounts at the IP, the account picker is shown.
// Public, no auth.
app.get("/stats", async (req, res) => {
try {
const { player, pickerShown } = await resolvePlayerForWeb(req, res, "/stats");
if (pickerShown) return;

if (!player) {
const html = myStatsTemplate({ hasStats: false });
res.send(html);
return;
}

const accountId = String((player as any)._id);
const stats = await PlayerStatsModel.findOne({ account_id: accountId }).lean();

if (!stats || !stats.aggregate || Object.keys(stats.aggregate).length === 0) {
const html = myStatsTemplate({ hasStats: false, playerName: (player as any).name || "" });
res.send(html);
return;
}

// Stringify the aggregate as JSON for the inline <script> block. Escape `</`
// so the JSON can't close the script tag if a field name ever contains it.
const aggregateJson = JSON.stringify(stats.aggregate).replace(/<\//g, "<\\/");

const html = myStatsTemplate({
hasStats: true,
playerName: (player as any).name || "Unknown",
aggregateJson,
updatedAt: Number(stats.updated_at) || Date.now(),
});
res.send(html);
} catch (e) {
logger.error(`${logPrefix} Error in GET /stats: ${e}`);
res.status(500).send("Error loading stats page");
}
});

app.get("/namechange", async (req, res) => {
try {
const ip = tryGetRealIP(req).replace(/^::ffff:/, ""); // Ensure we get the real IP for name changes, not just the direct connection IP
Expand Down
4 changes: 4 additions & 0 deletions src/static/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ <h3>Leaderboard</h3>
<h3>Live Matches</h3>
<p>Watch ranked matches in progress with live scores.</p>
</a>
<a href="/stats" class="link-card orange">
<h3>My Stats</h3>
<p>Your career move usage, accuracy, parries, and ringouts.</p>
</a>
</div>
</div>
</div>
Expand Down
Loading