mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-08 14:42:46 +08:00
impr: handle mongodb errors, fix user validation (@fehmer)
This commit is contained in:
parent
a15d84e0ce
commit
e5ae18e0e9
9 changed files with 85 additions and 51 deletions
|
@ -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}`);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(", ");
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 =
|
||||
|
|
22
frontend/src/ts/utils/remote-validation.ts
Normal file
22
frontend/src/ts/utils/remote-validation.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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>;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue