Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ We take the security and integrity of Monkeytype very seriously. If you have fou

### Reporting a Vulnerability

For vulnerabilities that impact the confidentiality, integrity, and availability of Monkeytype services, please send your disclosure via [email](contact@monkeytype.com). For non-security related platform bugs, follow the bug submission [guidelines](https://github.com/monkeytypegame/monkeytype#bug-report-or-feature-request). Include as much detail as possible to ensure reproducibility. At a minimum, vulnerability disclosures should include:
For vulnerabilities that impact the confidentiality, integrity, and availability of Monkeytype services, please send your disclosure via [email](mailto:contact@monkeytype.com). For non-security related platform bugs, follow the bug submission [guidelines](https://github.com/monkeytypegame/monkeytype#bug-report-or-feature-request). Include as much detail as possible to ensure reproducibility. At a minimum, vulnerability disclosures should include:

- Vulnerability Description
- Proof of Concept
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/styles/fonts.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
@use "sass:map";

@font-face {
font-family: "Vazirmatn";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("/webfonts/Vazirmatn-Regular.woff2") format("woff2");
}

@each $font, $config in $fonts {
$dir: "webfonts";
$previewDir: "webfonts-preview";
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/ts/elements/input-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ export type ValidationOptions<T> = (T extends string
callback?: (result: ValidationResult) => void;
};

export type ValidatedHtmlInputElement = HTMLInputElement & {
isValid: () => boolean | undefined;
};
/**
* adds an 'InputIndicator` to the given `inputElement` and updates its status depending on the given validation
* @param inputElement
Expand All @@ -151,7 +154,7 @@ export type ValidationOptions<T> = (T extends string
export function validateWithIndicator<T>(
inputElement: HTMLInputElement,
options: ValidationOptions<T>
): void {
): ValidatedHtmlInputElement {
//use indicator
const indicator = new InputIndicator(inputElement, {
success: {
Expand All @@ -172,7 +175,10 @@ export function validateWithIndicator<T>(
level: 0,
},
});

let isValid: boolean | undefined = undefined;
const callback = (result: ValidationResult): void => {
isValid = result.status === "success" || result.status === "warning";
if (result.status === "failed" || result.status === "warning") {
indicator.show(result.status, result.errorMessage);
} else {
Expand All @@ -188,6 +194,11 @@ export function validateWithIndicator<T>(
);

inputElement.addEventListener("input", handler);

const result = inputElement as ValidatedHtmlInputElement;
result.isValid = () => isValid;

return result;
}

export type ConfigInputOptions<K extends ConfigKey, T = ConfigType[K]> = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/ts/elements/test-activity-calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class TestActivityCalendar implements TestActivityCalendar {
}

protected getInterval(lastDay: Date, fullYear = false): Interval {
const end = fullYear ? endOfYear(lastDay) : new Date();
const end = fullYear ? endOfYear(lastDay) : new UTCDateMini();
let start = startOfYear(lastDay);
if (!fullYear) {
//show the last 52 weeks. Not using one year to avoid the graph to show 54 weeks
Expand Down
19 changes: 9 additions & 10 deletions frontend/src/ts/modals/simple-modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
import {
createErrorMessage,
isDevEnvironment,
isPasswordStrong,
reloadAfter,
} from "../utils/misc";
import * as CustomTextState from "../states/custom-text-name";
Expand All @@ -38,9 +37,14 @@ import {
} from "../utils/simple-modal";
import { ShowOptions } from "../utils/animated-modal";
import { GenerateDataRequest } from "@monkeytype/contracts/dev";
import { UserEmailSchema, UserNameSchema } from "@monkeytype/schemas/users";
import {
PasswordSchema,
UserEmailSchema,
UserNameSchema,
} from "@monkeytype/schemas/users";
import { goToPage } from "../pages/leaderboards";
import FileStorage from "../utils/file-storage";
import { z } from "zod";

type PopupKey =
| "updateEmail"
Expand Down Expand Up @@ -551,6 +555,9 @@ list.updatePassword = new SimpleModal({
placeholder: "new password",
type: "password",
initVal: "",
validation: {
schema: isDevEnvironment() ? z.string().min(6) : PasswordSchema,
},
},
{
placeholder: "confirm new password",
Expand Down Expand Up @@ -580,14 +587,6 @@ list.updatePassword = new SimpleModal({
};
}

if (!isDevEnvironment() && !isPasswordStrong(newPassword)) {
return {
status: 0,
message:
"New password must contain at least one capital letter, number, a special character and must be between 8 and 64 characters long",
};
}

const reauth = await reauthenticate({ password: previousPass });
if (reauth.status !== 1) {
return {
Expand Down
174 changes: 89 additions & 85 deletions frontend/src/ts/pages/login.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Ape from "../ape";
import Page from "./page";
import * as Skeleton from "../utils/skeleton";
import * as Misc from "../utils/misc";
import TypoList from "../utils/typo-list";
import { UserEmailSchema, UserNameSchema } from "@monkeytype/schemas/users";
import {
PasswordSchema,
UserEmailSchema,
UserNameSchema,
} from "@monkeytype/schemas/users";
import { validateWithIndicator } from "../elements/input-validation";
import { isDevEnvironment } from "../utils/misc";
import { z } from "zod";

let registerForm: {
Expand Down Expand Up @@ -39,11 +43,19 @@ export function hidePreloader(): void {
$(".pageLogin .preloader").addClass("hidden");
}

function isFormComplete(): boolean {
return (
registerForm.name !== undefined &&
registerForm.email !== undefined &&
registerForm.password !== undefined
);
}

export const updateSignupButton = (): void => {
if (Object.values(registerForm).some((it) => it === undefined)) {
disableSignUpButton();
} else {
if (isFormComplete()) {
enableSignUpButton();
} else {
disableSignUpButton();
}
};

Expand All @@ -53,9 +65,7 @@ type SignupData = {
password: string;
};
export function getSignupData(): SignupData | false {
return Object.values(registerForm).some((it) => it === undefined)
? false
: (registerForm as SignupData);
return isFormComplete() ? (registerForm as SignupData) : false;
}

const nameInputEl = document.querySelector(
Expand Down Expand Up @@ -84,9 +94,58 @@ let disposableEmailModule: typeof import("disposable-email-domains-js") | null =
null;
let moduleLoadAttempted = false;

const emailInputEl = document.querySelector(
".page.pageLogin .register.side input.emailInput"
) as HTMLInputElement;
const emailInputEl = validateWithIndicator(
document.querySelector(
".page.pageLogin .register.side input.emailInput"
) as HTMLInputElement,
{
schema: UserEmailSchema,
isValid: async (email: string) => {
const educationRegex =
/@.*(student|education|school|\.edu$|\.edu\.|\.ac\.|\.sch\.)/i;
if (educationRegex.test(email)) {
return {
warning:
"Some education emails will fail to receive our messages, or disable the account as soon as you graduate. Consider using a personal email address.",
};
}

const emailHasTypo = TypoList.some((typo) => {
return email.endsWith(typo);
});
if (emailHasTypo) {
return {
warning: "Please check your email address, it may contain a typo.",
};
}

if (
disposableEmailModule &&
disposableEmailModule.isDisposableEmail !== undefined
) {
try {
if (disposableEmailModule.isDisposableEmail(email)) {
return {
warning:
"Using a temporary email may cause issues with logging in, password resets and support. Consider using a permanent email address. Don't worry, we don't send spam.",
};
}
} catch (e) {
// Silent failure
}
}

return true;
},
debounceDelay: 0,
callback: (result) => {
if (result.status === "success") {
//re-validate the verify email
emailVerifyInputEl.dispatchEvent(new Event("input"));
}
},
}
);

emailInputEl.addEventListener("focus", async () => {
if (!moduleLoadAttempted) {
Expand All @@ -99,54 +158,6 @@ emailInputEl.addEventListener("focus", async () => {
}
});

validateWithIndicator(emailInputEl, {
schema: UserEmailSchema,
isValid: async (email: string) => {
const educationRegex =
/@.*(student|education|school|\.edu$|\.edu\.|\.ac\.|\.sch\.)/i;
if (educationRegex.test(email)) {
return {
warning:
"Some education emails will fail to receive our messages, or disable the account as soon as you graduate. Consider using a personal email address.",
};
}

const emailHasTypo = TypoList.some((typo) => {
return email.endsWith(typo);
});
if (emailHasTypo) {
return {
warning: "Please check your email address, it may contain a typo.",
};
}

if (
disposableEmailModule &&
disposableEmailModule.isDisposableEmail !== undefined
) {
try {
if (disposableEmailModule.isDisposableEmail(email)) {
return {
warning:
"Using a temporary email may cause issues with logging in, password resets and support. Consider using a permanent email address. Don't worry, we don't send spam.",
};
}
} catch (e) {
// Silent failure
}
}

return true;
},
debounceDelay: 0,
callback: (result) => {
if (result.status === "success") {
//re-validate the verify email
emailVerifyInputEl.dispatchEvent(new Event("input"));
}
},
});

const emailVerifyInputEl = document.querySelector(
".page.pageLogin .register.side input.verifyEmailInput"
) as HTMLInputElement;
Expand All @@ -159,36 +170,27 @@ validateWithIndicator(emailVerifyInputEl, {
debounceDelay: 0,
callback: (result) => {
registerForm.email =
result.status === "success" ? emailInputEl.value : undefined;
emailInputEl.isValid() && result.status === "success"
? emailInputEl.value
: undefined;
updateSignupButton();
},
});

const passwordInputEl = document.querySelector(
".page.pageLogin .register.side .passwordInput"
) as HTMLInputElement;
validateWithIndicator(passwordInputEl, {
schema: z.string().min(6), //firebase requires min 6 chars, we apply stricter rules on prod
isValid: async (password: string) => {
if (!Misc.isDevEnvironment() && !Misc.isPasswordStrong(password)) {
if (password.length < 8) {
return "Password must be at least 8 characters";
} else if (password.length > 64) {
return "Password must be at most 64 characters";
} else {
return "Password must contain at least one capital letter, number, and special character";
const passwordInputEl = validateWithIndicator(
document.querySelector(
".page.pageLogin .register.side .passwordInput"
) as HTMLInputElement,
{
schema: isDevEnvironment() ? z.string().min(6) : PasswordSchema,
callback: (result) => {
if (result.status === "success") {
//re-validate the verify password
passwordVerifyInputEl.dispatchEvent(new Event("input"));
}
}
return true;
},
debounceDelay: 0,
callback: (result) => {
if (result.status === "success") {
//re-validate the verify password
passwordVerifyInputEl.dispatchEvent(new Event("input"));
}
},
});
},
}
);

const passwordVerifyInputEl = document.querySelector(
".page.pageLogin .register.side .verifyPasswordInput"
Expand All @@ -202,7 +204,9 @@ validateWithIndicator(passwordVerifyInputEl, {
debounceDelay: 0,
callback: (result) => {
registerForm.password =
result.status === "success" ? passwordInputEl.value : undefined;
passwordInputEl.isValid() && result.status === "success"
? passwordInputEl.value
: undefined;
updateSignupButton();
},
});
Expand Down
Loading
Loading