Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show dialog when changing email after using Google Login #9611

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9ad908b
improvement: changing email after using Google Sign in
JaideepGuntupalli Jun 18, 2023
d64cd8b
cleanup: improved serverConfig.ts to make it work with MAILHOG locally
JaideepGuntupalli Jun 18, 2023
5e81e06
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jun 18, 2023
891ce0e
Merge branch 'main' into improvement/changing-email-after-using-Googl…
PeerRich Jun 18, 2023
18200cd
Merge branch 'main' into improvement/changing-email-after-using-Googl…
PeerRich Jun 18, 2023
eed01bb
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jun 19, 2023
18f14c5
update: logging out user after the change, improved comments and desc…
JaideepGuntupalli Jun 19, 2023
9dc147f
update: improved logout messaging
JaideepGuntupalli Jun 19, 2023
5b28edc
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jun 20, 2023
b7d9baa
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jun 21, 2023
6b35724
feat: made it generalised, not only for google
JaideepGuntupalli Jun 21, 2023
62f6f76
chore: rebuild a bit of the password reset logic for re-use
emrysal Jun 21, 2023
ecc4c42
Removed || loading
emrysal Jun 21, 2023
221c702
Fix type error
emrysal Jun 21, 2023
5833d3e
Add await, remove error
emrysal Jun 21, 2023
881598e
setTimeout must respond with the awaited value, prevents no-response …
emrysal Jun 21, 2023
cc6dd39
Also signOut for when the idp doesn't change
emrysal Jun 21, 2023
97e8563
Merge branch 'main' into improvement/changing-email-after-using-Googl…
alannnc Jun 22, 2023
3330edb
Merge branch 'main' into improvement/changing-email-after-using-Googl…
alannnc Jun 22, 2023
8864163
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jun 23, 2023
3a02263
Merge branch 'main' into improvement/changing-email-after-using-Googl…
PeerRich Jun 23, 2023
6f27c82
both users who change either auth or email are shown same logout scre…
JaideepGuntupalli Jun 23, 2023
7dae85a
Merge pull request #1 from calcom/tweak/serverside-password-reset-req…
JaideepGuntupalli Jun 24, 2023
6333f8b
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jun 24, 2023
27f5927
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jun 26, 2023
6170e1c
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jul 3, 2023
036f33c
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jul 3, 2023
77e0bc2
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jul 6, 2023
ef03fa6
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jul 7, 2023
816b3c7
Merge branch 'main' into improvement/changing-email-after-using-Googl…
JaideepGuntupalli Jul 11, 2023
ff457b5
fix: Can reset forgotten password test passed
JaideepGuntupalli Jul 11, 2023
6c6c4c4
fix: import and type fixed
JaideepGuntupalli Jul 11, 2023
1677fe6
Merge main into branch, resolved user locale for sending password res…
JaideepGuntupalli Jul 24, 2023
d380db8
Fixed lint issues
JaideepGuntupalli Jul 24, 2023
4575627
Merge branch 'main' into improvement/changing-email-after-using-Googl…
alannnc Jul 26, 2023
8d99914
Merge branch 'main' into pr/9611
zomars Aug 3, 2023
3976d88
Update forgot-password.ts
zomars Aug 3, 2023
5b1942c
fix return type mismatch
zomars Aug 3, 2023
6aefc6c
fix: no need for useState here
zomars Aug 3, 2023
27920f6
revert: and refactor profile update handler
zomars Aug 3, 2023
9f2ce61
fix: don't double query the user
zomars Aug 3, 2023
1bbd9da
refactor: dead code
zomars Aug 3, 2023
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
69 changes: 9 additions & 60 deletions apps/web/pages/api/auth/forgot-password.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import type { ResetPasswordRequest } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";

import dayjs from "@calcom/dayjs";
import { sendPasswordResetEmail } from "@calcom/emails";
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";

async function handler(req: NextApiRequest, res: NextApiResponse) {
Expand Down Expand Up @@ -37,63 +33,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});

try {
const maybeUser = await prisma.user.findUnique({
where: {
email: email.data,
},
select: {
name: true,
identityProvider: true,
email: true,
locale: true,
},
const user = await prisma.user.findUnique({
where: { email: email.data },
select: { name: true, email: true, locale: true },
});

if (!maybeUser) {
// Don't leak information about whether an email is registered or not
return res
.status(200)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
}

const t = await getTranslation(maybeUser.locale ?? "en", "common");

const maybePreviousRequest = await prisma.resetPasswordRequest.findMany({
where: {
email: maybeUser.email,
expires: {
gt: new Date(),
},
},
});

let passwordRequest: ResetPasswordRequest;

if (maybePreviousRequest && maybePreviousRequest?.length >= 1) {
passwordRequest = maybePreviousRequest[0];
} else {
const expiry = dayjs().add(PASSWORD_RESET_EXPIRY_HOURS, "hours").toDate();
const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
data: {
email: maybeUser.email,
expires: expiry,
},
});
passwordRequest = createdResetPasswordRequest;
}

const resetLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/forgot-password/${passwordRequest.id}`;
await sendPasswordResetEmail({
language: t,
user: maybeUser,
resetLink,
});

return res
.status(201)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
// Don't leak info about whether the user exists
if (!user) return res.status(201).json({ message: "password_reset_email_sent" });
await passwordResetRequest(user);
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to send a localized error message here. The client should handle it. Just double checked and we don't actually use this message in the frontend anyways.

return res.status(201).json({ message: "password_reset_email_sent" });
} catch (reason) {
// console.error(reason);
console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
}
}
Expand Down
4 changes: 0 additions & 4 deletions apps/web/pages/auth/forgot-password/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { CSSProperties, SyntheticEvent } from "react";
import React from "react";

Expand All @@ -20,7 +19,6 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const [error, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [email, setEmail] = React.useState("");
const router = useRouter();

const handleChange = (e: SyntheticEvent) => {
const target = e.target as typeof e.target & { value: string };
Expand All @@ -40,8 +38,6 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const json = await res.json();
if (!res.ok) {
setError(json);
} else if ("resetLink" in json) {
Copy link
Member

Choose a reason for hiding this comment

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

This was needed for forgot-password E2E test but we don't rely on this anymore

router.push(json.resetLink);
} else {
setSuccess(true);
}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/pages/auth/logout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export function Logout(props: Props) {
}, [props.query?.survey]);
const { t } = useLocale();

const message = () => {
if (props.query?.passReset === "true") return "reset_your_password";
if (props.query?.emailChange === "true") return "email_change";
return "hope_to_see_you_soon";
};
Comment on lines +32 to +36
Copy link
Member

Choose a reason for hiding this comment

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

More legible than a ternary if you ask me. No need for constants either.


return (
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
<div className="mb-4">
Expand All @@ -40,7 +46,7 @@ export function Logout(props: Props) {
{t("youve_been_logged_out")}
</h3>
<div className="mt-2">
<p className="text-subtle text-sm">{t("hope_to_see_you_soon")}</p>
<p className="text-subtle text-sm">{t(message())}</p>
</div>
</div>
</div>
Expand Down
80 changes: 60 additions & 20 deletions apps/web/pages/settings/my-account/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { signOut } from "next-auth/react";
import { useSession } from "next-auth/react";
import { signOut, useSession } from "next-auth/react";
import type { BaseSyntheticEvent } from "react";
import { useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";

import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import turndown from "@calcom/lib/turndownService";
Expand All @@ -26,6 +24,7 @@
DialogContent,
DialogFooter,
DialogTrigger,
Editor,
Form,
ImageUploader,
Label,
Expand All @@ -37,7 +36,6 @@
SkeletonContainer,
SkeletonText,
TextField,
Editor,
} from "@calcom/ui";
import { AlertTriangle, Trash2 } from "@calcom/ui/components/icon";

Expand Down Expand Up @@ -79,16 +77,27 @@
const ProfileView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data: session, update } = useSession();

Check warning on line 80 in apps/web/pages/settings/my-account/profile.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/pages/settings/my-account/profile.tsx#L80

[@typescript-eslint/no-unused-vars] 'session' is assigned a value but never used. Allowed unused vars must match /^_/u.

const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: avatar, isLoading: isLoadingAvatar } = trpc.viewer.avatar.useQuery();
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: (values) => {
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (res) => {
showToast(t("settings_updated_successfully"), "success");
if (res.signOutUser && tempFormValues) {
if (res.passwordReset) {
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
// sign out the user to avoid unauthorized access error
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
} else {
// sign out the user to avoid unauthorized access error
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
}
}
utils.viewer.me.invalidate();
utils.viewer.avatar.invalidate();
update(values);
setConfirmAuthEmailChangeWarningDialogOpen(false);
update(res);
setTempFormValues(null);
},
onError: () => {
Expand All @@ -99,6 +108,8 @@
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
const [tempFormValues, setTempFormValues] = useState<FormValues | null>(null);
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
const [confirmAuthEmailChangeWarningDialogOpen, setConfirmAuthEmailChangeWarningDialogOpen] =
useState(false);

const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
Expand All @@ -119,9 +130,7 @@

const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
onSuccess() {
if (tempFormValues) {
mutation.mutate(tempFormValues);
}
if (tempFormValues) updateProfileMutation.mutate(tempFormValues);
setConfirmPasswordOpen(false);
},
onError() {
Expand All @@ -148,7 +157,7 @@
},
});

const isCALIdentityProviver = user?.identityProvider === IdentityProvider.CAL;
const isCALIdentityProvider = user?.identityProvider === IdentityProvider.CAL;

const onConfirmPassword = (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => {
e.preventDefault();
Expand All @@ -157,9 +166,15 @@
confirmPasswordMutation.mutate({ passwordInput: password });
};

const onConfirmAuthEmailChange = (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => {
e.preventDefault();

if (tempFormValues) updateProfileMutation.mutate(tempFormValues);
};

const onConfirmButton = (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => {
e.preventDefault();
if (isCALIdentityProviver) {
if (isCALIdentityProvider) {
const totpCode = form.getValues("totpCode");
const password = passwordRef.current.value;
deleteMeMutation.mutate({ password, totpCode });
Expand All @@ -170,7 +185,7 @@

const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
e?.preventDefault();
if (isCALIdentityProviver) {
if (isCALIdentityProvider) {
const password = passwordRef.current.value;
deleteMeMutation.mutate({ password, totpCode });
} else {
Expand Down Expand Up @@ -209,13 +224,17 @@
<ProfileForm
key={JSON.stringify(defaultValues)}
defaultValues={defaultValues}
isLoading={mutation.isLoading}
isLoading={updateProfileMutation.isLoading}
onSubmit={(values) => {
if (values.email !== user.email && isCALIdentityProviver) {
if (values.email !== user.email && isCALIdentityProvider) {
setTempFormValues(values);
setConfirmPasswordOpen(true);
} else if (values.email !== user.email && !isCALIdentityProvider) {
setTempFormValues(values);
// Opens a dialog warning the change
setConfirmAuthEmailChangeWarningDialogOpen(true);
} else {
mutation.mutate(values);
updateProfileMutation.mutate(values);
}
}}
extraField={
Expand Down Expand Up @@ -253,7 +272,7 @@
<p className="text-default mb-4">
{t("delete_account_confirmation_message", { appName: APP_NAME })}
</p>
{isCALIdentityProviver && (
{isCALIdentityProvider && (
<PasswordField
data-testid="password"
name="password"
Expand All @@ -265,7 +284,7 @@
/>
)}

{user?.twoFactorEnabled && isCALIdentityProviver && (
{user?.twoFactorEnabled && isCALIdentityProvider && (
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
<TwoFactor center={false} />
</Form>
Expand Down Expand Up @@ -314,6 +333,27 @@
</DialogFooter>
</DialogContent>
</Dialog>

{/* If changing email from !CAL Login */}
<Dialog
open={confirmAuthEmailChangeWarningDialogOpen}
onOpenChange={setConfirmAuthEmailChangeWarningDialogOpen}>
<DialogContent
title={t("confirm_auth_change")}
description={t("confirm_auth_email_change")}
type="creation"
Icon={AlertTriangle}>
<DialogFooter>
<Button
color="primary"
disabled={updateProfileMutation.isLoading}
onClick={(e) => onConfirmAuthEmailChange(e)}>
{t("confirm")}
</Button>
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
Expand Down
5 changes: 5 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"event_awaiting_approval_recurring": "A recurring event is waiting for your approval",
"someone_requested_an_event": "Someone has requested to schedule an event on your calendar.",
"someone_requested_password_reset": "Someone has requested a link to change your password.",
"password_reset_email_sent": "If this email exists in our system, you should receive a reset email.",
Copy link
Member

Choose a reason for hiding this comment

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

Restored the original message

"password_reset_instructions": "If you didn't request this, you can safely ignore this email and your password will not be changed.",
"event_awaiting_approval_subject": "Awaiting Approval: {{title}} at {{date}}",
"event_still_awaiting_approval": "An event is still waiting for your approval",
Expand Down Expand Up @@ -223,6 +224,10 @@
"already_have_an_account": "Already have an account?",
"create_account": "Create Account",
"confirm_password": "Confirm password",
"confirm_auth_change": "This will change the way you log in",
"confirm_auth_email_change": "Changing the email address will disconnect your current authentication method to log in to Cal.com. We will ask you to verify your new email address. Moving forward, you will be logged out and use your new email address to log in instead of your current authentication method after setting your password by following the instructions that will be sent to your mail.",
"reset_your_password": "Set your new password with the instructions sent to your email address.",
"email_change": "Log back in with your new email address and password.",
"create_your_account": "Create your account",
"sign_up": "Sign up",
"youve_been_logged_out": "You've been logged out",
Expand Down
4 changes: 1 addition & 3 deletions packages/emails/templates/forgot-password-email.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TFunction } from "next-i18next";
import type { TFunction } from "next-i18next";

import { APP_NAME } from "@calcom/lib/constants";

Expand All @@ -14,8 +14,6 @@ export type PasswordReset = {
resetLink: string;
};

export const PASSWORD_RESET_EXPIRY_HOURS = 6;

export default class ForgotPasswordEmail extends BaseEmail {
passwordEvent: PasswordReset;

Expand Down
Loading
Loading