From 7e703028bd621b3c86bcfeeaa540be65baf30968 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 18 Sep 2024 12:56:52 +0200 Subject: [PATCH] refactor: enable no-unsafe-member-access (@miodec) (#5887) ### Description ### 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. Closes # --- .../__tests__/api/controllers/user.spec.ts | 89 ++++++++++---- backend/src/api/controllers/admin.ts | 7 +- backend/src/api/controllers/dev.ts | 1 + backend/src/api/controllers/user.ts | 113 +++++++++++------- backend/src/dal/leaderboards.ts | 2 + backend/src/dal/user.ts | 1 + backend/src/init/configuration.ts | 10 +- backend/src/init/db.ts | 4 +- backend/src/init/email-client.ts | 5 +- backend/src/init/redis.ts | 3 +- backend/src/middlewares/auth.ts | 9 +- backend/src/middlewares/configuration.ts | 6 +- backend/src/middlewares/error.ts | 6 +- backend/src/middlewares/permission.ts | 6 +- backend/src/middlewares/rate-limit.ts | 6 +- backend/src/middlewares/utility.ts | 12 +- backend/src/server.ts | 4 +- backend/src/services/weekly-xp-leaderboard.ts | 3 +- backend/src/types/types.d.ts | 5 +- backend/src/utils/auth.ts | 4 +- backend/src/utils/error.ts | 47 ++++++++ backend/src/utils/pb.ts | 19 +-- backend/src/utils/prometheus.ts | 4 + frontend/src/ts/controllers/ad-controller.ts | 1 + .../src/ts/controllers/captcha-controller.ts | 4 +- .../src/ts/controllers/chart-controller.ts | 3 +- .../src/ts/controllers/eg-ad-controller.ts | 1 + .../src/ts/controllers/pw-ad-controller.ts | 1 + frontend/src/ts/event-handlers/global.ts | 2 + frontend/src/ts/pages/settings.ts | 2 +- frontend/src/ts/popups/video-ad-popup.ts | 3 +- frontend/src/ts/test/replay.ts | 4 +- frontend/src/ts/test/wikipedia.ts | 1 + packages/eslint-config/index.js | 5 +- 34 files changed, 273 insertions(+), 120 deletions(-) diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index ee37253bc..4e9cc9730 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -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 diff --git a/backend/src/api/controllers/admin.ts b/backend/src/api/controllers/admin.ts index b558f3935..518c50e05 100644 --- a/backend/src/api/controllers/admin.ts +++ b/backend/src/api/controllers/admin.ts @@ -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) + ); } } } diff --git a/backend/src/api/controllers/dev.ts b/backend/src/api/controllers/dev.ts index 036a1135d..1d80824e5 100644 --- a/backend/src/api/controllers/dev.ts +++ b/backend/src/api/controllers/dev.ts @@ -247,6 +247,7 @@ async function updateUser(uid: string): Promise { 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; } diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 33f3e4268..b8b48b702 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -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, diff --git a/backend/src/dal/leaderboards.ts b/backend/src/dal/leaderboards.ts index 7868b4031..9f833ca38 100644 --- a/backend/src/dal/leaderboards.ts +++ b/backend/src/dal/leaderboards.ts @@ -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; diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index d41929430..21d9805d1 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -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; } diff --git a/backend/src/init/configuration.ts b/backend/src/init/configuration.ts index 6a59d4df1..4ac432208 100644 --- a/backend/src/init/configuration.ts +++ b/backend/src/init/configuration.ts @@ -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 { }); // 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 { 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; diff --git a/backend/src/init/db.ts b/backend/src/init/db.ts index 9d556f94d..1e93ad0d6 100644 --- a/backend/src/init/db.ts +++ b/backend/src/init/db.ts @@ -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 { 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." ); diff --git a/backend/src/init/email-client.ts b/backend/src/init/email-client.ts index 3c4874392..8d2ebb71f 100644 --- a/backend/src/init/email-client.ts +++ b/backend/src/init/email-client.ts @@ -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 { 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", }; } diff --git a/backend/src/init/redis.ts b/backend/src/init/redis.ts index f74aaaae5..feeeb1c4f 100644 --- a/backend/src/init/redis.ts +++ b/backend/src/init/redis.ts @@ -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 { connected = true; } catch (error) { - Logger.error(error.message as string); + Logger.error(getErrorMessage(error) ?? "Unknown error"); if (isDevEnvironment()) { await connection.quit(); Logger.warning( diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index b2aff30c8..f42aa1bf9 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -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; -} & 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 => { 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")) { diff --git a/backend/src/middlewares/configuration.ts b/backend/src/middlewares/configuration.ts index 07e8f233a..c1990cb46 100644 --- a/backend/src/middlewares/configuration.ts +++ b/backend/src/middlewares/configuration.ts @@ -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 => { - const requiredConfigurations = getRequireConfigurations( - req.tsRestRoute["metadata"] as EndpointMetadata | undefined - ); + const requiredConfigurations = getRequireConfigurations(getMetadata(req)); if (requiredConfigurations === undefined) { next(); diff --git a/backend/src/middlewares/error.ts b/backend/src/middlewares/error.ts index 4d6bfef89..3d45a853c 100644 --- a/backend/src/middlewares/error.ts +++ b/backend/src/middlewares/error.ts @@ -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); } diff --git a/backend/src/middlewares/permission.ts b/backend/src/middlewares/permission.ts index 2c22a9153..60b4621aa 100644 --- a/backend/src/middlewares/permission.ts +++ b/backend/src/middlewares/permission.ts @@ -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 => { - const metadata = req.tsRestRoute["metadata"] as - | EndpointMetadata - | undefined; + const metadata = getMetadata(req); const requiredPermissionIds = getRequiredPermissionIds(metadata); if ( requiredPermissionIds === undefined || diff --git a/backend/src/middlewares/rate-limit.ts b/backend/src/middlewares/rate-limit.ts index 5f5deadb0..cb158e596 100644 --- a/backend/src/middlewares/rate-limit.ts +++ b/backend/src/middlewares/rate-limit.ts @@ -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 => { - const rateLimit = (req.tsRestRoute["metadata"] as EndpointMetadata) - ?.rateLimit; + const rateLimit = getMetadata(req).rateLimit; if (rateLimit === undefined) { next(); return; diff --git a/backend/src/middlewares/utility.ts b/backend/src/middlewares/utility.ts index 7bee3eace..2d29f0180 100644 --- a/backend/src/middlewares/utility.ts +++ b/backend/src/middlewares/utility.ts @@ -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; +} & 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; +} diff --git a/backend/src/server.ts b/backend/src/server.ts index ca8c315d2..2f4a0a5fe 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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 { try { @@ -74,7 +75,8 @@ async function bootServer(port: number): Promise { 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); } diff --git a/backend/src/services/weekly-xp-leaderboard.ts b/backend/src/services/weekly-xp-leaderboard.ts index df7e6e6ef..11f09178f 100644 --- a/backend/src/services/weekly-xp-leaderboard.ts +++ b/backend/src/services/weekly-xp-leaderboard.ts @@ -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 = diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index bb9ae13f4..d662b0733 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -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; -/* 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; type AppRouter = import("@ts-rest/core").AppRouter; declare namespace MonkeyTypes { export type DecodedToken = { diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index e76b9440e..6145bcdf2 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -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({ max: 20000, @@ -105,7 +106,8 @@ export async function sendForgotPasswordEmail(email: string): Promise { 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; } } diff --git a/backend/src/utils/error.ts b/backend/src/utils/error.ts index 862125492..2646c55c1 100644 --- a/backend/src/utils/error.ts +++ b/backend/src/utils/error.ts @@ -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; diff --git a/backend/src/utils/pb.ts b/backend/src/utils/pb.ts index 6011083f4..b20f801cb 100644 --- a/backend/src/utils/pb.ts +++ b/backend/src/utils/pb.ts @@ -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 = {}; (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; } diff --git a/backend/src/utils/prometheus.ts b/backend/src/utils/prometheus.ts index b86ce52ef..1fe1e3349 100644 --- a/backend/src/utils/prometheus.ts +++ b/backend/src/utils/prometheus.ts @@ -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 = "/"; diff --git a/frontend/src/ts/controllers/ad-controller.ts b/frontend/src/ts/controllers/ad-controller.ts index e68db230a..f751f3ee7 100644 --- a/frontend/src/ts/controllers/ad-controller.ts +++ b/frontend/src/ts/controllers/ad-controller.ts @@ -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"; diff --git a/frontend/src/ts/controllers/captcha-controller.ts b/frontend/src/ts/controllers/captcha-controller.ts index 68bb1b972..79e12d7d8 100644 --- a/frontend/src/ts/controllers/captcha-controller.ts +++ b/frontend/src/ts/controllers/captcha-controller.ts @@ -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, diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index d2eb9ca7d..e4ea43c81 100644 --- a/frontend/src/ts/controllers/chart-controller.ts +++ b/frontend/src/ts/controllers/chart-controller.ts @@ -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; diff --git a/frontend/src/ts/controllers/eg-ad-controller.ts b/frontend/src/ts/controllers/eg-ad-controller.ts index ff09ed60d..4cf446235 100644 --- a/frontend/src/ts/controllers/eg-ad-controller.ts +++ b/frontend/src/ts/controllers/eg-ad-controller.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ export function init(): void { $("head").append(`