Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
6 changes: 2 additions & 4 deletions frontend/src/ts/components/layout/overlays/Banners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { debounce } from "throttle-debounce";

import { createEffectOn } from "../../../hooks/effects";
import { useRefWithUtils } from "../../../hooks/useRefWithUtils";
import { showPopup } from "../../../modals/simple-modals-base";
import {
Banner as BannerType,
addBanner,
Expand All @@ -14,6 +13,7 @@ import { setGlobalOffsetTop } from "../../../states/core";
import { getSnapshot } from "../../../states/snapshot";
import { cn } from "../../../utils/cn";
import { Fa } from "../../common/Fa";
import { showUpdateNameModal } from "../../modals/account-settings/UpdateNameModal";

function Banner(props: BannerType): JSXElement {
const remove = (): void => {
Expand Down Expand Up @@ -89,9 +89,7 @@ export function Banners(): JSXElement {
<button
type="button"
class="px-2 py-1"
onClick={() => {
showPopup("updateName");
}}
onClick={() => showUpdateNameModal()}
>
Click here
</button>{" "}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/ts/components/modals/Modals.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { JSXElement } from "solid-js";

import { ViewApeKeyModal } from "./account-settings/ViewApeKeyModal";
import { ContactModal } from "./ContactModal";
import { CookiesModal } from "./CookiesModal";
import { CustomTestDurationModal } from "./CustomTestDurationModal";
Expand Down Expand Up @@ -36,6 +37,7 @@ export function Modals(): JSXElement {
<CookiesModal />
<AddPresetModal />
<EditPresetModal />
<ViewApeKeyModal />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { unlink } from "firebase/auth";
import { z } from "zod";

import { isAuthenticated } from "../../../states/core";
import { showNoticeNotification } from "../../../states/notifications";
import { showSimpleModal } from "../../../states/simple-modal";
import { createErrorMessage } from "../../../utils/error";
import {
AuthMethod,
getPasswordSchema,
isUsingGithubAuthentication,
isUsingGoogleAuthentication,
isUsingPasswordAuthentication,
reauthenticate,
} from "../../../utils/firebase-auth";
import { reloadAfter } from "../../../utils/misc";

export function showRemoveAuthMethodModal(options: {
authMethod: AuthMethod;
callback: () => void;
}): void {
if (!isAuthenticated()) return;

//check there is at least one authentication remaining
const hasRemainingAuth = [
isUsingPasswordAuthentication() && options.authMethod !== "password",
isUsingGithubAuthentication() && options.authMethod !== "github.com",
isUsingGoogleAuthentication() && options.authMethod !== "google.com",
].find((it) => it);
Comment on lines +30 to +34
Copy link
Copy Markdown
Contributor

@Leonabcd123 Leonabcd123 Jun 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggestion should be addressed after the previous review comment.

Suggested change
const hasRemainingAuth = [
isUsingPasswordAuthentication() && options.authMethod !== "password",
isUsingGithubAuthentication() && options.authMethod !== "github.com",
isUsingGoogleAuthentication() && options.authMethod !== "google.com",
].find((it) => it);
const hasRemainingAuth = authMethods.some((authMethod) => isUsingAuthentication(authMethod) && options.authMethod !== authMethod);

Also need to export isUsingAuthentication and authMethods, and remove isUsingGithubAuthentication and isUsingGoogleAuthentication.


if (!hasRemainingAuth) {
showNoticeNotification("No remaining authentication enabled");
return;
}

const methodDisplay =
Comment thread
fehmer marked this conversation as resolved.
Outdated
options.authMethod === "password"
? "Password"
: options.authMethod === "github.com"
? "GitHub"
: "Google";

showSimpleModal({
title: `Remove ${methodDisplay} authentication`,
buttonText: "remove",
buttonAlwaysEnabled: options.authMethod !== "password",
schema: z.object({
password: getPasswordSchema(),
checked: z.literal(true),
}),
inputs: {
password: {
placeholder: "Password",
type: "password",
hidden:
!isUsingPasswordAuthentication() || options.authMethod === "password",
},
checked: {
type: "checkbox",
label: `I understand I will lose access to my Monkeytype account if my Google/GitHub account is lost or disabled.`,
hidden: options.authMethod !== "password",
},
},

execFn: async ({ password }) => {
const reauth = await reauthenticate({
password,
excludeMethod: options.authMethod,
});
if (reauth.status !== "success") {
return {
status: reauth.status,
message: reauth.message,
};
}

try {
await unlink(reauth.user, options.authMethod);
} catch (e) {
const message = createErrorMessage(
e,
options.authMethod === "password"
? "Failed to remove password authentication"
: `Failed to unlink ${methodDisplay} account`,
);
return {
status: "error",
message,
};
}

options.callback();

reloadAfter(3);
return {
status: "success",
message: `${methodDisplay} authentication removed`,
};
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { UserEmailSchema } from "@monkeytype/schemas/users";
import { z } from "zod";

import Ape from "../../../ape";
import { signOut } from "../../../auth";
import { isAuthenticated } from "../../../states/core";
import { showNoticeNotification } from "../../../states/notifications";
import { showSimpleModal } from "../../../states/simple-modal";
import {
getPasswordSchema,
isUsingPasswordAuthentication,
reauthenticate,
} from "../../../utils/firebase-auth";

export function showUpdateEmailModal(): void {
if (!isAuthenticated()) return;
if (!isUsingPasswordAuthentication()) {
showNoticeNotification("Password authentication is not enabled");
return;
}

showSimpleModal({
title: "Update email",
buttonText: "update",
schema: z.object({
password: getPasswordSchema(),
email: UserEmailSchema,
emailConfirm: UserEmailSchema,
}),
inputs: {
password: {
placeholder: "Password",
type: "password",
initVal: "",
},
email: {
type: "text",
placeholder: "New email",
initVal: "",
},
emailConfirm: {
type: "text",
placeholder: "Confirm new email",
initVal: "",
},
},

execFn: async ({ password, email, emailConfirm }) => {
if (email !== emailConfirm) {
return {
status: "notice",
message: "Emails don't match",
};
}

const reauth = await reauthenticate({ password });
if (reauth.status !== "success") {
return {
status: reauth.status,
message: reauth.message,
};
}

const response = await Ape.users.updateEmail({
body: {
newEmail: email,
previousEmail: reauth.user.email as string,
},
});

if (response.status !== 200) {
return {
status: "error",
message: "Failed to update email",
notificationOptions: { response },
};
}

signOut();

return {
status: "success",
message: "Email updated",
};
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { UserNameSchema } from "@monkeytype/schemas/users";
import { z } from "zod";

import Ape from "../../../ape";
import * as DB from "../../../db";
import { isAuthenticated } from "../../../states/core";
import { showSimpleModal } from "../../../states/simple-modal";
import {
getPasswordSchema,
isUsingPasswordAuthentication,
reauthenticate,
} from "../../../utils/firebase-auth";
import { reloadAfter } from "../../../utils/misc";
import { remoteValidation } from "../../../utils/remote-validation";

export function showUpdateNameModal(): void {
const snapshot = DB.getSnapshot();
if (!isAuthenticated() || !snapshot) return;

showSimpleModal({
title: "Update name",
buttonText: isUsingPasswordAuthentication()
? "reauthenticate to update"
: "update",
text: DB.getSnapshot()?.needsToChangeName
? "You need to change your account name. This might be because you have a duplicate name, no account name or your name is not allowed (contains whitespace or invalid characters). Sorry for the inconvenience."
: undefined,
schema: z.object({
password: getPasswordSchema(),
newName: UserNameSchema,
}),
inputs: {
password: {
placeholder: "password",
type: "password",
initVal: "",
hidden: !isUsingPasswordAuthentication(),
},
newName: {
placeholder: "new name",
type: "text",
initVal: "",
validation: {
isValid: remoteValidation(
async (name: string) =>
Ape.users.getNameAvailability({ params: { name } }),
{ check: (data) => data.available || "Name not available" },
),
debounceDelay: 1000,
},
},
},

execFn: async ({ password, newName }) => {
const reauth = await reauthenticate({ password });
if (reauth.status !== "success") {
return {
status: reauth.status,
message: reauth.message,
};
}

const response = await Ape.users.updateName({
body: { name: newName },
});
if (response.status !== 200) {
return {
status: "error",
message: "Failed to update name",
notificationOptions: { response },
};
}

snapshot.name = newName;
DB.setSnapshot(snapshot);
if (snapshot.needsToChangeName) {
reloadAfter(2);
}

return {
status: "success",
message: "Name updated",
};
},
});
}
Loading
Loading