mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-10 07:36:09 +08:00
added option to report users
This commit is contained in:
parent
ef6da63b73
commit
adf47214db
11 changed files with 332 additions and 1 deletions
|
@ -16,6 +16,9 @@ import * as LeaderboardsDAL from "../../dal/leaderboards";
|
|||
import { purgeUserFromDailyLeaderboards } from "../../utils/daily-leaderboards";
|
||||
import { randomBytes } from "crypto";
|
||||
import * as RedisClient from "../../init/redis";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as ReportDAL from "../../dal/report";
|
||||
|
||||
async function verifyCaptcha(captcha: string): Promise<void> {
|
||||
if (!(await verify(captcha))) {
|
||||
|
@ -665,3 +668,31 @@ export async function updateInbox(
|
|||
|
||||
return new MonkeyResponse("Inbox updated");
|
||||
}
|
||||
|
||||
export async function reportUser(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const {
|
||||
reporting: { maxReports, contentReportLimit },
|
||||
} = req.ctx.configuration.quotes;
|
||||
|
||||
const { uid: uidToReport, reason, comment, captcha } = req.body;
|
||||
|
||||
await verifyCaptcha(captcha);
|
||||
|
||||
const newReport: MonkeyTypes.Report = {
|
||||
_id: new ObjectId(),
|
||||
id: uuidv4(),
|
||||
type: "user",
|
||||
timestamp: new Date().getTime(),
|
||||
uid,
|
||||
contentId: `${uidToReport}`,
|
||||
reason,
|
||||
comment,
|
||||
};
|
||||
|
||||
await ReportDAL.createReport(newReport, maxReports, contentReportLimit);
|
||||
|
||||
return new MonkeyResponse("User reported");
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
asyncHandler,
|
||||
validateRequest,
|
||||
validateConfiguration,
|
||||
checkUserPermissions,
|
||||
} from "../../middlewares/api-utils";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import { withApeRateLimiter } from "../../middlewares/ape-rate-limit";
|
||||
|
@ -530,4 +531,46 @@ router.patch(
|
|||
asyncHandler(UserController.updateInbox)
|
||||
);
|
||||
|
||||
const withCustomMessages = joi.string().messages({
|
||||
"string.pattern.base": "Invalid parameter format",
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/report",
|
||||
validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.quotes.reporting.enabled;
|
||||
},
|
||||
invalidMessage: "User reporting is unavailable.",
|
||||
}),
|
||||
authenticateRequest(),
|
||||
RateLimit.quoteReportSubmit,
|
||||
validateRequest({
|
||||
body: {
|
||||
uid: withCustomMessages.regex(/^\w+$/).required(),
|
||||
reason: joi
|
||||
.string()
|
||||
.valid(
|
||||
"Inappropriate name",
|
||||
"Inappropriate bio",
|
||||
"Inappropriate social links",
|
||||
"Suspected cheating"
|
||||
)
|
||||
.required(),
|
||||
comment: withCustomMessages
|
||||
.allow("")
|
||||
.regex(/^([.]|[^/<>])+$/)
|
||||
.max(250)
|
||||
.required(),
|
||||
captcha: withCustomMessages.regex(/[\w-_]+/).required(),
|
||||
},
|
||||
}),
|
||||
checkUserPermissions({
|
||||
criteria: (user) => {
|
||||
return !user.cannotReport;
|
||||
},
|
||||
}),
|
||||
asyncHandler(UserController.reportUser)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
2
backend/src/types/types.d.ts
vendored
2
backend/src/types/types.d.ts
vendored
|
@ -432,7 +432,7 @@ declare namespace MonkeyTypes {
|
|||
level?: number;
|
||||
}
|
||||
|
||||
type ReportTypes = "quote";
|
||||
type ReportTypes = "quote" | "user";
|
||||
|
||||
interface Report {
|
||||
_id: ObjectId;
|
||||
|
|
|
@ -1131,6 +1131,60 @@
|
|||
}
|
||||
}
|
||||
|
||||
#userReportPopupWrapper {
|
||||
#userReportPopup {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-rows: auto auto auto auto auto auto auto auto auto;
|
||||
height: auto;
|
||||
max-height: 40rem;
|
||||
overflow-y: scroll;
|
||||
width: calc(100% - 4rem);
|
||||
margin-left: 2rem;
|
||||
max-width: 800px;
|
||||
|
||||
label {
|
||||
color: var(--sub-color);
|
||||
margin-bottom: -1rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
// color: var(--sub-color);
|
||||
}
|
||||
|
||||
.user {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
line-height: 1.2rem;
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.characterCount {
|
||||
position: absolute;
|
||||
top: -1.25rem;
|
||||
right: 0.25rem;
|
||||
color: var(--sub-color);
|
||||
user-select: none;
|
||||
&.red {
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#resultEditTagsPanelWrapper {
|
||||
#resultEditTagsPanel {
|
||||
background: var(--bg-color);
|
||||
|
|
|
@ -232,6 +232,7 @@
|
|||
padding: 1rem;
|
||||
border-radius: var(--roundness);
|
||||
align-content: center;
|
||||
padding-right: 3rem;
|
||||
|
||||
// grid-template-columns: 15rem auto 15rem auto 2fr auto auto;
|
||||
|
||||
|
|
|
@ -225,4 +225,20 @@ export default class Users {
|
|||
};
|
||||
return await this.httpClient.patch(`${BASE_PATH}/inbox`, { payload });
|
||||
}
|
||||
|
||||
async report(
|
||||
uid: string,
|
||||
reason: string,
|
||||
comment: string,
|
||||
captcha: string
|
||||
): Ape.EndpointData {
|
||||
const payload = {
|
||||
uid,
|
||||
reason,
|
||||
comment,
|
||||
captcha,
|
||||
};
|
||||
|
||||
return await this.httpClient.post(`${BASE_PATH}/report`, { payload });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ type ProfileViewPaths = "profile" | "account";
|
|||
|
||||
interface ProfileData extends MonkeyTypes.Snapshot {
|
||||
allTimeLbs: MonkeyTypes.LeaderboardMemory;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export async function update(
|
||||
|
@ -21,6 +22,9 @@ export async function update(
|
|||
const profileElement = $(`.page${elementClass} .profile`);
|
||||
const details = $(`.page${elementClass} .profile .details`);
|
||||
|
||||
profileElement.attr("uid", profile.uid ?? "");
|
||||
profileElement.attr("name", profile.name ?? "");
|
||||
|
||||
// ============================================================================
|
||||
// DO FREAKING NOT USE .HTML OR .APPEND HERE - USER INPUT!!!!!!
|
||||
// ============================================================================
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as Profile from "../elements/profile";
|
|||
import * as PbTables from "../account/pb-tables";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import { checkIfGetParameterExists } from "../utils/misc";
|
||||
import * as UserReportPopup from "../popups/user-report-popup";
|
||||
|
||||
function reset(): void {
|
||||
$(".page.pageProfile .preloader").removeClass("hidden");
|
||||
|
@ -64,6 +65,15 @@ function reset(): void {
|
|||
<div class="title">socials</div>
|
||||
<div class="value">-</div>
|
||||
</div>
|
||||
<div class="buttonGroup">
|
||||
<div
|
||||
class="userReportButton button"
|
||||
data-balloon-pos="left"
|
||||
aria-label="Report user"
|
||||
>
|
||||
<i class="fas fa-flag"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leaderboardsPositions">
|
||||
<div class="title">All-Time English Leaderboards</div>
|
||||
|
@ -181,6 +191,13 @@ async function update(options: UpdateOptions): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
$(".page.pageProfile").on("click", ".profile .userReportButton", () => {
|
||||
const uid = $(".page.pageProfile .profile").attr("uid") ?? "";
|
||||
const name = $(".page.pageProfile .profile").attr("name") ?? "";
|
||||
|
||||
UserReportPopup.show({ uid, name });
|
||||
});
|
||||
|
||||
export const page = new Page(
|
||||
"profile",
|
||||
$(".page.pageProfile"),
|
||||
|
|
126
frontend/src/ts/popups/user-report-popup.ts
Normal file
126
frontend/src/ts/popups/user-report-popup.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import Ape from "../ape";
|
||||
import * as Loader from "../elements/loader";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as CaptchaController from "../controllers/captcha-controller";
|
||||
|
||||
interface State {
|
||||
userUid?: string;
|
||||
}
|
||||
|
||||
const state: State = {
|
||||
userUid: undefined,
|
||||
};
|
||||
|
||||
interface ShowOptions {
|
||||
uid: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function show(options: ShowOptions): Promise<void> {
|
||||
if ($("#userReportPopupWrapper").hasClass("hidden")) {
|
||||
CaptchaController.render(
|
||||
document.querySelector("#userReportPopup .g-recaptcha") as HTMLElement,
|
||||
"userReportPopup"
|
||||
);
|
||||
|
||||
const { name } = options;
|
||||
state.userUid = options.uid;
|
||||
|
||||
$("#userReportPopup .user").text(name);
|
||||
$("#userReportPopup .reason").val("Inappropriate name");
|
||||
$("#userReportPopup .comment").val("");
|
||||
$("#userReportPopup .characterCount").text("-");
|
||||
$("#userReportPopup .reason").select2({
|
||||
minimumResultsForSearch: Infinity,
|
||||
});
|
||||
$("#userReportPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, 100, () => {
|
||||
$("#userReportPopup textarea").trigger("focus").trigger("select");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function hide(): Promise<void> {
|
||||
if (!$("#userReportPopupWrapper").hasClass("hidden")) {
|
||||
$("#userReportPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
100,
|
||||
() => {
|
||||
CaptchaController.reset("userReportPopup");
|
||||
$("#userReportPopupWrapper").addClass("hidden");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReport(): Promise<void> {
|
||||
const captchaResponse = CaptchaController.getResponse("userReportPopup");
|
||||
if (!captchaResponse) {
|
||||
return Notifications.add("Please complete the captcha");
|
||||
}
|
||||
|
||||
const reason = $("#userReportPopup .reason").val() as string;
|
||||
const comment = $("#userReportPopup .comment").val() as string;
|
||||
const captcha = captchaResponse as string;
|
||||
|
||||
if (!reason) {
|
||||
return Notifications.add("Please select a valid report reason");
|
||||
}
|
||||
|
||||
if (!comment) {
|
||||
return Notifications.add("Please provide a comment");
|
||||
}
|
||||
|
||||
const characterDifference = comment.length - 250;
|
||||
if (characterDifference > 0) {
|
||||
return Notifications.add(
|
||||
`Report comment is ${characterDifference} character(s) too long`
|
||||
);
|
||||
}
|
||||
|
||||
Loader.show();
|
||||
const response = await Ape.users.report(
|
||||
state.userUid as string,
|
||||
reason,
|
||||
comment,
|
||||
captcha
|
||||
);
|
||||
Loader.hide();
|
||||
|
||||
if (response.status !== 200) {
|
||||
return Notifications.add("Failed to report user: " + response.message, -1);
|
||||
}
|
||||
|
||||
Notifications.add("Report submitted. Thank you!", 1);
|
||||
hide();
|
||||
}
|
||||
|
||||
$("#userReportPopupWrapper").on("mousedown", (e) => {
|
||||
if ($(e.target).attr("id") === "userReportPopupWrapper") {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
$("#userReportPopup .comment").on("input", () => {
|
||||
setTimeout(() => {
|
||||
const len = ($("#userReportPopup .comment").val() as string).length;
|
||||
$("#userReportPopup .characterCount").text(len);
|
||||
if (len > 250) {
|
||||
$("#userReportPopup .characterCount").addClass("red");
|
||||
} else {
|
||||
$("#userReportPopup .characterCount").removeClass("red");
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
|
||||
$("#userReportPopup .submit").on("click", async () => {
|
||||
await submitReport();
|
||||
});
|
|
@ -74,6 +74,15 @@
|
|||
<div class="title">socials</div>
|
||||
<div class="value">-</div>
|
||||
</div>
|
||||
<div class="buttonGroup">
|
||||
<div
|
||||
class="userReportButton button"
|
||||
data-balloon-pos="left"
|
||||
aria-label="Report user"
|
||||
>
|
||||
<i class="fas fa-flag"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leaderboardsPositions">
|
||||
<div class="title">All-Time English Leaderboards</div>
|
||||
|
|
|
@ -736,6 +736,36 @@
|
|||
<div class="button submit">Report</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="userReportPopupWrapper" class="popupWrapper hidden">
|
||||
<div id="userReportPopup" mode="">
|
||||
<div class="title">Report a User</div>
|
||||
<div class="text">
|
||||
Please report users responsibly. Please add comments in English only.
|
||||
Misuse may result in you losing access to this feature.
|
||||
</div>
|
||||
<label>user</label>
|
||||
<div class="user"></div>
|
||||
<label>reason</label>
|
||||
<select name="report-reason" class="reason">
|
||||
<option value="Inappropriate name">Inappropriate name</option>
|
||||
<option value="Inappropriate bio">Inappropriate bio</option>
|
||||
<option value="Inappropriate social links">
|
||||
Inappropriate social links
|
||||
</option>
|
||||
<option value="Suspected cheating">Suspected cheating</option>
|
||||
</select>
|
||||
<label>comment</label>
|
||||
<div style="position: relative">
|
||||
<textarea class="comment" type="text" autocomplete="off"></textarea>
|
||||
<div class="characterCount">-</div>
|
||||
</div>
|
||||
<div
|
||||
class="g-recaptcha"
|
||||
data-sitekey="6Lc-V8McAAAAAJ7s6LGNe7MBZnRiwbsbiWts87aj"
|
||||
></div>
|
||||
<div class="button submit">Report</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="quoteApprovePopupWrapper" class="popupWrapper hidden">
|
||||
<div id="quoteApprovePopup" mode="">
|
||||
<div class="top">
|
||||
|
|
Loading…
Add table
Reference in a new issue