impr: handle mongodb errors, fix user validation (@fehmer)

This commit is contained in:
Christian Fehmer 2025-10-01 12:18:18 +02:00
parent a15d84e0ce
commit e5ae18e0e9
No known key found for this signature in database
GPG key ID: FE53784A69964062
9 changed files with 85 additions and 51 deletions

View file

@ -46,9 +46,11 @@ async function errorHandlingMiddleware(
uid: monkeyError.uid ?? req.ctx?.decodedToken?.uid,
};
let message = "Unknown error";
let isDbError = false;
if (/ECONNREFUSED.*27017/i.test(error.message)) {
message = "Could not connect to the database. It may be down.";
isDbError = true;
} else if (error instanceof URIError || error instanceof SyntaxError) {
status = 400;
message = "Unprocessable request";
@ -73,27 +75,30 @@ async function errorHandlingMiddleware(
errorId: string;
};
try {
await addLog(
"system_error",
`${status} ${errorId} ${error.message} ${error.stack}`,
uid
);
await db.collection<DBError>("errors").insertOne({
_id: errorId,
timestamp: Date.now(),
status: status,
uid,
message: error.message,
stack: error.stack,
endpoint: req.originalUrl,
method: req.method,
url: req.url,
});
} catch (e) {
Logger.error("Logging to db failed.");
Logger.error(getErrorMessage(e) ?? "Unknown error");
console.error(e);
if (!isDbError) {
try {
await addLog(
"system_error",
`${status} ${errorId} ${error.message} ${error.stack}`,
uid
);
await db.collection<DBError>("errors").insertOne({
_id: errorId,
timestamp: Date.now(),
status: status,
uid,
message: error.message,
stack: error.stack,
endpoint: req.originalUrl,
method: req.method,
url: req.url,
});
} catch (e) {
Logger.error("Logging to db failed.");
Logger.error(getErrorMessage(e) ?? "Unknown error");
console.error(e);
}
}
} else {
Logger.error(`Error: ${error.message} Stack: ${error.stack}`);

View file

@ -18,11 +18,24 @@ 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";
import { exit } from "process";
async function bootServer(port: number): Promise<Server> {
try {
Logger.info(`Starting server version ${version}`);
Logger.info(`Starting server in ${process.env["MODE"]} mode`);
process.on("unhandledRejection", (err) => {
const isDbError =
err instanceof Error && /ECONNREFUSED.*27017/i.test(err.message);
if (isDbError) {
Logger.error("Failed to connect to database, ignore error");
} else {
Logger.error("Unhandled rejection: " + getErrorMessage(err));
exit(-1);
}
});
Logger.info(`Connecting to database ${process.env["DB_NAME"]}...`);
await db.connect();
Logger.success("Connected to database");

View file

@ -14,6 +14,8 @@ export type ValidationResult = {
errorMessage?: string;
};
export type IsValidResponse = true | string | { warning: string };
export type Validation<T> = {
/**
* Zod schema to validate the input value against.
@ -28,7 +30,7 @@ export type Validation<T> = {
* @param thisPopup the current modal
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
*/
isValid?: (value: T) => Promise<true | string | { warning: string }>;
isValid?: (value: T) => Promise<IsValidResponse>;
/** custom debounce delay for `isValid` call. defaults to 100 */
debounceDelay?: number;

View file

@ -6,7 +6,7 @@ import { SimpleModal, TextInput } from "../utils/simple-modal";
import { TagNameSchema } from "@monkeytype/schemas/users";
const cleanTagName = (tagName: string): string => tagName.replaceAll(" ", "_");
const tagNameValidation = async (tagName: string): Promise<true | string> => {
const tagNameValidation = async (tagName: string): Promise<IsValidResponse> => {
const validationResult = TagNameSchema.safeParse(cleanTagName(tagName));
if (validationResult.success) return true;
return validationResult.error.errors.map((err) => err.message).join(", ");

View file

@ -16,6 +16,7 @@ import AnimatedModal from "../utils/animated-modal";
import { resetIgnoreAuthCallback } from "../firebase";
import { validateWithIndicator } from "../elements/input-validation";
import { UserNameSchema } from "@monkeytype/schemas/users";
import { apeValidation } from "../utils/remote-validation";
let signedInUser: UserCredential | undefined = undefined;
@ -154,15 +155,10 @@ function disableInput(): void {
validateWithIndicator(nameInputEl, {
schema: UserNameSchema,
isValid: async (name: string) => {
const checkNameResponse = (
await Ape.users.getNameAvailability({
params: { name: name },
})
).status;
return checkNameResponse === 200 ? true : "Name not available";
},
isValid: apeValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
{ errorMessage: "Name not available" }
),
debounceDelay: 1000,
callback: (result) => {
if (result.status === "success") {

View file

@ -45,6 +45,7 @@ import {
import { goToPage } from "../pages/leaderboards";
import FileStorage from "../utils/file-storage";
import { z } from "zod";
import { apeValidation } from "../utils/remote-validation";
type PopupKey =
| "updateEmail"
@ -479,15 +480,10 @@ list.updateName = new SimpleModal({
initVal: "",
validation: {
schema: UserNameSchema,
isValid: async (newName: string) => {
const checkNameResponse = (
await Ape.users.getNameAvailability({
params: { name: newName },
})
).status;
return checkNameResponse === 200 ? true : "Name not available";
},
isValid: apeValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
{ errorMessage: "Name not available" }
),
debounceDelay: 1000,
},
},

View file

@ -10,6 +10,7 @@ import {
import { validateWithIndicator } from "../elements/input-validation";
import { isDevEnvironment } from "../utils/misc";
import { z } from "zod";
import { apeValidation } from "../utils/remote-validation";
let registerForm: {
name?: string;
@ -73,15 +74,10 @@ const nameInputEl = document.querySelector(
) as HTMLInputElement;
validateWithIndicator(nameInputEl, {
schema: UserNameSchema,
isValid: async (name: string) => {
const checkNameResponse = (
await Ape.users.getNameAvailability({
params: { name: name },
})
).status;
return checkNameResponse === 200 ? true : "Name not available";
},
isValid: apeValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
{ errorMessage: "Name not available" }
),
debounceDelay: 1000,
callback: (result) => {
registerForm.name =

View file

@ -0,0 +1,22 @@
import { IsValidResponse } from "../elements/input-validation";
export function apeValidation<T>(
call: (
val: string
) => Promise<{ status: number; body: { data?: T; message: string } }>,
options?: {
check?: (data: T) => IsValidResponse;
errorMessage?: string;
}
): (val: string) => Promise<IsValidResponse> {
return async (val) => {
const result = await call(val);
if (result.status === 200) {
return options?.check?.(result.body.data as T) ?? true;
} else if (result.status >= 500) {
return result.body.message;
} else {
return options?.errorMessage ?? result.body.message;
}
};
}

View file

@ -5,6 +5,7 @@ import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import * as ConnectionState from "../states/connection";
import {
IsValidResponse,
Validation,
ValidationOptions,
ValidationResult,
@ -33,7 +34,10 @@ type CommonInput<TType, TValue> = {
* @param thisPopup the current modal
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
*/
isValid?: (value: string, thisPopup: SimpleModal) => Promise<true | string>;
isValid?: (
value: string,
thisPopup: SimpleModal
) => Promise<IsValidResponse>;
};
};