mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-06 13:40:16 +08:00
Merge branch 'master' into newads
This commit is contained in:
commit
ecd97ebc51
16 changed files with 173 additions and 24 deletions
5
backend/src/api/controllers/admin.ts
Normal file
5
backend/src/api/controllers/admin.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
|
||||
export async function test(): Promise<MonkeyResponse> {
|
||||
return new MonkeyResponse("OK");
|
||||
}
|
18
backend/src/api/routes/admin.ts
Normal file
18
backend/src/api/routes/admin.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// import joi from "joi";
|
||||
import { Router } from "express";
|
||||
import { authenticateRequest } from "../../middlewares/auth";
|
||||
import { asyncHandler, checkIfUserIsAdmin } from "../../middlewares/api-utils";
|
||||
import * as AdminController from "../controllers/admin";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
authenticateRequest({
|
||||
noCache: true,
|
||||
}),
|
||||
checkIfUserIsAdmin(),
|
||||
asyncHandler(AdminController.test)
|
||||
);
|
||||
|
||||
export default router;
|
|
@ -8,6 +8,7 @@ import configs from "./configs";
|
|||
import results from "./results";
|
||||
import presets from "./presets";
|
||||
import apeKeys from "./ape-keys";
|
||||
import admin from "./admin";
|
||||
import configuration from "./configuration";
|
||||
import { version } from "../../version";
|
||||
import leaderboards from "./leaderboards";
|
||||
|
@ -37,6 +38,7 @@ const API_ROUTE_MAP = {
|
|||
"/leaderboards": leaderboards,
|
||||
"/quotes": quotes,
|
||||
"/ape-keys": apeKeys,
|
||||
"/admin": admin,
|
||||
};
|
||||
|
||||
function addApiRoutes(app: Application): void {
|
||||
|
|
10
backend/src/dal/admin-uids.ts
Normal file
10
backend/src/dal/admin-uids.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as db from "../init/db";
|
||||
|
||||
export async function isAdmin(uid: string): Promise<boolean> {
|
||||
const doc = await db.collection("admin-uids").findOne({ uid });
|
||||
if (doc) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import MonkeyError from "../utils/error";
|
|||
import { Response, NextFunction, RequestHandler } from "express";
|
||||
import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response";
|
||||
import { getUser } from "../dal/user";
|
||||
import { isAdmin } from "../dal/admin-uids";
|
||||
|
||||
interface ValidationOptions<T> {
|
||||
criteria: (data: T) => boolean;
|
||||
|
@ -40,6 +41,31 @@ function validateConfiguration(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is an admin before handling request.
|
||||
* Note that this middleware must be used after authentication in the middleware stack.
|
||||
*/
|
||||
function checkIfUserIsAdmin(): RequestHandler {
|
||||
return async (
|
||||
req: MonkeyTypes.Request,
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const admin = await isAdmin(uid);
|
||||
|
||||
if (!admin) {
|
||||
throw new MonkeyError(403, "You don't have permission to do this.");
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user permissions before handling request.
|
||||
* Note that this middleware must be used after authentication in the middleware stack.
|
||||
|
@ -158,6 +184,7 @@ function useInProduction(middlewares: RequestHandler[]): RequestHandler[] {
|
|||
export {
|
||||
validateConfiguration,
|
||||
checkUserPermissions,
|
||||
checkIfUserIsAdmin,
|
||||
asyncHandler,
|
||||
validateRequest,
|
||||
useInProduction,
|
||||
|
|
|
@ -17,6 +17,7 @@ interface RequestAuthenticationOptions {
|
|||
isPublic?: boolean;
|
||||
acceptApeKeys?: boolean;
|
||||
requireFreshToken?: boolean;
|
||||
noCache?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
|
||||
|
@ -149,7 +150,10 @@ async function authenticateWithBearerToken(
|
|||
options: RequestAuthenticationOptions
|
||||
): Promise<MonkeyTypes.DecodedToken> {
|
||||
try {
|
||||
const decodedToken = await verifyIdToken(token, options.requireFreshToken);
|
||||
const decodedToken = await verifyIdToken(
|
||||
token,
|
||||
options.requireFreshToken || options.noCache
|
||||
);
|
||||
|
||||
if (options.requireFreshToken) {
|
||||
const now = Date.now();
|
||||
|
|
|
@ -231,6 +231,7 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
UpdateConfig.setMode("words", true);
|
||||
UpdateConfig.setDifficulty("normal", true);
|
||||
} else if (challenge.type === "customText") {
|
||||
CustomText.setPopupTextareaState(challenge.parameters[0] as string);
|
||||
CustomText.setText((challenge.parameters[0] as string).split(" "));
|
||||
CustomText.setIsTimeRandom(false);
|
||||
CustomText.setIsWordRandom(challenge.parameters[1] as boolean);
|
||||
|
@ -249,6 +250,7 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
let text = scriptdata.trim();
|
||||
text = text.replace(/[\n\r\t ]/gm, " ");
|
||||
text = text.replace(/ +/gm, " ");
|
||||
CustomText.setPopupTextareaState(text);
|
||||
CustomText.setText(text.split(" "));
|
||||
CustomText.setIsWordRandom(false);
|
||||
CustomText.setTime(-1);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { getHTMLById } from "../controllers/badge-controller";
|
|||
import { throttle } from "throttle-debounce";
|
||||
import * as EditProfilePopup from "../popups/edit-profile-popup";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import { formatDistanceToNowStrict } from "date-fns/esm";
|
||||
|
||||
type ProfileViewPaths = "profile" | "account";
|
||||
|
||||
|
@ -93,6 +94,8 @@ export async function update(
|
|||
const balloonText = `${diffDays} day${diffDays != 1 ? "s" : ""} ago`;
|
||||
details.find(".joined").text(joinedText).attr("aria-label", balloonText);
|
||||
|
||||
let hoverText = "";
|
||||
|
||||
if (profile.streak && profile?.streak > 1) {
|
||||
details
|
||||
.find(".streak")
|
||||
|
@ -100,17 +103,42 @@ export async function update(
|
|||
`Current streak: ${profile.streak} ${
|
||||
profile.streak === 1 ? "day" : "days"
|
||||
}`
|
||||
)
|
||||
.attr(
|
||||
"aria-label",
|
||||
`Longest streak: ${profile.maxStreak} ${
|
||||
profile.maxStreak === 1 ? "day" : "days"
|
||||
}`
|
||||
);
|
||||
hoverText = `Longest streak: ${profile.maxStreak} ${
|
||||
profile.maxStreak === 1 ? "day" : "days"
|
||||
}`;
|
||||
} else {
|
||||
details.find(".streak").text("").attr("aria-label", "");
|
||||
details.find(".streak").text("");
|
||||
hoverText = "";
|
||||
}
|
||||
|
||||
if (where === "account") {
|
||||
const results = DB.getSnapshot()?.results;
|
||||
const lastResult = results?.[0];
|
||||
|
||||
const dayInMilis = 1000 * 60 * 60 * 24;
|
||||
const timeDif = formatDistanceToNowStrict(
|
||||
Misc.getCurrentDayTimestamp() + dayInMilis
|
||||
);
|
||||
|
||||
if (lastResult) {
|
||||
//check if the last result is from today
|
||||
const isToday = Misc.isToday(lastResult.timestamp);
|
||||
if (isToday) {
|
||||
hoverText += `\nClaimed today: yes`;
|
||||
hoverText += `\nCome back in: ${timeDif}`;
|
||||
} else {
|
||||
hoverText += `\nClaimed today: no`;
|
||||
hoverText += `\nStreak lost in: ${timeDif}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
details
|
||||
.find(".streak")
|
||||
.attr("aria-label", hoverText)
|
||||
.attr("data-balloon-break", "");
|
||||
|
||||
const typingStatsEl = details.find(".typingStats");
|
||||
typingStatsEl
|
||||
.find(".started .value")
|
||||
|
|
|
@ -798,9 +798,17 @@ function fillContent(): void {
|
|||
Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10));
|
||||
|
||||
if (!Config.startGraphsAtZero) {
|
||||
accountHistoryScaleOptions["wpm"].min = Math.floor(minWpmChartVal);
|
||||
const minWpmChartValFloor = Math.floor(minWpmChartVal);
|
||||
|
||||
accountHistoryScaleOptions["wpm"].min = minWpmChartValFloor;
|
||||
accountHistoryScaleOptions["pb"].min = minWpmChartValFloor;
|
||||
accountHistoryScaleOptions["wpmAvgTen"].min = minWpmChartValFloor;
|
||||
accountHistoryScaleOptions["wpmAvgHundred"].min = minWpmChartValFloor;
|
||||
} else {
|
||||
accountHistoryScaleOptions["wpm"].min = 0;
|
||||
accountHistoryScaleOptions["pb"].min = 0;
|
||||
accountHistoryScaleOptions["wpmAvgTen"].min = 0;
|
||||
accountHistoryScaleOptions["wpmAvgHundred"].min = 0;
|
||||
}
|
||||
|
||||
if (!chartData || chartData.length == 0) {
|
||||
|
|
|
@ -93,6 +93,7 @@ export function init(missed: boolean, slow: boolean): boolean {
|
|||
const numbers = before.numbers === null ? Config.numbers : before.numbers;
|
||||
UpdateConfig.setMode("custom", true);
|
||||
|
||||
CustomText.setPopupTextareaState(newCustomText.join(CustomText.delimiter));
|
||||
CustomText.setText(newCustomText);
|
||||
CustomText.setIsWordRandom(true);
|
||||
CustomText.setIsTimeRandom(false);
|
||||
|
|
|
@ -823,9 +823,6 @@ export async function update(
|
|||
$("#words").empty();
|
||||
ChartController.result.resize();
|
||||
|
||||
if (Config.alwaysShowWordsHistory && Config.burstHeatmap) {
|
||||
TestUI.applyBurstHeatmap();
|
||||
}
|
||||
$("#result").trigger("focus");
|
||||
window.scrollTo({ top: 0 });
|
||||
$("#testModesNotice").addClass("hidden");
|
||||
|
@ -838,7 +835,7 @@ export async function update(
|
|||
125
|
||||
);
|
||||
if (Config.alwaysShowWordsHistory && !GlarsesMode.get()) {
|
||||
TestUI.toggleResultWords();
|
||||
TestUI.toggleResultWords(true);
|
||||
}
|
||||
Keymap.hide();
|
||||
AdController.updateTestPageAds(true);
|
||||
|
|
|
@ -881,13 +881,17 @@ export async function init(): Promise<void> {
|
|||
}
|
||||
|
||||
if (Config.tapeMode !== "off" && !language.leftToRight) {
|
||||
Notifications.add("This language does not support tape mode.", 0);
|
||||
Notifications.add("This language does not support tape mode.", 0, {
|
||||
important: true,
|
||||
});
|
||||
UpdateConfig.setTapeMode("off");
|
||||
}
|
||||
|
||||
if (Config.lazyMode === true && language.noLazyMode) {
|
||||
rememberLazyMode = true;
|
||||
Notifications.add("This language does not support lazy mode.", 0);
|
||||
Notifications.add("This language does not support lazy mode.", 0, {
|
||||
important: true,
|
||||
});
|
||||
UpdateConfig.setLazyMode(false, true);
|
||||
} else if (rememberLazyMode === true && !language.noLazyMode) {
|
||||
UpdateConfig.setLazyMode(true, true);
|
||||
|
@ -1078,7 +1082,8 @@ export async function init(): Promise<void> {
|
|||
`No ${Config.language
|
||||
.replace(/_\d*k$/g, "")
|
||||
.replace(/_/g, " ")} quotes found`,
|
||||
0
|
||||
0,
|
||||
{ important: true }
|
||||
);
|
||||
if (Auth?.currentUser) {
|
||||
QuoteSubmitPopup.show(false);
|
||||
|
@ -1095,7 +1100,7 @@ export async function init(): Promise<void> {
|
|||
);
|
||||
if (targetQuote === undefined) {
|
||||
rq = <MonkeyTypes.Quote>quotesCollection.groups[0][0];
|
||||
Notifications.add("Quote Id Does Not Exist", 0);
|
||||
Notifications.add("Quote Id Does Not Exist", 0, { important: true });
|
||||
} else {
|
||||
rq = targetQuote;
|
||||
}
|
||||
|
@ -1105,7 +1110,7 @@ export async function init(): Promise<void> {
|
|||
);
|
||||
|
||||
if (randomQuote === null) {
|
||||
Notifications.add("No favorite quotes found", 0);
|
||||
Notifications.add("No favorite quotes found", 0, { important: true });
|
||||
UpdateConfig.setQuoteLength(-1);
|
||||
restart();
|
||||
return;
|
||||
|
@ -1115,7 +1120,9 @@ export async function init(): Promise<void> {
|
|||
} else {
|
||||
const randomQuote = QuotesController.getRandomQuote();
|
||||
if (randomQuote === null) {
|
||||
Notifications.add("No quotes found for selected quote length", 0);
|
||||
Notifications.add("No quotes found for selected quote length", 0, {
|
||||
important: true,
|
||||
});
|
||||
TestUI.setTestRestarting(false);
|
||||
return;
|
||||
}
|
||||
|
@ -1312,6 +1319,7 @@ export async function retrySavingResult(): Promise<void> {
|
|||
0,
|
||||
{
|
||||
duration: 5,
|
||||
important: true,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -1668,17 +1676,22 @@ export async function finish(difficultyFailed = false): Promise<void> {
|
|||
CustomText.setCustomTextLongProgress(customTextName, newProgress);
|
||||
Notifications.add("Long custom text progress saved", 1, {
|
||||
duration: 5,
|
||||
important: true,
|
||||
});
|
||||
|
||||
let newText = CustomText.getCustomText(customTextName, true);
|
||||
newText = newText.slice(newProgress);
|
||||
CustomText.setPopupTextareaState(newText.join(CustomText.delimiter));
|
||||
CustomText.setText(newText);
|
||||
} else {
|
||||
// They finished the test
|
||||
CustomText.setCustomTextLongProgress(customTextName, 0);
|
||||
CustomText.setText(CustomText.getCustomText(customTextName, true));
|
||||
const text = CustomText.getCustomText(customTextName, true);
|
||||
CustomText.setPopupTextareaState(text.join(CustomText.delimiter));
|
||||
CustomText.setText(text);
|
||||
Notifications.add("Long custom text completed", 1, {
|
||||
duration: 5,
|
||||
important: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1762,6 +1775,7 @@ async function saveResult(
|
|||
Notifications.add("Result not saved: disabled by user", -1, {
|
||||
duration: 3,
|
||||
customTitle: "Notice",
|
||||
important: true,
|
||||
});
|
||||
AccountButton.loading(false);
|
||||
return;
|
||||
|
@ -1771,6 +1785,7 @@ async function saveResult(
|
|||
Notifications.add("Result not saved: offline", -1, {
|
||||
duration: 2,
|
||||
customTitle: "Notice",
|
||||
important: true,
|
||||
});
|
||||
AccountButton.loading(false);
|
||||
retrySaving.canRetry = true;
|
||||
|
@ -1892,7 +1907,7 @@ async function saveResult(
|
|||
|
||||
$("#retrySavingResultButton").addClass("hidden");
|
||||
if (isRetrying) {
|
||||
Notifications.add("Result saved", 1);
|
||||
Notifications.add("Result saved", 1, { important: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -985,7 +985,7 @@ async function loadWordsHistory(): Promise<boolean> {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function toggleResultWords(): void {
|
||||
export function toggleResultWords(noAnimation = false): void {
|
||||
if (resultVisible) {
|
||||
if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) {
|
||||
//show
|
||||
|
@ -1001,7 +1001,7 @@ export function toggleResultWords(): void {
|
|||
$("#resultWordsHistory")
|
||||
.removeClass("hidden")
|
||||
.css("display", "none")
|
||||
.slideDown(250, () => {
|
||||
.slideDown(noAnimation ? 0 : 250, () => {
|
||||
if (Config.burstHeatmap) {
|
||||
applyBurstHeatmap();
|
||||
}
|
||||
|
@ -1014,7 +1014,7 @@ export function toggleResultWords(): void {
|
|||
$("#resultWordsHistory")
|
||||
.removeClass("hidden")
|
||||
.css("display", "none")
|
||||
.slideDown(250);
|
||||
.slideDown(noAnimation ? 0 : 250);
|
||||
}
|
||||
} else {
|
||||
//hide
|
||||
|
|
|
@ -1522,3 +1522,26 @@ export async function checkIfLanguageSupportsZipf(
|
|||
if (lang.orderedByFrequency === false) return "no";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function getStartOfDayTimestamp(timestamp: number): number {
|
||||
return timestamp - (timestamp % 86400000);
|
||||
}
|
||||
|
||||
export function getCurrentDayTimestamp(): number {
|
||||
const currentTime = Date.now();
|
||||
return getStartOfDayTimestamp(currentTime);
|
||||
}
|
||||
|
||||
export function isYesterday(timestamp: number): boolean {
|
||||
const yesterday = getStartOfDayTimestamp(Date.now() - 86400000);
|
||||
const date = getStartOfDayTimestamp(timestamp);
|
||||
|
||||
return yesterday === date;
|
||||
}
|
||||
|
||||
export function isToday(timestamp: number): boolean {
|
||||
const today = getStartOfDayTimestamp(Date.now());
|
||||
const date = getStartOfDayTimestamp(timestamp);
|
||||
|
||||
return today === date;
|
||||
}
|
||||
|
|
|
@ -124,6 +124,9 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
|
|||
|
||||
if (de[2]) {
|
||||
const customTextSettings = de[2];
|
||||
CustomText.setPopupTextareaState(
|
||||
customTextSettings["text"].join(customTextSettings["delimiter"])
|
||||
);
|
||||
CustomText.setText(customTextSettings["text"]);
|
||||
CustomText.setIsTimeRandom(customTextSettings["isTimeRandom"]);
|
||||
CustomText.setIsWordRandom(customTextSettings["isWordRandom"]);
|
||||
|
|
|
@ -38812,6 +38812,12 @@
|
|||
"source": "The World Was Wide Enough, Hamilton",
|
||||
"length": 81,
|
||||
"id": 6814
|
||||
},
|
||||
{
|
||||
"text": "You are a very special person. There is only one like you in the whole world. There's never been anyone exactly like you before, and there will never be again. Only you. And people can like you exactly as you are.",
|
||||
"source": "Fred Rogers",
|
||||
"length": 213,
|
||||
"id": 6815
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue