mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-19 23:06:15 +08:00
refactor: enable no-unsafe-member-access (@miodec) (#5887)
### Description <!-- Please describe the change(s) made in your PR --> ### Checks - [ ] Adding quotes? - [ ] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. - [ ] Adding a language or a theme? - [ ] If is a language, did you edit `_list.json`, `_groups.json` and add `languages.json`? - [ ] If is a theme, did you add the theme.css? - Also please add a screenshot of the theme, it would be extra awesome if you do so! - [ ] Check if any open issues are related to this PR; if so, be sure to tag them below. - [ ] Make sure the PR title follows the Conventional Commits standard. (https://www.conventionalcommits.org for more info) - [ ] Make sure to include your GitHub username prefixed with @ inside parentheses at the end of the PR title. <!-- label(optional scope): pull request title (@your_github_username) --> <!-- I know I know they seem boring but please do them, they help us and you will find out it also helps you.--> Closes # <!-- the issue(s) your PR resolves if any (delete if that is not the case) --> <!-- please also reference any issues and or PRs related to your pull request --> <!-- Also remove it if you are not following any issues. --> <!-- pro tip: you can mention an issue, PR, or discussion on GitHub by referencing its hash number e.g: [#1234](https://github.com/monkeytypegame/monkeytype/pull/1234) --> <!-- pro tip: you can press . (dot or period) in the code tab of any GitHub repo to get access to GitHub's VS Code web editor Enjoy! :) -->
This commit is contained in:
parent
c75ba9a2ba
commit
7e703028bd
|
@ -28,6 +28,7 @@ import { LeaderboardRank } from "@monkeytype/contracts/schemas/leaderboards";
|
|||
import { randomUUID } from "node:crypto";
|
||||
import _ from "lodash";
|
||||
import { MonkeyMail, UserStreak } from "@monkeytype/contracts/schemas/users";
|
||||
import { isFirebaseError } from "../../../src/utils/error";
|
||||
|
||||
const mockApp = request(app);
|
||||
const configuration = Configuration.getCachedConfiguration();
|
||||
|
@ -355,10 +356,16 @@ describe("user controller test", () => {
|
|||
|
||||
it("should fail with too many firebase requests", async () => {
|
||||
//GIVEN
|
||||
adminGenerateVerificationLinkMock.mockRejectedValue({
|
||||
code: "auth/internal-error",
|
||||
message: "TOO_MANY_ATTEMPTS_TRY_LATER",
|
||||
} as FirebaseError);
|
||||
const mockFirebaseError = {
|
||||
code: "auth/too-many-requests",
|
||||
codePrefix: "auth",
|
||||
errorInfo: {
|
||||
code: "auth/too-many-requests",
|
||||
message: "Too many requests",
|
||||
},
|
||||
};
|
||||
adminGenerateVerificationLinkMock.mockRejectedValue(mockFirebaseError);
|
||||
expect(isFirebaseError(mockFirebaseError)).toBe(true);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
@ -371,9 +378,16 @@ describe("user controller test", () => {
|
|||
});
|
||||
it("should fail with firebase user not found", async () => {
|
||||
//GIVEN
|
||||
adminGenerateVerificationLinkMock.mockRejectedValue({
|
||||
const mockFirebaseError = {
|
||||
code: "auth/user-not-found",
|
||||
} as FirebaseError);
|
||||
codePrefix: "auth",
|
||||
errorInfo: {
|
||||
code: "auth/user-not-found",
|
||||
message: "User not found",
|
||||
},
|
||||
};
|
||||
adminGenerateVerificationLinkMock.mockRejectedValue(mockFirebaseError);
|
||||
expect(isFirebaseError(mockFirebaseError)).toBe(true);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
@ -387,11 +401,13 @@ describe("user controller test", () => {
|
|||
'Stack: {"decodedTokenEmail":"newuser@mail.com","userInfoEmail":"newuser@mail.com"}'
|
||||
);
|
||||
});
|
||||
it("should fail with firebase errir", async () => {
|
||||
it("should fail with unknown error", async () => {
|
||||
//GIVEN
|
||||
adminGenerateVerificationLinkMock.mockRejectedValue({
|
||||
message: "Internal error encountered.",
|
||||
} as FirebaseError);
|
||||
const mockFirebaseError = {
|
||||
message: "Internal server error",
|
||||
};
|
||||
adminGenerateVerificationLinkMock.mockRejectedValue(mockFirebaseError);
|
||||
expect(isFirebaseError(mockFirebaseError)).toBe(false);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
@ -401,7 +417,7 @@ describe("user controller test", () => {
|
|||
|
||||
//THEN
|
||||
expect(body.message).toEqual(
|
||||
"Firebase failed to generate an email verification link. Please try again later."
|
||||
"Firebase failed to generate an email verification link: Internal server error"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1039,9 +1055,16 @@ describe("user controller test", () => {
|
|||
});
|
||||
it("should fail for duplicate email", async () => {
|
||||
//GIVEN
|
||||
authUpdateEmailMock.mockRejectedValue({
|
||||
const mockFirebaseError = {
|
||||
code: "auth/email-already-exists",
|
||||
} as FirebaseError);
|
||||
codePrefix: "auth",
|
||||
errorInfo: {
|
||||
code: "auth/email-already-exists",
|
||||
message: "Email already exists",
|
||||
},
|
||||
};
|
||||
authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
|
||||
expect(isFirebaseError(mockFirebaseError)).toBe(true);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
@ -1062,9 +1085,16 @@ describe("user controller test", () => {
|
|||
|
||||
it("should fail for invalid email", async () => {
|
||||
//GIVEN
|
||||
authUpdateEmailMock.mockRejectedValue({
|
||||
const mockFirebaseError = {
|
||||
code: "auth/invalid-email",
|
||||
} as FirebaseError);
|
||||
codePrefix: "auth",
|
||||
errorInfo: {
|
||||
code: "auth/invalid-email",
|
||||
message: "Invalid email",
|
||||
},
|
||||
};
|
||||
authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
|
||||
expect(isFirebaseError(mockFirebaseError)).toBe(true);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
@ -1082,9 +1112,16 @@ describe("user controller test", () => {
|
|||
});
|
||||
it("should fail for too many requests", async () => {
|
||||
//GIVEN
|
||||
authUpdateEmailMock.mockRejectedValue({
|
||||
const mockFirebaseError = {
|
||||
code: "auth/too-many-requests",
|
||||
} as FirebaseError);
|
||||
codePrefix: "auth",
|
||||
errorInfo: {
|
||||
code: "auth/too-many-requests",
|
||||
message: "Too many requests",
|
||||
},
|
||||
};
|
||||
authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
|
||||
expect(isFirebaseError(mockFirebaseError)).toBe(true);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
@ -1102,9 +1139,16 @@ describe("user controller test", () => {
|
|||
});
|
||||
it("should fail for unknown user", async () => {
|
||||
//GIVEN
|
||||
authUpdateEmailMock.mockRejectedValue({
|
||||
const mockFirebaseError = {
|
||||
code: "auth/user-not-found",
|
||||
} as FirebaseError);
|
||||
codePrefix: "auth",
|
||||
errorInfo: {
|
||||
code: "auth/user-not-found",
|
||||
message: "User not found",
|
||||
},
|
||||
};
|
||||
authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
|
||||
expect(isFirebaseError(mockFirebaseError)).toBe(true);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
@ -1126,7 +1170,12 @@ describe("user controller test", () => {
|
|||
//GIVEN
|
||||
authUpdateEmailMock.mockRejectedValue({
|
||||
code: "auth/invalid-user-token",
|
||||
} as FirebaseError);
|
||||
codePrefix: "auth",
|
||||
errorInfo: {
|
||||
code: "auth/invalid-user-token",
|
||||
message: "Invalid user token",
|
||||
},
|
||||
});
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
ToggleBanRequest,
|
||||
ToggleBanResponse,
|
||||
} from "@monkeytype/contracts/admin";
|
||||
import MonkeyError from "../../utils/error";
|
||||
import MonkeyError, { getErrorMessage } from "../../utils/error";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { addImportantLog } from "../../dal/logs";
|
||||
|
||||
|
@ -117,7 +117,10 @@ export async function handleReports(
|
|||
if (e instanceof MonkeyError) {
|
||||
throw new MonkeyError(e.status, e.message);
|
||||
} else {
|
||||
throw new MonkeyError(500, "Error handling reports: " + e.message);
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Error handling reports: " + getErrorMessage(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -247,6 +247,7 @@ async function updateUser(uid: string): Promise<void> {
|
|||
if (lbPersonalBests[mode.mode][mode.mode2] === undefined)
|
||||
lbPersonalBests[mode.mode][mode.mode2] = {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
lbPersonalBests[mode.mode][mode.mode2][mode.language] = entry;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import _ from "lodash";
|
||||
import * as UserDAL from "../../dal/user";
|
||||
import MonkeyError from "../../utils/error";
|
||||
import MonkeyError, {
|
||||
getErrorMessage,
|
||||
isFirebaseError,
|
||||
} from "../../utils/error";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import * as DiscordUtils from "../../utils/discord";
|
||||
import {
|
||||
|
@ -12,7 +15,6 @@ import {
|
|||
sanitizeString,
|
||||
} from "../../utils/misc";
|
||||
import GeorgeQueue from "../../queues/george-queue";
|
||||
import { type FirebaseError } from "firebase-admin";
|
||||
import { deleteAllApeKeys } from "../../dal/ape-keys";
|
||||
import { deleteAllPresets } from "../../dal/preset";
|
||||
import { deleteAll as deleteAllResults } from "../../dal/result";
|
||||
|
@ -183,31 +185,48 @@ export async function sendVerificationEmail(
|
|||
: "https://monkeytype.com",
|
||||
});
|
||||
} catch (e) {
|
||||
const firebaseError = e as FirebaseError;
|
||||
if (
|
||||
firebaseError.code === "auth/internal-error" &&
|
||||
firebaseError.message.includes("TOO_MANY_ATTEMPTS_TRY_LATER")
|
||||
) {
|
||||
// for some reason this error is not handled with a custom auth/ code, so we have to do it manually
|
||||
throw new MonkeyError(429, "Too many requests. Please try again later");
|
||||
}
|
||||
if (firebaseError.code === "auth/user-not-found") {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Auth user not found when the user was found in the database. Contact support with this error message and your email",
|
||||
JSON.stringify({
|
||||
decodedTokenEmail: email,
|
||||
userInfoEmail: userInfo.email,
|
||||
stack: e.stack as unknown,
|
||||
}),
|
||||
userInfo.uid
|
||||
);
|
||||
}
|
||||
if (firebaseError.message.includes("Internal error encountered.")) {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Firebase failed to generate an email verification link. Please try again later."
|
||||
);
|
||||
if (isFirebaseError(e)) {
|
||||
if (e.errorInfo.code === "auth/user-not-found") {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Auth user not found when the user was found in the database. Contact support with this error message and your email",
|
||||
JSON.stringify({
|
||||
decodedTokenEmail: email,
|
||||
userInfoEmail: userInfo.email,
|
||||
}),
|
||||
userInfo.uid
|
||||
);
|
||||
} else if (e.errorInfo.code === "auth/too-many-requests") {
|
||||
throw new MonkeyError(429, "Too many requests. Please try again later");
|
||||
} else {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Firebase failed to generate an email verification link: " +
|
||||
e.errorInfo.message
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const message = getErrorMessage(e);
|
||||
if (message === undefined) {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Firebase failed to generate an email verification link. Unknown error occured"
|
||||
);
|
||||
} else {
|
||||
if (message.toLowerCase().includes("too_many_attempts")) {
|
||||
throw new MonkeyError(
|
||||
429,
|
||||
"Too many requests. Please try again later"
|
||||
);
|
||||
} else {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Firebase failed to generate an email verification link: " +
|
||||
message,
|
||||
(e as Error).stack
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await emailQueue.sendVerificationEmail(email, userInfo.name, link);
|
||||
|
@ -394,24 +413,26 @@ export async function updateEmail(
|
|||
await AuthUtil.updateUserEmail(uid, newEmail);
|
||||
await UserDAL.updateEmail(uid, newEmail);
|
||||
} catch (e) {
|
||||
if (e.code === "auth/email-already-exists") {
|
||||
throw new MonkeyError(
|
||||
409,
|
||||
"The email address is already in use by another account"
|
||||
);
|
||||
} else if (e.code === "auth/invalid-email") {
|
||||
throw new MonkeyError(400, "Invalid email address");
|
||||
} else if (e.code === "auth/too-many-requests") {
|
||||
throw new MonkeyError(429, "Too many requests. Please try again later");
|
||||
} else if (e.code === "auth/user-not-found") {
|
||||
throw new MonkeyError(
|
||||
404,
|
||||
"User not found in the auth system",
|
||||
"update email",
|
||||
uid
|
||||
);
|
||||
} else if (e.code === "auth/invalid-user-token") {
|
||||
throw new MonkeyError(401, "Invalid user token", "update email", uid);
|
||||
if (isFirebaseError(e)) {
|
||||
if (e.code === "auth/email-already-exists") {
|
||||
throw new MonkeyError(
|
||||
409,
|
||||
"The email address is already in use by another account"
|
||||
);
|
||||
} else if (e.code === "auth/invalid-email") {
|
||||
throw new MonkeyError(400, "Invalid email address");
|
||||
} else if (e.code === "auth/too-many-requests") {
|
||||
throw new MonkeyError(429, "Too many requests. Please try again later");
|
||||
} else if (e.code === "auth/user-not-found") {
|
||||
throw new MonkeyError(
|
||||
404,
|
||||
"User not found in the auth system",
|
||||
"update email",
|
||||
uid
|
||||
);
|
||||
} else if (e.code === "auth/invalid-user-token") {
|
||||
throw new MonkeyError(401, "Invalid user token", "update email", uid);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
@ -475,6 +496,7 @@ export async function getUser(
|
|||
try {
|
||||
userInfo = await UserDAL.getUser(uid, "get user");
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (e.status === 404) {
|
||||
//if the user is in the auth system but not in the db, its possible that the user was created by bypassing captcha
|
||||
//since there is no data in the database anyway, we can just delete the user from the auth system
|
||||
|
@ -488,6 +510,7 @@ export async function getUser(
|
|||
uid
|
||||
);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (e.code === "auth/user-not-found") {
|
||||
throw new MonkeyError(
|
||||
404,
|
||||
|
|
|
@ -54,6 +54,7 @@ export async function get(
|
|||
|
||||
return preset;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (e.error === 175) {
|
||||
//QueryPlanKilled, collection was removed during the query
|
||||
return false;
|
||||
|
@ -84,6 +85,7 @@ export async function getRank(
|
|||
entry: entry !== null ? entry : undefined,
|
||||
};
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (e.error === 175) {
|
||||
//QueryPlanKilled, collection was removed during the query
|
||||
return false;
|
||||
|
|
|
@ -735,6 +735,7 @@ export async function getPersonalBests(
|
|||
]);
|
||||
|
||||
if (mode2 !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return user.personalBests?.[mode]?.[mode2] as PersonalBest;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { BASE_CONFIGURATION } from "../constants/base-configuration";
|
|||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { addLog } from "../dal/logs";
|
||||
import { PartialConfiguration } from "@monkeytype/contracts/configuration";
|
||||
import { getErrorMessage } from "../utils/error";
|
||||
|
||||
const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes
|
||||
|
||||
|
@ -86,9 +87,10 @@ export async function getLiveConfiguration(): Promise<Configuration> {
|
|||
}); // Seed the base configuration.
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error) ?? "Unknown error";
|
||||
void addLog(
|
||||
"fetch_configuration_failure",
|
||||
`Could not fetch configuration: ${error.message}`
|
||||
`Could not fetch configuration: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -104,9 +106,10 @@ async function pushConfiguration(configuration: Configuration): Promise<void> {
|
|||
await db.collection("configuration").replaceOne({}, configuration);
|
||||
serverConfigurationUpdated = true;
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error) ?? "Unknown error";
|
||||
void addLog(
|
||||
"push_configuration_failure",
|
||||
`Could not push configuration: ${error.message}`
|
||||
`Could not push configuration: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -124,9 +127,10 @@ export async function patchConfiguration(
|
|||
|
||||
await getLiveConfiguration();
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error) ?? "Unknown error";
|
||||
void addLog(
|
||||
"patch_configuration_failure",
|
||||
`Could not patch configuration: ${error.message}`
|
||||
`Could not patch configuration: ${errorMessage}`
|
||||
);
|
||||
|
||||
return false;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
type MongoClientOptions,
|
||||
type WithId,
|
||||
} from "mongodb";
|
||||
import MonkeyError from "../utils/error";
|
||||
import MonkeyError, { getErrorMessage } from "../utils/error";
|
||||
import Logger from "../utils/logger";
|
||||
|
||||
let db: Db;
|
||||
|
@ -58,7 +58,7 @@ export async function connect(): Promise<void> {
|
|||
await mongoClient.connect();
|
||||
db = mongoClient.db(DB_NAME);
|
||||
} catch (error) {
|
||||
Logger.error(error.message as string);
|
||||
Logger.error(getErrorMessage(error) ?? "Unknown error");
|
||||
Logger.error(
|
||||
"Failed to connect to database. Exiting with exit status code 1."
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import mustache from "mustache";
|
|||
import { recordEmail } from "../utils/prometheus";
|
||||
import type { EmailTaskContexts, EmailType } from "../queues/email-queue";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { getErrorMessage } from "../utils/error";
|
||||
|
||||
type EmailMetadata = {
|
||||
subject: string;
|
||||
|
@ -72,7 +73,7 @@ export async function init(): Promise<void> {
|
|||
Logger.success("Email client configuration verified");
|
||||
} catch (error) {
|
||||
transportInitialized = false;
|
||||
Logger.error(error.message as string);
|
||||
Logger.error(getErrorMessage(error) ?? "Unknown error");
|
||||
Logger.error("Failed to verify email client configuration.");
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +113,7 @@ export async function sendEmail(
|
|||
recordEmail(templateName, "fail");
|
||||
return {
|
||||
success: false,
|
||||
message: e.message as string,
|
||||
message: getErrorMessage(e) ?? "Unknown error",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { join } from "path";
|
|||
import IORedis from "ioredis";
|
||||
import Logger from "../utils/logger";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { getErrorMessage } from "../utils/error";
|
||||
|
||||
let connection: IORedis.Redis;
|
||||
let connected = false;
|
||||
|
@ -53,7 +54,7 @@ export async function connect(): Promise<void> {
|
|||
|
||||
connected = true;
|
||||
} catch (error) {
|
||||
Logger.error(error.message as string);
|
||||
Logger.error(getErrorMessage(error) ?? "Unknown error");
|
||||
if (isDevEnvironment()) {
|
||||
await connection.quit();
|
||||
Logger.warning(
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
RequestAuthenticationOptions,
|
||||
} from "@monkeytype/contracts/schemas/api";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { getMetadata, TsRestRequestWithCtx } from "./utility";
|
||||
|
||||
const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
|
||||
isGithubWebhook: false,
|
||||
|
@ -28,11 +29,6 @@ const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
|
|||
isPublicOnDev: false,
|
||||
};
|
||||
|
||||
export type TsRestRequestWithCtx = {
|
||||
ctx: Readonly<MonkeyTypes.Context>;
|
||||
} & TsRestRequest &
|
||||
ExpressRequest;
|
||||
|
||||
/**
|
||||
* Authenticate request based on the auth settings of the route.
|
||||
* By default a Bearer token with user authentication is required.
|
||||
|
@ -48,7 +44,7 @@ export function authenticateTsRestRequest<
|
|||
): Promise<void> => {
|
||||
const options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...((req.tsRestRoute["metadata"]?.["authenticationOptions"] ??
|
||||
...((getMetadata(req)["authenticationOptions"] ??
|
||||
{}) as EndpointMetadata),
|
||||
};
|
||||
|
||||
|
@ -188,6 +184,7 @@ async function authenticateWithBearerToken(
|
|||
email: decodedToken.email ?? "",
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const errorCode = error?.errorInfo?.code as string | undefined;
|
||||
|
||||
if (errorCode?.includes("auth/id-token-expired")) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { Response, NextFunction } from "express";
|
||||
import { TsRestRequestWithCtx } from "./auth";
|
||||
import { TsRestRequestHandler } from "@ts-rest/express";
|
||||
import { EndpointMetadata } from "@monkeytype/contracts/schemas/api";
|
||||
import MonkeyError from "../utils/error";
|
||||
|
@ -8,6 +7,7 @@ import {
|
|||
ConfigurationPath,
|
||||
RequireConfiguration,
|
||||
} from "@monkeytype/contracts/require-configuration/index";
|
||||
import { getMetadata, TsRestRequestWithCtx } from "./utility";
|
||||
|
||||
export function verifyRequiredConfiguration<
|
||||
T extends AppRouter | AppRoute
|
||||
|
@ -17,9 +17,7 @@ export function verifyRequiredConfiguration<
|
|||
_res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const requiredConfigurations = getRequireConfigurations(
|
||||
req.tsRestRoute["metadata"] as EndpointMetadata | undefined
|
||||
);
|
||||
const requiredConfigurations = getRequireConfigurations(getMetadata(req));
|
||||
|
||||
if (requiredConfigurations === undefined) {
|
||||
next();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as db from "../init/db";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import Logger from "../utils/logger";
|
||||
import MonkeyError from "../utils/error";
|
||||
import MonkeyError, { getErrorMessage } from "../utils/error";
|
||||
import { incrementBadAuth } from "./rate-limit";
|
||||
import type { NextFunction, Response } from "express";
|
||||
import { isCustomCode } from "../constants/monkey-status-codes";
|
||||
|
@ -92,7 +92,7 @@ async function errorHandlingMiddleware(
|
|||
});
|
||||
} catch (e) {
|
||||
Logger.error("Logging to db failed.");
|
||||
Logger.error(e.message as string);
|
||||
Logger.error(getErrorMessage(e) ?? "Unknown error");
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
|
@ -107,7 +107,7 @@ async function errorHandlingMiddleware(
|
|||
return;
|
||||
} catch (e) {
|
||||
Logger.error("Error handling middleware failed.");
|
||||
Logger.error(e.message as string);
|
||||
Logger.error(getErrorMessage(e) ?? "Unknown error");
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@ import type { Response, NextFunction } from "express";
|
|||
import { getPartialUser } from "../dal/user";
|
||||
import { isAdmin } from "../dal/admin-uids";
|
||||
import { TsRestRequestHandler } from "@ts-rest/express";
|
||||
import { TsRestRequestWithCtx } from "./auth";
|
||||
import {
|
||||
EndpointMetadata,
|
||||
RequestAuthenticationOptions,
|
||||
PermissionId,
|
||||
} from "@monkeytype/contracts/schemas/api";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { getMetadata, TsRestRequestWithCtx } from "./utility";
|
||||
|
||||
type RequestPermissionCheck = {
|
||||
type: "request";
|
||||
|
@ -77,9 +77,7 @@ export function verifyPermissions<
|
|||
_res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const metadata = req.tsRestRoute["metadata"] as
|
||||
| EndpointMetadata
|
||||
| undefined;
|
||||
const metadata = getMetadata(req);
|
||||
const requiredPermissionIds = getRequiredPermissionIds(metadata);
|
||||
if (
|
||||
requiredPermissionIds === undefined ||
|
||||
|
|
|
@ -8,8 +8,6 @@ import {
|
|||
type Options,
|
||||
} from "express-rate-limit";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { EndpointMetadata } from "@monkeytype/contracts/schemas/api";
|
||||
import { TsRestRequestWithCtx } from "./auth";
|
||||
import { TsRestRequestHandler } from "@ts-rest/express";
|
||||
import {
|
||||
limits,
|
||||
|
@ -18,6 +16,7 @@ import {
|
|||
Window,
|
||||
} from "@monkeytype/contracts/rate-limit/index";
|
||||
import statuses from "../constants/monkey-status-codes";
|
||||
import { getMetadata, TsRestRequestWithCtx } from "./utility";
|
||||
|
||||
export const REQUEST_MULTIPLIER = isDevEnvironment() ? 100 : 1;
|
||||
|
||||
|
@ -99,8 +98,7 @@ export function rateLimitRequest<
|
|||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const rateLimit = (req.tsRestRoute["metadata"] as EndpointMetadata)
|
||||
?.rateLimit;
|
||||
const rateLimit = getMetadata(req).rateLimit;
|
||||
if (rateLimit === undefined) {
|
||||
next();
|
||||
return;
|
||||
|
|
|
@ -3,7 +3,12 @@ import type { Request, Response, NextFunction, RequestHandler } from "express";
|
|||
import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import MonkeyError from "../utils/error";
|
||||
import { TsRestRequestWithCtx } from "./auth";
|
||||
import { EndpointMetadata } from "@monkeytype/contracts/schemas/api";
|
||||
|
||||
export type TsRestRequestWithCtx = {
|
||||
ctx: Readonly<MonkeyTypes.Context>;
|
||||
} & TsRestRequest &
|
||||
ExpressRequest;
|
||||
|
||||
/**
|
||||
* record the client version from the `x-client-version` or ` client-version` header to prometheus
|
||||
|
@ -35,3 +40,8 @@ export function onlyAvailableOnDev(): MonkeyTypes.RequestHandler {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getMetadata(req: TsRestRequestWithCtx): EndpointMetadata {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return (req.tsRestRoute["metadata"] ?? {}) as EndpointMetadata;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as EmailClient from "./init/email-client";
|
|||
import { init as initFirebaseAdmin } from "./init/firebase-admin";
|
||||
import { createIndicies as leaderboardDbSetup } from "./dal/leaderboards";
|
||||
import { createIndicies as blocklistDbSetup } from "./dal/blocklist";
|
||||
import { getErrorMessage } from "./utils/error";
|
||||
|
||||
async function bootServer(port: number): Promise<Server> {
|
||||
try {
|
||||
|
@ -74,7 +75,8 @@ async function bootServer(port: number): Promise<Server> {
|
|||
recordServerVersion(version);
|
||||
} catch (error) {
|
||||
Logger.error("Failed to boot server");
|
||||
Logger.error(error.message as string);
|
||||
const message = getErrorMessage(error);
|
||||
Logger.error(message ?? "Unknown error");
|
||||
console.error(error);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
|
|
@ -89,7 +89,8 @@ export class WeeklyXpLeaderboard {
|
|||
|
||||
const currentEntryTimeTypedSeconds =
|
||||
currentEntry !== null
|
||||
? (JSON.parse(currentEntry)?.timeTypedSeconds as number | undefined)
|
||||
? (JSON.parse(currentEntry) as { timeTypedSeconds: number | undefined })
|
||||
?.timeTypedSeconds
|
||||
: undefined;
|
||||
|
||||
const totalTimeTypedSeconds =
|
||||
|
|
5
backend/src/types/types.d.ts
vendored
5
backend/src/types/types.d.ts
vendored
|
@ -1,10 +1,9 @@
|
|||
type ObjectId = import("mongodb").ObjectId;
|
||||
|
||||
type ExpressRequest = import("express").Request;
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type TsRestRequest = import("@ts-rest/express").TsRestRequest<any>;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
type AppRoute = import("@ts-rest/core").AppRoute;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type TsRestRequest = import("@ts-rest/express").TsRestRequest<any>;
|
||||
type AppRouter = import("@ts-rest/core").AppRouter;
|
||||
declare namespace MonkeyTypes {
|
||||
export type DecodedToken = {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { type DecodedIdToken, UserRecord } from "firebase-admin/auth";
|
|||
import { isDevEnvironment } from "./misc";
|
||||
import emailQueue from "../queues/email-queue";
|
||||
import * as UserDAL from "../dal/user";
|
||||
import { isFirebaseError } from "./error";
|
||||
|
||||
const tokenCache = new LRUCache<string, DecodedIdToken>({
|
||||
max: 20000,
|
||||
|
@ -105,7 +106,8 @@ export async function sendForgotPasswordEmail(email: string): Promise<void> {
|
|||
|
||||
await emailQueue.sendForgotPasswordEmail(email, name, link);
|
||||
} catch (err) {
|
||||
if (err.errorInfo?.code !== "auth/user-not-found") {
|
||||
if (isFirebaseError(err) && err.errorInfo.code !== "auth/user-not-found") {
|
||||
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,53 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { isDevEnvironment } from "./misc";
|
||||
import { MonkeyServerErrorType } from "@monkeytype/contracts/schemas/api";
|
||||
import { FirebaseError } from "firebase-admin";
|
||||
|
||||
type FirebaseErrorParent = {
|
||||
code: string;
|
||||
errorInfo: FirebaseError;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function isFirebaseError(err: any): err is FirebaseErrorParent {
|
||||
return (
|
||||
typeof err === "object" &&
|
||||
"code" in err &&
|
||||
"errorInfo" in err &&
|
||||
"codePrefix" in err &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
typeof err.errorInfo === "object" &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
"code" in err.errorInfo &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
"message" in err.errorInfo
|
||||
);
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: unknown): string | undefined {
|
||||
let message = "";
|
||||
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
} else if (
|
||||
error !== null &&
|
||||
typeof error === "object" &&
|
||||
"message" in error &&
|
||||
(typeof error.message === "string" || typeof error.message === "number")
|
||||
) {
|
||||
message = `${error.message}`;
|
||||
} else if (typeof error === "string") {
|
||||
message = error;
|
||||
} else if (typeof error === "number") {
|
||||
message = `${error}`;
|
||||
}
|
||||
|
||||
if (message === "") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
class MonkeyError extends Error implements MonkeyServerErrorType {
|
||||
status: number;
|
||||
|
|
|
@ -180,12 +180,13 @@ export function updateLeaderboardPersonalBests(
|
|||
if (!shouldUpdateLeaderboardPersonalBests(result)) {
|
||||
return null;
|
||||
}
|
||||
const mode = result.mode;
|
||||
const mode2 = result.mode2;
|
||||
const lbPb = lbPersonalBests ?? {};
|
||||
const mode = result.mode as keyof typeof lbPb;
|
||||
const mode2 = result.mode2 as unknown as keyof (typeof lbPb)[typeof mode];
|
||||
lbPb[mode] ??= {};
|
||||
lbPb[mode][mode2] ??= {};
|
||||
const bestForEveryLanguage = {};
|
||||
|
||||
const bestForEveryLanguage: Record<string, PersonalBest> = {};
|
||||
(userPersonalBests[mode][mode2] as PersonalBest[]).forEach(
|
||||
(pb: PersonalBest) => {
|
||||
const language = pb.language;
|
||||
|
@ -198,12 +199,14 @@ export function updateLeaderboardPersonalBests(
|
|||
}
|
||||
);
|
||||
_.each(bestForEveryLanguage, (pb: PersonalBest, language: string) => {
|
||||
const languageDoesNotExist = lbPb[mode][mode2][language] === undefined;
|
||||
const languageIsEmpty = _.isEmpty(lbPb[mode][mode2][language]);
|
||||
const languageDoesNotExist = lbPb[mode][mode2]?.[language] === undefined;
|
||||
const languageIsEmpty = _.isEmpty(lbPb[mode][mode2]?.[language]);
|
||||
|
||||
if (
|
||||
languageDoesNotExist ||
|
||||
languageIsEmpty ||
|
||||
lbPb[mode][mode2][language].wpm < pb.wpm
|
||||
(languageDoesNotExist ||
|
||||
languageIsEmpty ||
|
||||
(lbPb[mode][mode2]?.[language]?.wpm ?? 0) < pb.wpm) &&
|
||||
lbPb[mode][mode2] !== undefined
|
||||
) {
|
||||
lbPb[mode][mode2][language] = pb;
|
||||
}
|
||||
|
|
|
@ -215,6 +215,8 @@ export function recordAuthTime(
|
|||
time: number,
|
||||
req: Request
|
||||
): void {
|
||||
// for some reason route is not in the types
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const reqPath = req.baseUrl + req.route.path;
|
||||
|
||||
let normalizedPath = "/";
|
||||
|
@ -234,6 +236,8 @@ const requestCountry = new Counter({
|
|||
});
|
||||
|
||||
export function recordRequestCountry(country: string, req: Request): void {
|
||||
// for some reason route is not in the types
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const reqPath = req.baseUrl + req.route.path;
|
||||
|
||||
let normalizedPath = "/";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { debounce } from "throttle-debounce";
|
||||
// import * as Numbers from "../utils/numbers";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { envConfig } from "../constants/env-config";
|
||||
const siteKey = envConfig.recaptchaSiteKey;
|
||||
|
||||
|
@ -13,7 +16,6 @@ export function render(
|
|||
}
|
||||
|
||||
//@ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||
const widgetId = grecaptcha.render(element, {
|
||||
sitekey: siteKey,
|
||||
callback,
|
||||
|
|
|
@ -95,7 +95,7 @@ class ChartWithUpdateColors<
|
|||
id: DatasetIds extends never ? never : "x" | DatasetIds
|
||||
): DatasetIds extends never ? never : CartesianScaleOptions {
|
||||
//@ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return this.options.scales[id];
|
||||
}
|
||||
}
|
||||
|
@ -1119,6 +1119,7 @@ async function updateColors<
|
|||
|
||||
//@ts-expect-error
|
||||
chart.data.datasets[0].borderColor = (ctx): string => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const isPb = ctx.raw?.isPb as boolean;
|
||||
const color = isPb ? textcolor : maincolor;
|
||||
return color;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
export function init(): void {
|
||||
$("head").append(`<script>
|
||||
!function(e){var s=new XMLHttpRequest;s.open("GET","https://api.enthusiastgaming.net/scripts/cdn.enthusiast.gg/script/eg-aps/release/eg-aps-bootstrap-v2.0.0.bundle.js?site=monkeytype.com",!0),s.onreadystatechange=function(){var t;4==s.readyState&&(200<=s.status&&s.status<300||304==s.status)&&((t=e.createElement("script")).type="text/javascript",t.text=s.responseText,e.head.appendChild(t))},s.send(null)}(document);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
//@ts-nocheck
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ window.onerror = function (message, url, line, column, error): void {
|
|||
|
||||
window.onunhandledrejection = function (e): void {
|
||||
if (Misc.isDevEnvironment()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const message = (e.reason.message ?? e.reason) as string;
|
||||
Notifications.add(`${message}`, -1, {
|
||||
customTitle: "DEV: Unhandled rejection",
|
||||
|
@ -54,6 +55,7 @@ window.onunhandledrejection = function (e): void {
|
|||
console.error(e);
|
||||
}
|
||||
void log("error", {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
error: (e.reason.stack ?? "") as string,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -426,7 +426,7 @@ function reset(): void {
|
|||
$(".pageSettings .section[data-config-name='fontFamily'] .buttons").empty();
|
||||
for (const select of document.querySelectorAll(".pageSettings select")) {
|
||||
//@ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
select?.slim?.destroy?.();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as AdController from "../controllers/ad-controller";
|
||||
import * as Skeleton from "../utils/skeleton";
|
||||
|
@ -38,7 +40,6 @@ export async function show(): Promise<void> {
|
|||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, 125, () => {
|
||||
//@ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
window.dataLayer.push({ event: "EG_Video" });
|
||||
});
|
||||
}
|
||||
|
|
|
@ -325,7 +325,9 @@ $("#replayWords").on("click", "letter", (event) => {
|
|||
const replayWords = document.querySelector("#replayWords");
|
||||
|
||||
const words = [...(replayWords?.children ?? [])];
|
||||
targetWordPos = words.indexOf(event.target.parentNode as HTMLElement);
|
||||
targetWordPos = words.indexOf(
|
||||
(event.target as HTMLElement).parentNode as HTMLElement
|
||||
);
|
||||
const letters = [...(words[targetWordPos] as HTMLElement).children];
|
||||
targetCurPos = letters.indexOf(event.target as HTMLElement);
|
||||
|
||||
|
|
|
@ -286,6 +286,7 @@ export async function getSection(language: string): Promise<Section> {
|
|||
sectionReq.onload = (): void => {
|
||||
if (sectionReq.readyState === 4) {
|
||||
if (sectionReq.status === 200) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
let sectionText = JSON.parse(sectionReq.responseText).query.pages[
|
||||
pageid.toString()
|
||||
].extract as string;
|
||||
|
|
|
@ -79,10 +79,7 @@ module.exports = {
|
|||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
|
||||
// TODO: enable at some point
|
||||
"@typescript-eslint/no-unsafe-member-access": "off", //~105
|
||||
//
|
||||
|
||||
"@typescript-eslint/no-unsafe-member-access": "error",
|
||||
"@typescript-eslint/no-unsafe-call": "error",
|
||||
"@typescript-eslint/no-unsafe-argument": "error",
|
||||
"@typescript-eslint/no-unsafe-assignment": "error",
|
||||
|
|
Loading…
Reference in a new issue