feat(backend, frontend): fix stats from singleplayer, fix admin men.

This commit is contained in:
Aleksander Wilczyński 2024-12-16 19:53:03 +01:00
parent e2732c2008
commit 687fa01983
Signed by untrusted user: alekswilc
GPG Key ID: D4464A248E5F27FE
16 changed files with 171 additions and 69 deletions

View File

@ -59,9 +59,9 @@ export class AdminRoute
);
});
app.post("/profile/:playerId/clear", async (req, res) =>
app.post("/profile/:playerId/hideLeaderboard", async (req, res) =>
{
const token = req.headers["x-auth-token"];
const token = req.headers[ "x-auth-token" ];
if (!token)
{
@ -93,14 +93,58 @@ export class AdminRoute
return;
}
await MProfile.updateOne({id: player.id}, {
dispatcherTime: 0,
trainTime: 0,
trainDistance: 0,
trainPoints: 0,
player.flags.push("leaderboard_hidden");
trainStats: {},
dispatcherStats: {},
await MProfile.updateOne({ id: player.id }, {
flags: player.flags,
});
res.json(
new SuccessResponseBuilder()
.setCode(200)
.setData({})
.toJSON(),
);
});
app.post("/profile/:playerId/showLeaderboard", async (req, res) =>
{
const token = req.headers[ "x-auth-token" ];
if (!token)
{
res.status(400).json(new ErrorResponseBuilder()
.setCode(400)
.setData("Missing token query").toJSON());
return;
}
const admin = await MAdmin.findOne({ token });
if (!admin)
{
res.status(401).json(new ErrorResponseBuilder()
.setCode(401)
.setData("Invalid token").toJSON());
return;
}
const player = await MProfile.findOne({
id: req.params.playerId,
});
if (!player)
{
res.status(401).json(new ErrorResponseBuilder()
.setCode(401)
.setData("Invalid playerId").toJSON());
return;
}
player.flags = player.flags.filter(x => x !== "leaderboard_hidden");
await MProfile.updateOne({ id: player.id }, {
flags: player.flags,
});
res.json(
@ -113,7 +157,7 @@ export class AdminRoute
app.post("/profile/:playerId/hide", async (req, res) =>
{
const token = req.headers["x-auth-token"];
const token = req.headers[ "x-auth-token" ];
if (!token)
{
@ -147,7 +191,7 @@ export class AdminRoute
player.flags.push("hidden");
await MProfile.updateOne({id: player.id}, {
await MProfile.updateOne({ id: player.id }, {
flags: player.flags,
});

View File

@ -48,7 +48,7 @@ export class LeaderboardRoute
const filter: PipelineStage[] = [
{
$match: {
flags: { $nin: ["hidden"] }
flags: { $nin: ["hidden", "leaderboard_hidden"] }
}
}
];
@ -83,7 +83,7 @@ export class LeaderboardRoute
const filter: PipelineStage[] = [
{
$match: {
flags: { $nin: ["hidden"] }
flags: { $nin: ["hidden", "leaderboard_hidden"] }
}
}
];

View File

@ -85,6 +85,23 @@ export class StationsModule
player.steamTrainScore = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
if ((player.steamTrainDistance > player.trainDistance) || (player.trainPoints > player.steamTrainScore))
{
player.trainStats[ "N/A" ] = {
time: 0, distance: player.steamTrainDistance > player.trainDistance ? player.steamTrainDistance - player.trainDistance : player.trainDistance,
score: player.trainPoints > player.steamTrainScore ? player.steamTrainScore - player.trainPoints : player.trainPoints,
};
if (player.steamTrainDistance > player.trainDistance)
{
player.trainDistance = player.steamTrainDistance;
}
if (player.trainPoints > player.steamTrainScore)
{
player.trainPoints = player.steamTrainScore;
}
}
player.flags = player.flags.filter(x => x !== "private");
}

View File

@ -114,6 +114,23 @@ export class TrainsModule
player.steamDispatcherTime = stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0;
player.steamTrainScore = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
if ((player.steamTrainDistance > player.trainDistance) || (player.trainPoints > player.steamTrainScore))
{
player.trainStats[ "N/A" ] = {
time: 0, distance: player.steamTrainDistance > player.trainDistance ? player.steamTrainDistance - player.trainDistance : player.trainDistance,
score: player.trainPoints > player.steamTrainScore ? player.steamTrainScore - player.trainPoints : player.trainPoints,
};
if (player.steamTrainDistance > player.trainDistance)
{
player.trainDistance = player.steamTrainDistance;
}
if (player.trainPoints > player.steamTrainScore)
{
player.trainPoints = player.steamTrainScore;
}
}
player.flags = player.flags.filter(x => x !== "private");
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* See LICENSE for more.
*/
import { FaUserShield, FaUserSlash, FaUserLock } from "react-icons/fa6";
export const UserIcons = ({ flags }: { flags: string[] }) =>
{
return <> { flags.includes("administrator") &&
<FaUserShield className={ "inline text-meta-1 ml-1" }/> } { flags.includes("leaderboard_hidden") &&
<FaUserLock className={ "inline text-meta-6 ml-1" }/> } { flags.includes("hidden") &&
<FaUserSlash className={ "inline text-meta-1 ml-1" }/> }</>;
};

View File

@ -16,8 +16,8 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { FaCheck } from "react-icons/fa6";
import { TActiveStationPlayersData } from "../../../types/active.ts";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
export const ActiveStationTable = ({ stations }: { stations: TActiveStationPlayersData[] }) =>
{
@ -65,8 +65,8 @@ export const ActiveStationTable = ({ stations }: { stations: TActiveStationPlaye
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
<Link to={ "/profile/" + station.steam }
className="color-orchid">{ station.username }</Link> { station.player.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
className="color-orchid">{ station.username }</Link> <UserIcons
flags={ station.player.flags }/>
</p>
</div>

View File

@ -16,8 +16,9 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { FaCheck } from "react-icons/fa6";
import { TActiveTrainPlayersData } from "../../../types/active.ts";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
export const ActiveTrainTable = ({ trains }: {
trains: TActiveTrainPlayersData[],
}) =>
@ -70,8 +71,8 @@ export const ActiveTrainTable = ({ trains }: {
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
<Link to={ "/profile/" + train.steam }
className="color-orchid">{ train.username }</Link> { train.player.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
className="color-orchid">{ train.username }</Link> <UserIcons
flags={ train.player.flags }/>
</p>
</div>

View File

@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
import { formatTime } from "../../../util/time.ts";
import { FaCheck } from "react-icons/fa6";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
export const StationTable = ({ stations }: { stations: TLeaderboardRecord[] }) =>
{
@ -58,8 +58,8 @@ export const StationTable = ({ stations }: { stations: TLeaderboardRecord[] }) =
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
<Link to={ "/profile/" + station.id }
className="color-orchid">{ station.username }</Link> { station.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
className="color-orchid">{ station.username }</Link> <UserIcons flags={station.flags} />
</p>
</div>

View File

@ -18,9 +18,9 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
import { formatTime } from "../../../util/time.ts";
import { FaCheck } from "react-icons/fa6";
import { Dispatch, SetStateAction } from "react";
import { FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
export const TrainTable = ({ trains, setSortBy, sortBy }: {
trains: TLeaderboardRecord[],
@ -80,8 +80,7 @@ export const TrainTable = ({ trains, setSortBy, sortBy }: {
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
<Link to={ "/profile/" + train.id }
className="color-orchid">{ train.username }</Link> { train.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
className="color-orchid">{ train.username }</Link> <UserIcons flags={train.flags} />
</p>
</div>

View File

@ -19,7 +19,8 @@ import { TLogStationData } from "../../../types/log.ts";
import dayjs from "dayjs";
import { toast } from "react-toastify";
import { Link } from "react-router-dom";
import { FaCheck } from 'react-icons/fa6';
import { UserIcons } from "../../mini/util/UserIcons.tsx";
export const StationLog = ({ data }: { data: TLogStationData }) =>
{
@ -50,7 +51,7 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
</div>
<div className="mt-4">
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
{ data.player.username }{ data.player.flags.includes('verified') && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
{ data.player.username } <UserIcons flags={data.player.flags} />
</h3>
</div>
</div>

View File

@ -19,7 +19,7 @@ import { TLogTrainData } from "../../../types/log.ts";
import dayjs from "dayjs";
import { toast } from "react-toastify";
import { Link } from "react-router-dom";
import { FaCheck } from "react-icons/fa6";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
export const TrainLog = ({ data }: { data: TLogTrainData }) =>
@ -51,8 +51,7 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) =>
</div>
<div className="mt-4">
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
{ data.player.username } { data.player.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
{ data.player.username } <UserIcons flags={data.player.flags} />
</h3>
</div>
</div>

View File

@ -17,11 +17,8 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import dayjs from "dayjs";
import { TStationRecord } from "../../../types/station.ts";
import { FaCheck } from "react-icons/fa6";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
// setSearchItem: Dispatch<SetStateAction<string>>
export const StationTable = ({ stations }: {
@ -68,8 +65,7 @@ export const StationTable = ({ stations }: {
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
<Link to={ "/profile/" + (station.steam ?? station.player.id) }
className="color-orchid">{ station.username ?? station.player.username }</Link> { station.player.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
className="color-orchid">{ station.username ?? station.player.username }</Link> <UserIcons flags={station.player.flags} />
</p>
</div>

View File

@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TTrainRecord } from "../../../types/train.ts";
import dayjs from "dayjs";
import { FaCheck } from "react-icons/fa6";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
// setSearchItem: Dispatch<SetStateAction<string>>
export const TrainTable = ({ trains }: {
@ -75,8 +75,7 @@ export const TrainTable = ({ trains }: {
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
<Link to={ "/profile/" + (train.steam ?? train.player.id) }
className="color-orchid">{ train.username ?? train.player.username }</Link> { train.player.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
className="color-orchid">{ train.username ?? train.player.username }</Link> <UserIcons flags={train.player.flags} />
</p>
</div>

View File

@ -19,12 +19,12 @@ import { TProfileData } from "../../../types/profile.ts";
import { useTranslation } from "react-i18next";
import { ArrowIcon, FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
import { formatTime } from "../../../util/time.ts";
import { FaCheck } from "react-icons/fa6";
import { useAuth } from "../../../hooks/useAuth.tsx";
import { ConfirmModal } from "../../mini/modal/ConfirmModal.tsx";
import { post } from "../../../util/fetcher.ts";
import { toast } from "react-toastify";
import dayjs from 'dayjs';
import dayjs from "dayjs";
import { UserIcons } from "../../mini/util/UserIcons.tsx";
export const ProfileCard = ({ data }: { data: TProfileData }) =>
{
@ -32,19 +32,19 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
const [ showTrains, setShowTrains ] = useState(false);
const [ showStations, setShowStations ] = useState(false);
const [ sortTrainsBy, setSortTrainsBy ] = useState<"time" | "score" | "distance">("distance");
const [ clearStatsModal, setClearStatsModal ] = useState(false);
const [ hideLeaderboardStatsModal, setHideLeaderboardStatsModal ] = useState(false);
const [ hideProfileModal, setHideProfileModal ] = useState(false);
const { isAdmin, token } = useAuth();
const adminClearPlayerStats = () =>
const adminToggleHideLeaderboardPlayerProfile = () =>
{
post(`/admin/profile/${ data.player.id }/clear`, {}, { "X-Auth-Token": token })
post(`/admin/profile/${ data.player.id }/${ data.player.flags.includes("leaderboard_hidden") ? "showLeaderboard" : "hideLeaderboard" }`, {}, { "X-Auth-Token": token })
.then((response) =>
{
if (response.code === 200)
{
toast.success(t("admin.clear.alert"));
toast.success(t("admin.hideLeaderboard.alert"));
}
});
};
@ -64,14 +64,12 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
const { t } = useTranslation();
return <>
<ConfirmModal showModal={ clearStatsModal } setShowModal={ setClearStatsModal }
onConfirm={ adminClearPlayerStats } title={ t("admin.clear.modal.title") }
description={ t("admin.clear.modal.description") }/>
<ConfirmModal showModal={ hideLeaderboardStatsModal } setShowModal={ setHideLeaderboardStatsModal }
onConfirm={ adminToggleHideLeaderboardPlayerProfile } title={ t("admin.hideLeaderboard.modal.title") }
description={ t("admin.hideLeaderboard.modal.description") }/>
<ConfirmModal showModal={ hideProfileModal } setShowModal={ setHideProfileModal }
onConfirm={ adminHidePlayerProfile } title={ t("admin.hide.modal.title") }
description={ t("admin.hide.modal.description") }/>
<div
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
<div className="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
@ -83,8 +81,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
</div>
<div className="mt-4">
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
{ data.player.username } { data.player.flags.includes("verified") &&
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
{ data.player.username } <UserIcons flags={ data.player.flags }/>
</h3>
<div
@ -223,20 +220,24 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
{ isAdmin && <>
<div className="shadow-default dark:bg-boxdark items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
<h1 className="text-xl text-black dark:text-white">Moderator actions</h1>
<h1 className="text-xl text-black dark:text-white">{ t("admin.header") }</h1>
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap">
<button className="inline-flex items-center justify-center rounded-md bg-danger py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
onClick={ () => setClearStatsModal(true) }>
{t("admin.clear.button")}
</button>
{ data.player.flags.includes("leaderboard_hidden") ?
<button className={ "inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-success" }
onClick={ () => adminToggleHideLeaderboardPlayerProfile() }>
{ t("admin.hideLeaderboard.button2") }
</button> :
<button className={ "inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-danger" }
onClick={ () => setHideLeaderboardStatsModal(true) }>
{ t("admin.hideLeaderboard.button") }
</button> }
<button className="inline-flex items-center justify-center rounded-md bg-danger py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
onClick={ () => setHideProfileModal(true) }>
{t("admin.hide.button")}
{ t("admin.hide.button") }
</button>
</div>
</div>
@ -244,7 +245,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
<div className="shadow-default dark:bg-boxdark items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
<h1 className="text-sm text-black dark:text-white">
{t("profile.info", { date: dayjs(data.player.createdAt).format('DD/MM/YYYY') })}
{ t("profile.info", { date: dayjs(data.player.createdAt).format("DD/MM/YYYY") }) }
</h1>
</div>

View File

@ -154,13 +154,15 @@
"logout": "Log out"
},
"admin": {
"clear": {
"header": "Akcje moderacyjne",
"hideLeaderboard": {
"modal": {
"title": "Are you sure?",
"description": "This action will permanently clear user statistics."
"description": "This action will hide user profile from leaderboard."
},
"button": "Clear profile",
"alert": "Player stats cleared."
"button": "Hide profile in leaderboard",
"button2": "Show profile in leaderboard",
"alert": "Player profile hidden."
},
"hide": {
"modal": {

View File

@ -154,13 +154,15 @@
"logout": "Wyloguj"
},
"admin": {
"clear": {
"header": "Moderator actions",
"hideLeaderboard": {
"modal": {
"title": "Czy jesteś pewien?",
"description": "Ta akcja permanentnie wyczyści wszystkie statystyki gracza."
"description": "Ta akcja ukryje profil gracza w tablicy wyników."
},
"button": "Wyczyść profil",
"alert": "Wyczyszczono statystyki gracza."
"button": "Ukryj profil w tablicy wyników",
"button2": "Pokaż profil w tablicy wyników",
"alert": "Ukryto profil gracza w tablicy wyników."
},
"hide": {
"modal": {