From 970a1b175854c53fd1600d1976e3475bb691ed23 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 28 Feb 2026 09:12:02 +0100 Subject: [PATCH 1/2] chore: add no mixed nullish coalescing rule --- backend/__tests__/__testData__/connections.ts | 5 +- packages/oxlint-config/plugin.jsonc | 1 + .../oxlint-config/plugins/monkeytype-rules.js | 50 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/__testData__/connections.ts b/backend/__tests__/__testData__/connections.ts index a927ad4985a2..70fc8b0dfc33 100644 --- a/backend/__tests__/__testData__/connections.ts +++ b/backend/__tests__/__testData__/connections.ts @@ -5,14 +5,15 @@ export async function createConnection( data: Partial, maxPerUser = 25, ): Promise { + const defaultName = "user" + new ObjectId().toHexString(); const result = await ConnectionsDal.create( { uid: data.initiatorUid ?? new ObjectId().toHexString(), - name: data.initiatorName ?? "user" + new ObjectId().toHexString(), + name: data.initiatorName ?? defaultName, }, { uid: data.receiverUid ?? new ObjectId().toHexString(), - name: data.receiverName ?? "user" + new ObjectId().toHexString(), + name: data.receiverName ?? defaultName, }, maxPerUser, ); diff --git a/packages/oxlint-config/plugin.jsonc b/packages/oxlint-config/plugin.jsonc index e7c8a441c43e..3fbf3b81a41b 100644 --- a/packages/oxlint-config/plugin.jsonc +++ b/packages/oxlint-config/plugin.jsonc @@ -13,6 +13,7 @@ "rules": { "all": "off", "monkeytype-rules/no-testing-access": "error", + "monkeytype-rules/no-mixed-nullish-coalescing": "error", }, "overrides": [ { diff --git a/packages/oxlint-config/plugins/monkeytype-rules.js b/packages/oxlint-config/plugins/monkeytype-rules.js index a868639aae73..82fb3b5cf6ef 100644 --- a/packages/oxlint-config/plugins/monkeytype-rules.js +++ b/packages/oxlint-config/plugins/monkeytype-rules.js @@ -198,6 +198,56 @@ const plugin = { }; }, }), + "no-mixed-nullish-coalescing": defineRule({ + createOnce(context) { + /** + * Returns true for expression node types that, when combined with ?? + * without explicit parentheses, create confusing/ambiguous precedence. + * Excluded: UnaryExpression (clearly bound to its operand), + * LogicalExpression with ?? (same operator, unambiguous). + */ + const isParenthesized = (node, source) => { + // OXC strips ParenthesizedExpression from the AST before visiting, + // so check the raw source surrounding the node's range instead. + const start = node.range?.[0] ?? node.start; + const end = node.range?.[1] ?? node.end; + return source[start - 1] === "(" && source[end] === ")"; + }; + + const isMixedOperatorNode = (node, source) => { + if (isParenthesized(node, source)) return false; + return ( + node.type === "BinaryExpression" || + (node.type === "LogicalExpression" && node.operator !== "??") || + node.type === "ConditionalExpression" + ); + }; + + return { + LogicalExpression(node) { + if (node.operator !== "??") return; + + const source = context.sourceCode.getText(); + + if (isMixedOperatorNode(node.left, source)) { + context.report({ + node: node.left, + message: + "Nullish coalescing (`??`) mixed with other operators without explicit parentheses. Extract to a helper variable or wrap in parentheses for clarity.", + }); + } + + if (isMixedOperatorNode(node.right, source)) { + context.report({ + node: node.right, + message: + "Nullish coalescing (`??`) mixed with other operators without explicit parentheses. Extract to a helper variable or wrap in parentheses for clarity.", + }); + } + }, + }; + }, + }), "component-pascal-case": defineRule({ createOnce(context) { const isPascalCase = (name) => /^[A-Z][a-zA-Z0-9]*$/.test(name); From 2fd6a660898593dee5cae5ddfe65e191e6726604 Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 28 Feb 2026 10:22:32 +0100 Subject: [PATCH 2/2] refactor: auth unwrap (@miodec) (#7553) Remove dom handling from auth.tsx --- frontend/src/ts/auth.tsx | 251 ++++++------------ .../src/ts/controllers/route-controller.ts | 32 +++ frontend/src/ts/event-handlers/nav.ts | 6 + frontend/src/ts/index.ts | 1 + frontend/src/ts/modals/dev-options.ts | 12 +- frontend/src/ts/observables/auth-event.ts | 5 +- frontend/src/ts/pages/account-settings.ts | 9 + frontend/src/ts/pages/account.ts | 13 + frontend/src/ts/pages/login.ts | 115 ++++++++ 9 files changed, 261 insertions(+), 183 deletions(-) create mode 100644 frontend/src/ts/event-handlers/nav.ts diff --git a/frontend/src/ts/auth.tsx b/frontend/src/ts/auth.tsx index ea1de43e560e..5adf922c78a0 100644 --- a/frontend/src/ts/auth.tsx +++ b/frontend/src/ts/auth.tsx @@ -10,7 +10,6 @@ import { import Ape from "./ape"; import { updateFromServer as updateConfigFromServer } from "./config"; -import { navigate } from "./controllers/route-controller"; import * as DB from "./db"; import * as Notifications from "./elements/notifications"; import { @@ -26,18 +25,16 @@ import { import * as RegisterCaptchaModal from "./modals/register-captcha"; import { showPopup } from "./modals/simple-modals-base"; import * as AuthEvent from "./observables/auth-event"; -import * as LoginPage from "./pages/login"; import * as Sentry from "./sentry"; import { showLoaderBar, hideLoaderBar } from "./signals/loader-bar"; import * as ConnectionState from "./states/connection"; import { addBanner } from "./stores/banners"; -import { qs, qsa } from "./utils/dom"; import * as Misc from "./utils/misc"; export const gmailProvider = new GoogleAuthProvider(); export const githubProvider = new GithubAuthProvider(); -async function sendVerificationEmail(): Promise { +export async function sendVerificationEmail(): Promise { if (!isAuthAvailable()) { Notifications.add("Authentication uninitialized", -1, { duration: 3, @@ -46,9 +43,7 @@ async function sendVerificationEmail(): Promise { } showLoaderBar(); - qs(".sendVerificationEmail")?.disable(); const response = await Ape.users.verificationEmail(); - qs(".sendVerificationEmail")?.enable(); if (response.status !== 200) { hideLoaderBar(); Notifications.add("Failed to request verification email", -1, { response }); @@ -97,8 +92,6 @@ async function getDataAndInit(): Promise { return true; } catch (error) { console.error(error); - LoginPage.enableInputs(); - qs("header nav .view-account")?.setStyle({ opacity: "1" }); if (error instanceof DB.SnapshotInitError) { if (error.responseCode === 429) { Notifications.add( @@ -155,127 +148,95 @@ export async function onAuthStateChanged( void Sentry.clearUser(); } - let keyframes = [ - { - percentage: 90, - durationMs: 1000, - text: "Downloading user data...", - }, - ]; - - //undefined means navigate to whatever the current window.location.pathname is - await navigate(undefined, { - force: true, - loadingOptions: { - loadingMode: () => { - if (user !== null) { - return "sync"; - } else { - return "none"; - } - }, - loadingPromise: async () => { - await userPromise; - }, - style: "bar", - keyframes: keyframes, - }, - }); - AuthEvent.dispatch({ type: "authStateChanged", - data: { isUserSignedIn: user !== null }, + data: { isUserSignedIn: user !== null, loadPromise: userPromise }, }); } -export async function signIn(email: string, password: string): Promise { +export async function signIn( + email: string, + password: string, + rememberMe: boolean, +): Promise< + | { + success: true; + } + | { + success: false; + message: string; + } +> { if (!isAuthAvailable()) { - Notifications.add("Authentication uninitialized", -1); - return; - } - if (!ConnectionState.get()) { - Notifications.add("You are offline", 0, { - duration: 2, - }); - return; + return { success: false, message: "Authentication uninitialized" }; } - LoginPage.showPreloader(); - LoginPage.disableInputs(); - LoginPage.disableSignUpButton(); - - if (email === "" || password === "") { - Notifications.add("Please fill in all fields", 0); - LoginPage.hidePreloader(); - LoginPage.enableInputs(); - LoginPage.enableSignUpButton(); - return; - } - - const rememberMe = - qs(".pageLogin .login #rememberMe input")?.isChecked() ?? - false; - const { error } = await tryCatch( signInWithEmailAndPassword(email, password, rememberMe), ); if (error !== null) { - Notifications.add(error.message, -1); - LoginPage.hidePreloader(); - LoginPage.enableInputs(); - LoginPage.updateSignupButton(); - return; + return { success: false, message: error.message }; } + return { success: true }; } -async function signInWithProvider(provider: AuthProvider): Promise { +async function signInWithProvider( + provider: AuthProvider, + rememberMe: boolean, +): Promise< + | { + success: true; + } + | { + success: false; + message: string; + } +> { if (!isAuthAvailable()) { - Notifications.add("Authentication uninitialized", -1, { - duration: 3, - }); - return; - } - if (!ConnectionState.get()) { - Notifications.add("You are offline", 0, { - duration: 2, - }); - return; + return { success: false, message: "Authentication uninitialized" }; } - LoginPage.showPreloader(); - LoginPage.disableInputs(); - LoginPage.disableSignUpButton(); - const rememberMe = - qs(".pageLogin .login #rememberMe input")?.isChecked() ?? - false; - const { error } = await tryCatch(signInWithPopup(provider, rememberMe)); if (error !== null) { if (error.message !== "") { Notifications.add(error.message, -1); } - LoginPage.hidePreloader(); - LoginPage.enableInputs(); - LoginPage.updateSignupButton(); - return; + return { success: false, message: error.message }; } + return { success: true }; } -async function signInWithGoogle(): Promise { - return signInWithProvider(gmailProvider); +export async function signInWithGoogle(rememberMe: boolean): Promise< + | { + success: true; + } + | { + success: false; + message: string; + } +> { + return signInWithProvider(gmailProvider, rememberMe); } -async function signInWithGitHub(): Promise { - return signInWithProvider(githubProvider); +export async function signInWithGitHub(rememberMe: boolean): Promise< + | { + success: true; + } + | { + success: false; + message: string; + } +> { + return signInWithProvider(githubProvider, rememberMe); } -async function addGoogleAuth(): Promise { +export async function addGoogleAuth(): Promise { return addAuthProvider("Google", gmailProvider); } -async function addGithubAuth(): Promise { +export async function addGithubAuth(): Promise { return addAuthProvider("GitHub", githubProvider); } @@ -324,38 +285,27 @@ export function signOut(): void { void authSignOut(); } -async function signUp(): Promise { +export async function signUp( + name: string, + email: string, + password: string, +): Promise< + | { + success: true; + } + | { + success: false; + message: string; + } +> { if (!isAuthAvailable()) { - Notifications.add("Authentication uninitialized", -1, { - duration: 3, - }); - return; - } - if (!ConnectionState.get()) { - Notifications.add("You are offline", 0, { - duration: 2, - }); - return; + return { success: false, message: "Authentication uninitialized" }; } await RegisterCaptchaModal.show(); const captchaToken = await RegisterCaptchaModal.promise; if (captchaToken === undefined || captchaToken === "") { - Notifications.add("Please complete the captcha", -1); - return; + return { success: false, message: "Please complete the captcha" }; } - LoginPage.disableInputs(); - LoginPage.disableSignUpButton(); - LoginPage.showPreloader(); - - const signupData = LoginPage.getSignupData(); - if (signupData === false) { - LoginPage.hidePreloader(); - LoginPage.enableInputs(); - LoginPage.updateSignupButton(); - Notifications.add("Please fill in all fields", 0); - return; - } - const { name: nname, email, password } = signupData; try { const createdAuthUser = await createUserWithEmailAndPassword( @@ -365,7 +315,7 @@ async function signUp(): Promise { const signInResponse = await Ape.users.create({ body: { - name: nname, + name: name, captcha: captchaToken, email, uid: createdAuthUser.user.uid, @@ -375,13 +325,13 @@ async function signUp(): Promise { throw new Error(`Failed to sign in: ${signInResponse.body.message}`); } - await updateProfile(createdAuthUser.user, { displayName: nname }); + await updateProfile(createdAuthUser.user, { displayName: name }); await sendVerificationEmail(); - LoginPage.hidePreloader(); await onAuthStateChanged(true, createdAuthUser.user); resetIgnoreAuthCallback(); Notifications.add("Account created", 1); + return { success: true }; } catch (e) { let message = Misc.createErrorMessage(e, "Failed to create account"); @@ -395,60 +345,7 @@ async function signUp(): Promise { } Notifications.add(message, -1); - LoginPage.hidePreloader(); - LoginPage.enableInputs(); - LoginPage.updateSignupButton(); signOut(); - return; + return { success: false, message }; } } - -qs(".pageLogin .login form")?.on("submit", (e) => { - e.preventDefault(); - const email = - qsa(".pageLogin .login input")?.[0]?.getValue() ?? ""; - const password = - qsa(".pageLogin .login input")?.[1]?.getValue() ?? ""; - void signIn(email, password); -}); - -qs(".pageLogin .login button.signInWithGoogle")?.on("click", () => { - void signInWithGoogle(); -}); - -qs(".pageLogin .login button.signInWithGitHub")?.on("click", () => { - void signInWithGitHub(); -}); - -qs("nav .accountButtonAndMenu .menu button.signOut")?.on("click", () => { - if (!isAuthAvailable()) { - Notifications.add("Authentication uninitialized", -1, { - duration: 3, - }); - return; - } - signOut(); -}); - -qs(".pageLogin .register form")?.on("submit", (e) => { - e.preventDefault(); - void signUp(); -}); - -qs(".pageAccountSettings")?.onChild("click", "#addGoogleAuth", () => { - void addGoogleAuth(); -}); - -qs(".pageAccountSettings")?.onChild("click", "#addGithubAuth", () => { - void addGithubAuth(); -}); - -qs(".pageAccount")?.onChild("click", ".sendVerificationEmail", () => { - if (!ConnectionState.get()) { - Notifications.add("You are offline", 0, { - duration: 2, - }); - return; - } - void sendVerificationEmail(); -}); diff --git a/frontend/src/ts/controllers/route-controller.ts b/frontend/src/ts/controllers/route-controller.ts index bf18c4fd983b..ad3310dc67ce 100644 --- a/frontend/src/ts/controllers/route-controller.ts +++ b/frontend/src/ts/controllers/route-controller.ts @@ -6,6 +6,7 @@ import { isFunboxActive } from "../test/funbox/list"; import * as TestState from "../test/test-state"; import * as Notifications from "../elements/notifications"; import * as NavigationEvent from "../observables/navigation-event"; +import * as AuthEvent from "../observables/auth-event"; //source: https://www.youtube.com/watch?v=OstALBk-jTc // https://www.youtube.com/watch?v=OstALBk-jTc @@ -248,3 +249,34 @@ document.addEventListener("DOMContentLoaded", () => { NavigationEvent.subscribe((url, options) => { void navigate(url, options); }); + +AuthEvent.subscribe((event) => { + if (event.type === "authStateChanged") { + let keyframes = [ + { + percentage: 90, + durationMs: 1000, + text: "Downloading user data...", + }, + ]; + + //undefined means navigate to whatever the current window.location.pathname is + void navigate(undefined, { + force: true, + loadingOptions: { + loadingMode: () => { + if (event.data.isUserSignedIn) { + return "sync"; + } else { + return "none"; + } + }, + loadingPromise: async () => { + await event.data.loadPromise; + }, + style: "bar", + keyframes: keyframes, + }, + }); + } +}); diff --git a/frontend/src/ts/event-handlers/nav.ts b/frontend/src/ts/event-handlers/nav.ts new file mode 100644 index 000000000000..2878adda99e6 --- /dev/null +++ b/frontend/src/ts/event-handlers/nav.ts @@ -0,0 +1,6 @@ +import { signOut } from "../auth"; +import { qs } from "../utils/dom"; + +qs("nav .accountButtonAndMenu .menu button.signOut")?.on("click", () => { + signOut(); +}); diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts index f26d9a3fd3d6..f3a8f9e5f307 100644 --- a/frontend/src/ts/index.ts +++ b/frontend/src/ts/index.ts @@ -5,6 +5,7 @@ import "./event-handlers/settings"; import "./event-handlers/account"; import "./event-handlers/leaderboards"; import "./event-handlers/login"; +import "./event-handlers/nav"; import "./modals/google-sign-up"; diff --git a/frontend/src/ts/modals/dev-options.ts b/frontend/src/ts/modals/dev-options.ts index 73ff64740068..b8726f2e7d0e 100644 --- a/frontend/src/ts/modals/dev-options.ts +++ b/frontend/src/ts/modals/dev-options.ts @@ -65,11 +65,13 @@ async function setup(modalEl: ElementWithUtils): Promise { return; } showLoaderBar(); - void signIn(envConfig.quickLoginEmail, envConfig.quickLoginPassword).then( - () => { - hideLoaderBar(); - }, - ); + void signIn( + envConfig.quickLoginEmail, + envConfig.quickLoginPassword, + true, + ).then(() => { + hideLoaderBar(); + }); void modal.hide(); }); modalEl.qs(".xpBarTest")?.on("click", () => { diff --git a/frontend/src/ts/observables/auth-event.ts b/frontend/src/ts/observables/auth-event.ts index ca5c2ecf604d..1a9523aeeed5 100644 --- a/frontend/src/ts/observables/auth-event.ts +++ b/frontend/src/ts/observables/auth-event.ts @@ -1,5 +1,8 @@ type AuthEvent = - | { type: "authStateChanged"; data: { isUserSignedIn: boolean } } + | { + type: "authStateChanged"; + data: { isUserSignedIn: boolean; loadPromise: Promise }; + } | { type: "snapshotUpdated"; data: { isInitial: boolean } } | { type: "authConfigUpdated"; data?: undefined }; diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts index 657725c87d71..323f29f8999b 100644 --- a/frontend/src/ts/pages/account-settings.ts +++ b/frontend/src/ts/pages/account-settings.ts @@ -14,6 +14,7 @@ import { z } from "zod"; import * as AuthEvent from "../observables/auth-event"; import { qs, qsa, qsr, onDOMReady } from "../utils/dom"; import { showPopup } from "../modals/simple-modals-base"; +import { addGithubAuth, addGoogleAuth } from "../auth"; const pageElement = qsr(".page.pageAccountSettings"); @@ -246,6 +247,14 @@ qs(".pageAccountSettings")?.onChild("click", "#updateAccountName", () => { showPopup("updateName"); }); +qs(".pageAccountSettings")?.onChild("click", "#addGoogleAuth", () => { + void addGoogleAuth(); +}); + +qs(".pageAccountSettings")?.onChild("click", "#addGithubAuth", () => { + void addGithubAuth(); +}); + AuthEvent.subscribe((event) => { if (event.type === "authConfigUpdated") { updateUI(); diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 19a245d4dd6f..6bf43f9a4b7c 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -36,6 +36,7 @@ import Ape from "../ape"; import { AccountChart } from "@monkeytype/schemas/configs"; import { SortedTableWithLimit } from "../utils/sorted-table"; import { qs, qsa, qsr, ElementWithUtils, onDOMReady } from "../utils/dom"; +import { sendVerificationEmail } from "../auth"; let filterDebug = false; //toggle filterdebug @@ -1195,6 +1196,18 @@ qs(".pageAccount button.loadMoreResults")?.on("click", async () => { hideLoaderBar(); }); +qs(".pageAccount")?.onChild("click", ".sendVerificationEmail", async () => { + if (!ConnectionState.get()) { + Notifications.add("You are offline", 0, { + duration: 2, + }); + return; + } + qs(".sendVerificationEmail")?.disable(); + await sendVerificationEmail(); + qs(".sendVerificationEmail")?.enable(); +}); + ConfigEvent.subscribe(({ key }) => { if (getActivePage() === "account" && key === "typingSpeedUnit") { void update(); diff --git a/frontend/src/ts/pages/login.ts b/frontend/src/ts/pages/login.ts index 4f9a25af62f9..e109b264d88d 100644 --- a/frontend/src/ts/pages/login.ts +++ b/frontend/src/ts/pages/login.ts @@ -12,6 +12,9 @@ import { isDevEnvironment } from "../utils/misc"; import { z } from "zod"; import { remoteValidation } from "../utils/remote-validation"; import { qs, qsa, qsr, onDOMReady } from "../utils/dom"; +import { signIn, signInWithGitHub, signInWithGoogle, signUp } from "../auth"; +import * as Notifications from "../elements/notifications"; +import * as ConnectionState from "../states/connection"; let registerForm: { name?: string; @@ -206,6 +209,118 @@ new ValidatedHtmlInputElement(passwordVerifyInputEl, { }, }); +qs(".pageLogin .login button.signInWithGoogle")?.on("click", async () => { + if (!ConnectionState.get()) { + Notifications.add("You are offline", 0); + return; + } + + const rememberMe = + qs(".pageLogin .login #rememberMe input")?.isChecked() ?? + false; + + showPreloader(); + disableInputs(); + disableSignUpButton(); + const data = await signInWithGoogle(rememberMe); + hidePreloader(); + + if (!data.success) { + Notifications.add(data.message, -1); + enableInputs(); + enableSignUpButton(); + } +}); + +qs(".pageLogin .login form")?.on("submit", async (e) => { + e.preventDefault(); + + if (!ConnectionState.get()) { + Notifications.add("You are offline", 0); + return; + } + + const email = + qsa(".pageLogin .login input")?.[0]?.getValue() ?? ""; + const password = + qsa(".pageLogin .login input")?.[1]?.getValue() ?? ""; + const rememberMe = + qs(".pageLogin .login #rememberMe input")?.isChecked() ?? + false; + + if (email === "" || password === "") { + Notifications.add("Please fill in all fields", 0); + return; + } + + showPreloader(); + disableInputs(); + disableSignUpButton(); + const data = await signIn(email, password, rememberMe); + hidePreloader(); + + if (!data.success) { + Notifications.add(data.message, -1); + enableInputs(); + enableSignUpButton(); + } +}); + +qs(".pageLogin .login button.signInWithGitHub")?.on("click", async () => { + if (!ConnectionState.get()) { + Notifications.add("You are offline", 0); + return; + } + + const rememberMe = + qs(".pageLogin .login #rememberMe input")?.isChecked() ?? + false; + + showPreloader(); + disableInputs(); + disableSignUpButton(); + const data = await signInWithGitHub(rememberMe); + hidePreloader(); + + if (!data.success) { + Notifications.add(data.message, -1); + enableInputs(); + enableSignUpButton(); + } +}); + +qs(".pageLogin .register form")?.on("submit", async (e) => { + e.preventDefault(); + + if (!ConnectionState.get()) { + Notifications.add("You are offline", 0); + return; + } + + const signupData = getSignupData(); + if (signupData === false) { + Notifications.add("Please fill in all fields", 0); + return; + } + + showPreloader(); + disableInputs(); + disableSignUpButton(); + + const data = await signUp( + signupData.name, + signupData.email, + signupData.password, + ); + + hidePreloader(); + if (!data.success) { + Notifications.add(data.message, -1); + enableInputs(); + enableSignUpButton(); + } +}); + export const page = new Page({ id: "login", element: qsr(".page.pageLogin"),