Merge branch 'master' into offline

This commit is contained in:
Miodec 2022-09-28 13:31:35 +02:00
commit 2920722315
19 changed files with 252 additions and 36 deletions

View file

@ -25,7 +25,16 @@ export async function createNewUser(
const { name, captcha } = req.body;
const { email, uid } = req.ctx.decodedToken;
await verifyCaptcha(captcha);
try {
await verifyCaptcha(captcha);
} catch (e) {
try {
await admin.auth().deleteUser(uid);
} catch (e) {
// user might be deleted on the frontend
}
throw e;
}
if (email.endsWith("@tidal.lol") || email.endsWith("@selfbot.cc")) {
throw new MonkeyError(400, "Invalid domain");

View file

@ -81,7 +81,9 @@ const CONFIG_SCHEMA = joi.object({
soundVolume: joi.string().valid("0.1", "0.5", "1.0"),
startGraphsAtZero: joi.boolean(),
showOutOfFocusWarning: joi.boolean(),
paceCaret: joi.string().valid("off", "average", "pb", "last", "custom"),
paceCaret: joi
.string()
.valid("off", "average", "pb", "last", "daily", "custom"),
paceCaretCustomSpeed: joi.number().min(0),
repeatedPace: joi.boolean(),
pageWidth: joi.string().valid("100", "125", "150", "200", "max"),

View file

@ -144,7 +144,7 @@ async function authenticateWithBearerToken(
options: RequestAuthenticationOptions
): Promise<MonkeyTypes.DecodedToken> {
try {
const decodedToken = await verifyIdToken(token);
const decodedToken = await verifyIdToken(token, options.requireFreshToken);
if (options.requireFreshToken) {
const now = Date.now();

View file

@ -17,7 +17,14 @@ const tokenCache = new LRUCache<string, DecodedIdToken>({
const TOKEN_CACHE_BUFFER = 1000 * 60 * 5; // 5 minutes
export async function verifyIdToken(idToken: string): Promise<DecodedIdToken> {
export async function verifyIdToken(
idToken: string,
noCache = false
): Promise<DecodedIdToken> {
if (noCache) {
return await admin.auth().verifyIdToken(idToken, true);
}
setTokenCacheLength(tokenCache.size);
setTokenCacheSize(tokenCache.calculatedSize ?? 0);

View file

@ -9,8 +9,8 @@
"version": "1.14.3",
"license": "GPL-3.0",
"dependencies": {
"@types/throttle-debounce": "2.1.0",
"axios": "0.21.4",
"canvas-confetti": "1.5.1",
"chart.js": "3.7.1",
"chartjs-adapter-date-fns": "2.0.0",
"chartjs-plugin-annotation": "1.4.0",
@ -29,6 +29,7 @@
"throttle-debounce": "3.0.1"
},
"devDependencies": {
"@types/canvas-confetti": "1.4.3",
"@types/chartjs-plugin-trendline": "1.0.1",
"@types/damerau-levenshtein": "1.0.0",
"@types/grecaptcha": "3.0.4",
@ -36,6 +37,7 @@
"@types/jquery": "3.5.14",
"@types/object-hash": "2.2.1",
"@types/select2": "4.0.55",
"@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.3",
"buffer": "6.0.3",
"circular-dependency-plugin": "5.2.2",
@ -951,6 +953,12 @@
"@types/node": "*"
}
},
"node_modules/@types/canvas-confetti": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.4.3.tgz",
"integrity": "sha512-UwFPTsW1ZwVyo/ETp4hPSikSD7yl2V42E3VWBF5P/0+DHO4iajyceWv7hfNdZ2AX5tkZnuViiBWOqyCPohU2FQ==",
"dev": true
},
"node_modules/@types/chartjs-plugin-trendline": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz",
@ -1158,7 +1166,8 @@
"node_modules/@types/throttle-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ=="
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==",
"dev": true
},
"node_modules/@types/tinycolor2": {
"version": "1.4.3",
@ -2548,6 +2557,15 @@
"url": "https://opencollective.com/browserslist"
}
},
"node_modules/canvas-confetti": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.5.1.tgz",
"integrity": "sha512-Ncz+oZJP6OvY7ti4E1slxVlyAV/3g7H7oQtcCDXgwGgARxPnwYY9PW5Oe+I8uvspYNtuHviAdgA0LfcKFWJfpg==",
"funding": {
"type": "donate",
"url": "https://www.paypal.me/kirilvatev"
}
},
"node_modules/centra": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/centra/-/centra-1.0.1.tgz",
@ -15157,6 +15175,12 @@
"@types/node": "*"
}
},
"@types/canvas-confetti": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.4.3.tgz",
"integrity": "sha512-UwFPTsW1ZwVyo/ETp4hPSikSD7yl2V42E3VWBF5P/0+DHO4iajyceWv7hfNdZ2AX5tkZnuViiBWOqyCPohU2FQ==",
"dev": true
},
"@types/chartjs-plugin-trendline": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz",
@ -15364,7 +15388,8 @@
"@types/throttle-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ=="
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==",
"dev": true
},
"@types/tinycolor2": {
"version": "1.4.3",
@ -16453,6 +16478,11 @@
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001309.tgz",
"integrity": "sha512-Pl8vfigmBXXq+/yUz1jUwULeq9xhMJznzdc/xwl4WclDAuebcTHVefpz8lE/bMI+UN7TOkSSe7B7RnZd6+dzjA=="
},
"canvas-confetti": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.5.1.tgz",
"integrity": "sha512-Ncz+oZJP6OvY7ti4E1slxVlyAV/3g7H7oQtcCDXgwGgARxPnwYY9PW5Oe+I8uvspYNtuHviAdgA0LfcKFWJfpg=="
},
"centra": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/centra/-/centra-1.0.1.tgz",

View file

@ -17,6 +17,7 @@
"npm": "8.1.2"
},
"devDependencies": {
"@types/canvas-confetti": "1.4.3",
"@types/chartjs-plugin-trendline": "1.0.1",
"@types/damerau-levenshtein": "1.0.0",
"@types/grecaptcha": "3.0.4",
@ -55,6 +56,7 @@
},
"dependencies": {
"axios": "0.21.4",
"canvas-confetti": "1.5.1",
"chart.js": "3.7.1",
"chartjs-adapter-date-fns": "2.0.0",
"chartjs-plugin-annotation": "1.4.0",

View file

@ -41,6 +41,15 @@ const subgroup: MonkeyTypes.CommandsSubgroup = {
TestLogic.restart();
},
},
{
id: "setPaceCaretDaily",
display: "daily",
configValue: "daily",
exec: (): void => {
UpdateConfig.setPaceCaret("daily");
TestLogic.restart();
},
},
{
id: "setPaceCaretCustom",
display: "custom...",

View file

@ -346,7 +346,7 @@ export function setPaceCaret(
): boolean {
if (
!isConfigValueValid("pace caret", val, [
["custom", "off", "average", "pb", "last"],
["custom", "off", "average", "pb", "last", "daily"],
])
) {
return false;

View file

@ -608,30 +608,6 @@ async function signUp(): Promise<void> {
return;
}
// Force user to use a capital letter, number, special character when setting up an account and changing password
if (password.length < 8) {
Notifications.add("Password must be at least 8 characters", 0, 3);
LoginPage.hidePreloader();
LoginPage.enableInputs();
LoginPage.updateSignupButton();
return;
}
const hasCapital = password.match(/[A-Z]/);
const hasNumber = password.match(/[\d]/);
const hasSpecial = password.match(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/);
if (!hasCapital || !hasNumber || !hasSpecial) {
Notifications.add(
"Password must contain at least one capital letter, number, and special character",
0,
3
);
LoginPage.hidePreloader();
LoginPage.enableInputs();
LoginPage.updateSignupButton();
return;
}
if (password !== passwordVerify) {
Notifications.add("Passwords do not match", 0, 3);
LoginPage.hidePreloader();
@ -640,6 +616,22 @@ async function signUp(): Promise<void> {
return;
}
// Force user to use a capital letter, number, special character when setting up an account and changing password
if (
window.location.hostname !== "localhost" &&
!Misc.isPasswordStrong(password)
) {
Notifications.add(
"Password must contain at least one capital letter, number, a special character and at least 8 characters long",
0,
4
);
LoginPage.hidePreloader();
LoginPage.enableInputs();
LoginPage.updateSignupButton();
return;
}
authListener();
let createdAuthUser;
@ -688,8 +680,16 @@ async function signUp(): Promise<void> {
} catch (e) {
//make sure to do clean up here
if (createdAuthUser) {
await Ape.users.delete();
await createdAuthUser.user.delete();
try {
await Ape.users.delete();
} catch (e) {
// account might already be deleted
}
try {
await createdAuthUser.user.delete();
} catch (e) {
// account might already be deleted
}
}
console.log(e);
const message = Misc.createErrorMessage(e, "Failed to create account");

View file

@ -428,6 +428,65 @@ export async function getUserAverage10<M extends MonkeyTypes.Mode>(
return retval;
}
export async function getUserDailyBest<M extends MonkeyTypes.Mode>(
mode: M,
mode2: MonkeyTypes.Mode2<M>,
punctuation: boolean,
language: string,
difficulty: MonkeyTypes.Difficulty,
lazyMode: boolean
): Promise<number> {
const snapshot = getSnapshot();
if (!snapshot) return 0;
function cont(): number {
const activeTagIds: string[] = [];
snapshot.tags?.forEach((tag) => {
if (tag.active === true) {
activeTagIds.push(tag._id);
}
});
let bestWpm = 0;
if (snapshot.results !== undefined) {
for (const result of snapshot.results) {
if (
result.mode === mode &&
result.punctuation === punctuation &&
result.language === language &&
result.difficulty === difficulty &&
(result.lazyMode === lazyMode ||
(result.lazyMode === undefined && lazyMode === false)) &&
(activeTagIds.length === 0 ||
activeTagIds.some((tagId) => result.tags.includes(tagId)))
) {
if (result.timestamp < Date.now() - 86400000) {
continue;
}
// Continue if the mode2 doesn't match and it's not a quote
if (result.mode2 !== mode2 && mode !== "quote") {
continue;
}
if (result.wpm > bestWpm) {
bestWpm = result.wpm;
}
}
}
}
return bestWpm;
}
const retval: number =
snapshot === null || (await getUserResults()) === null ? 0 : cont();
return retval;
}
export async function getLocalPB<M extends MonkeyTypes.Mode>(
mode: M,
mode2: MonkeyTypes.Mode2<M>,

View file

@ -116,6 +116,8 @@ export async function update(): Promise<void> {
? "pb"
: Config.paceCaret === "last"
? "last"
: Config.paceCaret === "daily"
? "daily"
: "custom"
} pace${speed}</div>`
);

View file

@ -20,6 +20,7 @@ import {
unlink,
updatePassword,
} from "firebase/auth";
import { isPasswordStrong } from "../utils/misc";
interface Input {
placeholder?: string;
@ -524,6 +525,17 @@ list["updatePassword"] = new SimplePopup(
Notifications.add("New passwords don't match", 0);
return;
}
if (
window.location.hostname !== "localhost" &&
!isPasswordStrong(newPass)
) {
Notifications.add(
"New password must contain at least one capital letter, number, a special character and at least 8 characters long",
0,
4
);
return;
}
Loader.show();
await reauthenticateWithCredential(user, credential);
await updatePassword(user, newPass);

View file

@ -75,6 +75,16 @@ export async function init(): Promise<void> {
Config.lazyMode
);
wpm = Math.round(wpm);
} else if (Config.paceCaret === "daily") {
wpm = await DB.getUserDailyBest(
Config.mode,
mode2,
Config.punctuation,
Config.language,
Config.difficulty,
Config.lazyMode
);
wpm = Math.round(wpm);
} else if (Config.paceCaret === "custom") {
wpm = Config.paceCaretCustomSpeed;
} else if (Config.paceCaret === "last" || TestState.isPaceRepeat == true) {

View file

@ -18,11 +18,13 @@ import * as AdController from "../controllers/ad-controller";
import * as TestConfig from "./test-config";
import { Chart } from "chart.js";
import { Auth } from "../firebase";
import * as SlowTimer from "../states/slow-timer";
// eslint-disable-next-line no-duplicate-imports -- need to ignore because eslint doesnt know what import type is
import type { PluginChartOptions, ScaleChartOptions } from "chart.js";
import type { AnnotationOptions } from "chartjs-plugin-annotation";
import Ape from "../ape";
import confetti from "canvas-confetti";
let result: MonkeyTypes.Result<MonkeyTypes.Mode>;
let maxChartVal: number;
@ -343,6 +345,38 @@ export function showCrown(): void {
PbCrown.show();
}
export function showConfetti(): void {
if (SlowTimer.get()) return;
const style = getComputedStyle(document.body);
const colors = [
style.getPropertyValue("--main-color"),
style.getPropertyValue("--text-color"),
style.getPropertyValue("--sub-color"),
];
const duration = Date.now() + 125;
(function f(): void {
confetti({
particleCount: 5,
angle: 60,
spread: 75,
origin: { x: 0 },
colors: colors,
});
confetti({
particleCount: 5,
angle: 120,
spread: 75,
origin: { x: 1 },
colors: colors,
});
if (Date.now() < duration) {
requestAnimationFrame(f);
}
})();
}
export function hideCrown(): void {
PbCrown.hide();
$("#result .stats .wpm .crown").attr("aria-label", "");

View file

@ -1738,6 +1738,11 @@ async function saveResult(
if (response?.data?.isPb) {
//new pb
if (
DB.getSnapshot()?.personalBests?.[Config.mode]?.[completedEvent.mode2]
) {
Result.showConfetti();
}
Result.showCrown();
Result.updateCrown();
DB.saveLocalPB(

View file

@ -108,7 +108,7 @@ declare namespace MonkeyTypes {
type SoundVolume = "0.1" | "0.5" | "1.0";
type PaceCaret = "off" | "average" | "pb" | "last" | "custom";
type PaceCaret = "off" | "average" | "pb" | "last" | "custom" | "daily";
type PageWidth = "100" | "125" | "150" | "200" | "max";

View file

@ -1235,3 +1235,11 @@ export function abbreviateNumber(num: number): string {
export async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function isPasswordStrong(password: string): boolean {
const hasCapital = !!password.match(/[A-Z]/);
const hasNumber = !!password.match(/[\d]/);
const hasSpecial = !!password.match(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/);
const isLong = password.length >= 8;
return hasCapital && hasNumber && hasSpecial && isLong;
}

View file

@ -175,6 +175,16 @@
<!-- Initialize Firebase -->
<script src="/__/firebase/init.js?useEmulator=true'"></script>
<script defer>
function isPasswordStrong(password) {
const hasCapital = !!password.match(/[A-Z]/);
const hasNumber = !!password.match(/[\d]/);
const hasSpecial = !!password.match(
/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/
);
const isLong = password.length >= 8;
return hasCapital && hasNumber && hasSpecial && isLong;
}
function handleVerifyEmail(actionCode, continueUrl) {
firebase
.auth()
@ -232,6 +242,14 @@
return;
}
if (!isPasswordStrong(newPassword)) {
alert(
"Password must be at least 8 characters long and contain at least one capital letter, one number and one special character."
);
showResetPassword();
return;
}
// Save the new password.
firebase
.auth()

View file

@ -1035,7 +1035,8 @@
<h1>pace caret</h1>
<div class="text">
Displays a second caret that moves at constant speed. The 'average'
option averages the speed of last 10 results.
option averages the speed of last 10 results. The 'daily' option takes
the highest speed of the last 24 hours.
</div>
<div>
<div class="inputAndButton">
@ -1085,6 +1086,14 @@
>
last
</div>
<div
class="button"
paceCaret="daily"
tabindex="0"
onclick="this.blur();"
>
daily
</div>
<div
class="button"
paceCaret="custom"