2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
import type { SyntheticEvent } from "react";
import { useState } from "react";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
const ChangePasswordSection = () => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { t, isLocaleReady } = useLocale();
// hold display until the locale is loaded
if (!isLocaleReady) {
return null;
}
const errorMessages: { [key: string]: string } = {
[ErrorCode.IncorrectPassword]: t("current_incorrect_password"),
[ErrorCode.NewPasswordMatchesOld]: t("new_password_matches_old_password"),
};
async function changePasswordHandler(e: SyntheticEvent) {
e.preventDefault();
if (isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const response = await fetch("/api/auth/changepw", {
method: "PATCH",
body: JSON.stringify({ oldPassword, newPassword }),
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 200) {
setOldPassword("");
setNewPassword("");
showToast(t("password_has_been_changed"), "success");
return;
}
const body = await response.json();
setErrorMessage(errorMessages[body.error] || `${t("something_went_wrong")}${t("please_try_again")}`);
} catch (err) {
console.error(t("error_changing_password"), err);
setErrorMessage(`${t("something_went_wrong")}${t("please_try_again")}`);
} finally {
setIsSubmitting(false);
}
}
return (
<>
<form className="divide-subtle divide-y lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 lg:pb-5">
<div className="my-3">
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("change_password")}</h2>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
<label htmlFor="current_password" className="text-default block text-sm font-medium">
{t("current_password")}
</label>
<div className="mt-1">
<input
type="password"
value={oldPassword}
onInput={(e) => setOldPassword(e.currentTarget.value)}
name="current_password"
id="current_password"
required
className="border-default block w-full rounded-sm text-sm"
placeholder={t("your_old_password")}
/>
</div>
</div>
<div className="w-full sm:w-1/2">
<label htmlFor="new_password" className="text-default block text-sm font-medium">
{t("new_password")}
</label>
<div className="mt-1">
<input
type="password"
name="new_password"
id="new_password"
value={newPassword}
required
onInput={(e) => setNewPassword(e.currentTarget.value)}
className="border-default block w-full rounded-sm text-sm"
placeholder={t("super_secure_new_password")}
/>
</div>
</div>
</div>
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
<div className="flex py-8 sm:justify-end">
<Button color="secondary" type="submit">
{t("save")}
</Button>
</div>
</div>
</form>
</>
);
};
export default ChangePasswordSection;

View File

@@ -0,0 +1,96 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, PasswordField } from "@calcom/ui";
import TwoFactor from "@components/auth/TwoFactor";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
interface DisableTwoFactorAuthModalProps {
/** Called when the user closes the modal without disabling two-factor auth */
onCancel: () => void;
/** Called when the user disables two-factor auth */
onDisable: () => void;
}
interface DisableTwoFactorValues {
totpCode: string;
password: string;
}
const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuthModalProps) => {
const [isDisabling, setIsDisabling] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { t } = useLocale();
const form = useForm<DisableTwoFactorValues>();
async function handleDisable({ totpCode, password }: DisableTwoFactorValues) {
if (isDisabling) {
return;
}
setIsDisabling(true);
setErrorMessage(null);
try {
const response = await TwoFactorAuthAPI.disable(password, totpCode);
if (response.status === 200) {
onDisable();
return;
}
const body = await response.json();
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage(t("incorrect_password"));
}
if (body.error === ErrorCode.SecondFactorRequired) {
setErrorMessage(t("2fa_required"));
}
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
setErrorMessage(t("incorrect_2fa"));
} else {
setErrorMessage(t("something_went_wrong"));
}
} catch (e) {
setErrorMessage(t("something_went_wrong"));
console.error(t("error_disabling_2fa"), e);
} finally {
setIsDisabling(false);
}
}
return (
<Dialog open={true}>
<DialogContent>
<Form form={form} handleSubmit={handleDisable}>
<TwoFactorModalHeader title={t("disable_2fa")} description={t("disable_2fa_recommendation")} />
<div className="mb-4">
<PasswordField
labelProps={{
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
<TwoFactor center={false} />
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="me-2 ms-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
</div>
</Form>
</DialogContent>
</Dialog>
);
};
export default DisableTwoFactorAuthModal;

View File

@@ -0,0 +1,48 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Badge, Button, showToast } from "@calcom/ui";
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
const utils = trpc.useUtils();
const { t } = useLocale();
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.viewer.me.invalidate();
},
});
return (
<>
<div className="flex flex-col justify-between pl-2 pt-9 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
{t("user_impersonation_heading")}
</h2>
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
{!disableImpersonation ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="text-subtle mt-1 text-sm">{t("user_impersonation_description")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
type="submit"
color="secondary"
onClick={() =>
!disableImpersonation
? mutation.mutate({ disableImpersonation: true })
: mutation.mutate({ disableImpersonation: false })
}>
{!disableImpersonation ? t("disable") : t("enable")}
</Button>
</div>
</div>
</>
);
};
export default DisableUserImpersonation;

View File

@@ -0,0 +1,219 @@
import type { BaseSyntheticEvent } from "react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
import TwoFactor from "@components/auth/TwoFactor";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
interface EnableTwoFactorModalProps {
/**
* Called when the user closes the modal without disabling two-factor auth
*/
onCancel: () => void;
/**
* Called when the user enables two-factor auth
*/
onEnable: () => void;
}
enum SetupStep {
ConfirmPassword,
DisplayQrCode,
EnterTotpCode,
}
const WithStep = ({
step,
current,
children,
}: {
step: SetupStep;
current: SetupStep;
children: JSX.Element;
}) => {
return step === current ? children : null;
};
interface EnableTwoFactorValues {
totpCode: string;
}
const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps) => {
const { t } = useLocale();
const form = useForm<EnableTwoFactorValues>();
const setupDescriptions = {
[SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
[SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
[SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
};
const [step, setStep] = useState(SetupStep.ConfirmPassword);
const [password, setPassword] = useState("");
const [dataUri, setDataUri] = useState("");
const [secret, setSecret] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
async function handleSetup(e: React.FormEvent) {
e.preventDefault();
if (isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const response = await TwoFactorAuthAPI.setup(password);
const body = await response.json();
if (response.status === 200) {
setDataUri(body.dataUri);
setSecret(body.secret);
setStep(SetupStep.DisplayQrCode);
return;
}
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage(t("incorrect_password"));
} else {
setErrorMessage(t("something_went_wrong"));
}
} catch (e) {
setErrorMessage(t("something_went_wrong"));
console.error(t("error_enabling_2fa"), e);
} finally {
setIsSubmitting(false);
}
}
async function handleEnable({ totpCode }: EnableTwoFactorValues, e: BaseSyntheticEvent | undefined) {
e?.preventDefault();
if (isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const response = await TwoFactorAuthAPI.enable(totpCode);
const body = await response.json();
if (response.status === 200) {
onEnable();
return;
}
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
setErrorMessage(`${t("code_is_incorrect")} ${t("please_try_again")}`);
} else {
setErrorMessage(t("something_went_wrong"));
}
} catch (e) {
setErrorMessage(t("something_went_wrong"));
console.error(t("error_enabling_2fa"), e);
} finally {
setIsSubmitting(false);
}
}
const handleEnableRef = useCallbackRef(handleEnable);
const totpCode = form.watch("totpCode");
// auto submit 2FA if all inputs have a value
useEffect(() => {
if (totpCode?.trim().length === 6) {
form.handleSubmit(handleEnableRef.current)();
}
}, [form, handleEnableRef, totpCode]);
return (
<Dialog open={true}>
<DialogContent>
<TwoFactorModalHeader title={t("enable_2fa")} description={setupDescriptions[step]} />
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
<label htmlFor="password" className="text-default mt-4 block text-sm font-medium">
{t("password")}
</label>
<div className="mt-1">
<input
type="password"
name="password"
id="password"
required
value={password}
onInput={(e) => setPassword(e.currentTarget.value)}
className="border-default block w-full rounded-sm text-sm"
/>
</div>
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
</form>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<>
<div className="flex justify-center">
{
// eslint-disable-next-line @next/next/no-img-element
<img src={dataUri} alt="" />
}
</div>
<p className="text-center font-mono text-xs">{secret}</p>
</>
</WithStep>
<Form handleSubmit={handleEnable} form={form}>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<div className="mb-4">
<TwoFactor center />
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
</WithStep>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="me-2 ms-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button type="submit" className="me-2 ms-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="me-2 ms-2" disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
</div>
</Form>
</DialogContent>
</Dialog>
);
};
export default EnableTwoFactorModal;

View File

@@ -0,0 +1,33 @@
const TwoFactorAuthAPI = {
async setup(password: string) {
return fetch("/api/auth/two-factor/totp/setup", {
method: "POST",
body: JSON.stringify({ password }),
headers: {
"Content-Type": "application/json",
},
});
},
async enable(code: string) {
return fetch("/api/auth/two-factor/totp/enable", {
method: "POST",
body: JSON.stringify({ code }),
headers: {
"Content-Type": "application/json",
},
});
},
async disable(password: string, code: string) {
return fetch("/api/auth/two-factor/totp/disable", {
method: "POST",
body: JSON.stringify({ password, code }),
headers: {
"Content-Type": "application/json",
},
});
},
};
export default TwoFactorAuthAPI;

View File

@@ -0,0 +1,59 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Button } from "@calcom/ui";
import DisableTwoFactorModal from "./DisableTwoFactorModal";
import EnableTwoFactorModal from "./EnableTwoFactorModal";
const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean }) => {
const [enabled, setEnabled] = useState(twoFactorEnabled);
const [enableModalOpen, setEnableModalOpen] = useState(false);
const [disableModalOpen, setDisableModalOpen] = useState(false);
const { t } = useLocale();
return (
<>
<div className="flex flex-col justify-between pl-2 pt-9 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
{enabled ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="text-subtle mt-1 text-sm">{t("add_an_extra_layer_of_security")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
type="submit"
color="secondary"
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
{enabled ? t("disable") : t("enable")}
</Button>
</div>
</div>
{enableModalOpen && (
<EnableTwoFactorModal
onEnable={() => {
setEnabled(true);
setEnableModalOpen(false);
}}
onCancel={() => setEnableModalOpen(false)}
/>
)}
{disableModalOpen && (
<DisableTwoFactorModal
onDisable={() => {
setEnabled(false);
setDisableModalOpen(false);
}}
onCancel={() => setDisableModalOpen(false)}
/>
)}
</>
);
};
export default TwoFactorAuthSection;

View File

@@ -0,0 +1,19 @@
import { Icon } from "@calcom/ui";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<Icon name="shield" className="text-inverted h-6 w-6" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
{title}
</h3>
<p className="text-muted text-sm">{description}</p>
</div>
</div>
);
};
export default TwoFactorModalHeader;