Merge branch 'main' into fix/after-meeting-ends-migration
This commit is contained in:
commit
554cc1203e
|
@ -0,0 +1,107 @@
|
|||
import { SyntheticEvent, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
||||
|
||||
import { ErrorCode } from "@lib/auth";
|
||||
|
||||
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
|
||||
|
||||
interface DisableTwoFactorAuthModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: () => void;
|
||||
|
||||
/** Called when the user closes the modal without disabling two-factor auth */
|
||||
onCancel: () => void;
|
||||
/** Called when the user disables two-factor auth */
|
||||
onDisable: () => void;
|
||||
}
|
||||
|
||||
const DisableTwoFactorAuthModal = ({
|
||||
onDisable,
|
||||
onCancel,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DisableTwoFactorAuthModalProps) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [isDisabling, setIsDisabling] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const { t } = useLocale();
|
||||
|
||||
async function handleDisable(e: SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (isDisabling) {
|
||||
return;
|
||||
}
|
||||
setIsDisabling(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const response = await TwoFactorAuthAPI.disable(password);
|
||||
if (response.status === 200) {
|
||||
onDisable();
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
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_disabling_2fa"), e);
|
||||
} finally {
|
||||
setIsDisabling(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
title={t("disable_2fa")}
|
||||
description={t("disable_2fa_recommendation")}
|
||||
type="creation"
|
||||
useOwnActionButtons>
|
||||
<form onSubmit={handleDisable}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
required
|
||||
value={password}
|
||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
onClick={handleDisable}
|
||||
disabled={password.length === 0 || isDisabling}>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisableTwoFactorAuthModal;
|
|
@ -0,0 +1,232 @@
|
|||
import React, { SyntheticEvent, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
||||
|
||||
import { ErrorCode } from "@lib/auth";
|
||||
|
||||
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
|
||||
|
||||
interface EnableTwoFactorModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: () => void;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: EnableTwoFactorModalProps) => {
|
||||
const { t } = useLocale();
|
||||
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 [totpCode, setTotpCode] = 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: SyntheticEvent) {
|
||||
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(e: SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (isSubmitting || totpCode.length !== 6) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
title={t("enable_2fa")}
|
||||
description={setupDescriptions[step]}
|
||||
type="creation"
|
||||
useOwnActionButtons
|
||||
// Icon={Icon.FiAlertTriangle}>
|
||||
>
|
||||
{/* <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="mt-4 block text-sm font-medium text-gray-700">
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
required
|
||||
value={password}
|
||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||
className="block w-full rounded-sm border-gray-300 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>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
<form onSubmit={handleEnable}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="code" className="mt-4 block text-sm font-medium text-gray-700">
|
||||
{t("code")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
id="code"
|
||||
required
|
||||
value={totpCode}
|
||||
maxLength={6}
|
||||
minLength={6}
|
||||
inputMode="numeric"
|
||||
onInput={(e) => setTotpCode(e.currentTarget.value)}
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
</div>
|
||||
</form>
|
||||
</WithStep>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
onClick={handleSetup}
|
||||
disabled={password.length === 0 || isSubmitting}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
onClick={handleEnable}
|
||||
disabled={totpCode.length !== 6 || isSubmitting}>
|
||||
{t("enable")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnableTwoFactorModal;
|
|
@ -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) {
|
||||
return fetch("/api/auth/two-factor/totp/disable", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ password }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default TwoFactorAuthAPI;
|
|
@ -11,6 +11,7 @@ import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/
|
|||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
import { ContractsProvider } from "@calcom/features/ee/web3/contexts/contractsContext";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { MetaProvider } from "@calcom/ui/v2/core/Meta";
|
||||
|
||||
import usePublicPage from "@lib/hooks/usePublicPage";
|
||||
|
||||
|
@ -80,7 +81,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||
storageKey={storageKey}
|
||||
forcedTheme={forcedTheme}
|
||||
attribute="class">
|
||||
{props.children}
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</ThemeProvider>
|
||||
</TooltipProvider>
|
||||
</CustomI18nextProvider>
|
||||
|
|
|
@ -32,6 +32,7 @@ export enum ErrorCode {
|
|||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
InvalidPassword = "invalid-password",
|
||||
}
|
||||
|
||||
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
|||
const V2_WHITELIST = [
|
||||
"/settings/admin",
|
||||
"/settings/my-account",
|
||||
"/settings/security",
|
||||
"/availability",
|
||||
"/bookings",
|
||||
"/event-types",
|
||||
|
|
|
@ -161,6 +161,12 @@ const nextConfig = {
|
|||
destination: "/settings/my-account/profile",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/settings/security",
|
||||
has: [{ type: "cookie", key: "calcom-v2-early-access" }],
|
||||
destination: "/settings/security/password",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/bookings",
|
||||
destination: "/bookings/upcoming",
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
|
||||
function AdminAppsView() {
|
||||
return (
|
||||
<>
|
||||
<Meta title="apps" description="apps_description" />
|
||||
<h1>App listing</h1>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useRef } from "react";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { TextField } from "@calcom/ui/form/fields";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
|
||||
function AdminView() {
|
||||
|
@ -12,6 +13,7 @@ function AdminView() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Meta title="impersonation" description="impersonation_description" />
|
||||
<form
|
||||
className="mb-6 w-full sm:w-1/2"
|
||||
onSubmit={(e) => {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
|
||||
function AdminAppsView() {
|
||||
return (
|
||||
<>
|
||||
<Meta title="admin" description="admin_description" />
|
||||
<h1>Admin index</h1>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
|
||||
function AdminUsersView() {
|
||||
return (
|
||||
<>
|
||||
<Meta title="users" description="users_description" />
|
||||
<h1>Users listing</h1>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { title } from "process";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import Avatar from "@calcom/ui/v2/core/Avatar";
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
import { Button } from "@calcom/ui/v2/core/Button";
|
||||
import Loader from "@calcom/ui/v2/core/Loader";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import Switch from "@calcom/ui/v2/core/Switch";
|
||||
import TimezoneSelect from "@calcom/ui/v2/core/TimezoneSelect";
|
||||
import ColorPicker from "@calcom/ui/v2/core/colorpicker";
|
||||
import Select from "@calcom/ui/v2/core/form/Select";
|
||||
import { TextField, Form, Label } from "@calcom/ui/v2/core/form/fields";
|
||||
import { Form } from "@calcom/ui/v2/core/form/fields";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
|
@ -54,6 +47,7 @@ const AppearanceView = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
theme: values.theme.value,
|
||||
});
|
||||
}}>
|
||||
<Meta title="appearance" description="appearance_description" />
|
||||
<Controller
|
||||
name="theme"
|
||||
control={formMethods.control}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { trpc } from "@calcom/trpc/react";
|
|||
import { Icon } from "@calcom/ui";
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
import { List, ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/modules/List";
|
||||
import DestinationCalendarSelector from "@calcom/ui/v2/modules/event-types/DestinationCalendarSelector";
|
||||
|
@ -29,6 +30,8 @@ const CalendarsView = () => {
|
|||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="calendars" description="calendars_description" />
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => {
|
||||
|
@ -54,7 +57,9 @@ const CalendarsView = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<h4 className="leading-20 mt-12 text-xl font-semibold text-black">{t("check_for_conflicts")}</h4>
|
||||
<h4 className="leading-20 mt-12 text-xl font-semibold text-black">
|
||||
{t("check_for_conflicts")}
|
||||
</h4>
|
||||
<p className="pb-2 text-sm text-gray-600">{t("select_calendars")}</p>
|
||||
<List>
|
||||
{data.connectedCalendars.map((item) => (
|
||||
|
@ -88,7 +93,9 @@ const CalendarsView = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="w-full border-t border-gray-200">
|
||||
<p className="px-2 pt-4 text-sm text-neutral-500">{t("toggle_calendars_conflict")}</p>
|
||||
<p className="px-2 pt-4 text-sm text-neutral-500">
|
||||
{t("toggle_calendars_conflict")}
|
||||
</p>
|
||||
<ul className="space-y-2 px-2 pt-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
|
@ -118,6 +125,7 @@ const CalendarsView = () => {
|
|||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import Dropdown, {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/v2/core/Dropdown";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
||||
|
||||
|
@ -27,6 +28,7 @@ const ConferencingLayout = (props: inferSSRProps<typeof getServerSideProps>) =>
|
|||
|
||||
return (
|
||||
<div className="m-4 rounded-md border-neutral-200 bg-white sm:mx-0 md:border xl:mt-0">
|
||||
<Meta title="conferencing" description="conferencing_description" />
|
||||
{apps.map((app) => (
|
||||
<div
|
||||
key={app.title}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||
import prisma from "@calcom/prisma";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/v2/core/Button";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import TimezoneSelect from "@calcom/ui/v2/core/TimezoneSelect";
|
||||
import Select from "@calcom/ui/v2/core/form/Select";
|
||||
import { Form, Label } from "@calcom/ui/v2/core/form/fields";
|
||||
|
@ -101,6 +102,7 @@ const GeneralView = ({ localeProp, t, user }: GeneralViewProps) => {
|
|||
weekStart: values.weekStart.value,
|
||||
});
|
||||
}}>
|
||||
<Meta title="general" description="general_description" />
|
||||
<Controller
|
||||
name="locale"
|
||||
control={formMethods.control}
|
||||
|
|
|
@ -2,8 +2,8 @@ import crypto from "crypto";
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useState, useRef } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
@ -11,8 +11,9 @@ import { trpc } from "@calcom/trpc/react";
|
|||
import { Icon } from "@calcom/ui";
|
||||
import Avatar from "@calcom/ui/v2/core/Avatar";
|
||||
import { Button } from "@calcom/ui/v2/core/Button";
|
||||
import { Dialog, DialogTrigger, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
||||
import { TextField, Form, Label } from "@calcom/ui/v2/core/form/fields";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@calcom/ui/v2/core/Dialog";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { Form, Label, TextField } from "@calcom/ui/v2/core/form/fields";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
|
@ -71,6 +72,7 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
handleSubmit={(values) => {
|
||||
mutation.mutate(values);
|
||||
}}>
|
||||
<Meta title="profile" description="profile_description" />
|
||||
<div className="flex items-center">
|
||||
{/* TODO upload new avatar */}
|
||||
<Controller
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import { IdentityProvider } from "@prisma/client";
|
||||
import { Trans } from "next-i18next";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/v2/core/Button";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { Form, TextField } from "@calcom/ui/v2/core/form/fields";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
import { identityProviderNameMap } from "@lib/auth";
|
||||
|
||||
const PasswordView = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
|
||||
const mutation = trpc.useMutation("viewer.auth.changePassword", {
|
||||
onSuccess: () => {
|
||||
showToast(t("password_updated_successfully"), "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`${t("error_updating_password")}, ${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const formMethods = useForm();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="password" description="password_description" />
|
||||
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
||||
<div>
|
||||
<div className="mt-6">
|
||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
|
||||
{t("account_managed_by_identity_provider", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
{t("account_managed_by_identity_provider_description", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
const { oldPassword, newPassword } = values;
|
||||
mutation.mutate({ oldPassword, newPassword });
|
||||
}}>
|
||||
<div className="flex space-x-4">
|
||||
<Controller
|
||||
name="oldPassword"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<TextField
|
||||
name="oldPassword"
|
||||
label={t("old_password")}
|
||||
value={value}
|
||||
type="password"
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("oldPassword", e?.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="newPassword"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<TextField
|
||||
name="newPassword"
|
||||
label={t("new_password")}
|
||||
value={value}
|
||||
type="password"
|
||||
placeholder={t("secure_password")}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("newPassword", e?.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<Trans i18nKey="valid_password">
|
||||
Password must be at least at least 7 characters, mix of uppercase & lowercase letters, and
|
||||
contain at least 1 number
|
||||
</Trans>
|
||||
</p>
|
||||
<Button color="primary" className="mt-8" disabled={formMethods.formState.isSubmitting}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordView.getLayout = getLayout;
|
||||
|
||||
export default PasswordView;
|
|
@ -0,0 +1,75 @@
|
|||
import { useState, useContext } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
import Loader from "@calcom/ui/v2/core/Loader";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import Switch from "@calcom/ui/v2/core/Switch";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
|
||||
|
||||
import DisableTwoFactorModal from "@components/v2/settings/DisableTwoFactorModal";
|
||||
import EnableTwoFactorModal from "@components/v2/settings/EnableTwoFactorModal";
|
||||
|
||||
const TwoFactorAuthView = () => {
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { t } = useLocale();
|
||||
const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
|
||||
|
||||
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="2fa" description="2fa_description" />
|
||||
<div className="mt-6 flex items-start space-x-4">
|
||||
<Switch
|
||||
checked={user?.twoFactorEnabled}
|
||||
onCheckedChange={() =>
|
||||
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex">
|
||||
<p>{t("two_factor_auth")}</p>
|
||||
<Badge className="ml-2 text-xs" variant={user?.twoFactorEnabled ? "success" : "gray"}>
|
||||
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
|
||||
</Badge>
|
||||
</div>
|
||||
<p>Add an extra layer of security to your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnableTwoFactorModal
|
||||
open={enableModalOpen}
|
||||
onOpenChange={() => setEnableModalOpen(!enableModalOpen)}
|
||||
onEnable={() => {
|
||||
setEnableModalOpen(false);
|
||||
utils.invalidateQueries("viewer.me");
|
||||
}}
|
||||
onCancel={() => {
|
||||
setEnableModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<DisableTwoFactorModal
|
||||
open={disableModalOpen}
|
||||
onOpenChange={() => setDisableModalOpen(!disableModalOpen)}
|
||||
onDisable={() => {
|
||||
setDisableModalOpen(false);
|
||||
utils.invalidateQueries("viewer.me");
|
||||
}}
|
||||
onCancel={() => {
|
||||
setDisableModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TwoFactorAuthView.getLayout = getLayout;
|
||||
|
||||
export default TwoFactorAuthView;
|
|
@ -1079,5 +1079,9 @@
|
|||
"custom_brand_colors": "Custom brand colors",
|
||||
"customize_your_brand_colors": "Customize your own brand colour into your booking page.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Removes any Cal related brandings, i.e. 'Powered by Cal.'"
|
||||
"removes_cal_branding": "Removes any Cal related brandings, i.e. 'Powered by Cal.'",
|
||||
"old_password": "Old password",
|
||||
"secure_password": "Your new super secure password",
|
||||
"error_updating_password": "Error updating password",
|
||||
"two_factor_auth": "Two factor authentication"
|
||||
}
|
||||
|
|
|
@ -15,6 +15,16 @@ export async function verifyPassword(password: string, hashedPassword: string) {
|
|||
return isValid;
|
||||
}
|
||||
|
||||
export function validPassword(password: string) {
|
||||
if (password.length < 7) return false;
|
||||
|
||||
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password)) return false;
|
||||
|
||||
if (!/\d+/.test(password)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getSession(options: GetSessionParams): Promise<Session | null> {
|
||||
const session = await getSessionInner(options);
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import { TRPCError } from "@trpc/server";
|
|||
|
||||
import { createProtectedRouter, createRouter } from "../createRouter";
|
||||
import { apiKeysRouter } from "./viewer/apiKeys";
|
||||
import { authRouter } from "./viewer/auth";
|
||||
import { availabilityRouter } from "./viewer/availability";
|
||||
import { bookingsRouter } from "./viewer/bookings";
|
||||
import { eventTypesRouter } from "./viewer/eventTypes";
|
||||
|
@ -1287,6 +1288,7 @@ export const viewerRouter = createRouter()
|
|||
.merge("apiKeys.", apiKeysRouter)
|
||||
.merge("slots.", slotsRouter)
|
||||
.merge("workflows.", workflowsRouter)
|
||||
.merge("auth.", authRouter)
|
||||
|
||||
// NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved.
|
||||
// After that there would just one merge call here for all the apps.
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { IdentityProvider } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import { hashPassword, verifyPassword, validPassword } from "@calcom/lib/auth";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createProtectedRouter } from "../../createRouter";
|
||||
|
||||
export const authRouter = createProtectedRouter().mutation("changePassword", {
|
||||
input: z.object({
|
||||
oldPassword: z.string(),
|
||||
newPassword: z.string(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const { oldPassword, newPassword } = input;
|
||||
|
||||
const { user } = ctx;
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" });
|
||||
}
|
||||
|
||||
const currentPasswordQuery = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
select: {
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
|
||||
const currentPassword = currentPasswordQuery?.password;
|
||||
|
||||
if (!currentPassword) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" });
|
||||
}
|
||||
|
||||
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
|
||||
if (!passwordsMatch) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "INCORRECT_PASSWORD" });
|
||||
}
|
||||
|
||||
if (oldPassword === newPassword) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "PASSWORD_MATCHES_OLD" });
|
||||
}
|
||||
|
||||
if (!validPassword(newPassword)) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "INVALID_PASSWORD" });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
import Head from "next/head";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
type MetaType = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const initialMeta = {
|
||||
title: "",
|
||||
description: "",
|
||||
};
|
||||
|
||||
const MetaContext = createContext({
|
||||
meta: initialMeta,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setMeta: (newMeta: Partial<MetaType>) => {},
|
||||
});
|
||||
|
||||
export function useMeta() {
|
||||
return useContext(MetaContext);
|
||||
}
|
||||
|
||||
export function MetaProvider({ children }: { children: React.ReactNode }) {
|
||||
const [value, setValue] = useState(initialMeta);
|
||||
const setMeta = (newMeta: Partial<MetaType>) => {
|
||||
setValue((v) => ({ ...v, ...newMeta }));
|
||||
};
|
||||
|
||||
return <MetaContext.Provider value={{ meta: value, setMeta }}>{children}</MetaContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The purpose of this component is to simplify title and description handling.
|
||||
* Similarly to `next/head`'s `Head` component this allow us to update the metadata for a page
|
||||
* from any children, also exposes the metadata via the `useMeta` hook in case we need them
|
||||
* elsewhere (ie. on a Heading, Title, Subtitle, etc.)
|
||||
* @example <Meta title="Password" description="Manage settings for your account passwords" />
|
||||
*/
|
||||
export default function Meta({ title, description }: MetaType) {
|
||||
const { t } = useLocale();
|
||||
const { setMeta, meta } = useMeta();
|
||||
/* @TODO: maybe find a way to have this data on first render to prevent flicker */
|
||||
if (meta.title !== title || meta.description !== description) {
|
||||
setMeta({ title, description });
|
||||
}
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{t(title)} | Cal.com</title>
|
||||
<meta name="description" content={t(description)} />
|
||||
</Head>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
import React, { ComponentProps } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Icon } from "../../../Icon";
|
||||
import { useMeta } from "../Meta";
|
||||
import Shell from "../Shell";
|
||||
import { VerticalTabItem } from "../navigation/tabs";
|
||||
import VerticalTabs from "../navigation/tabs/VerticalTabs";
|
||||
|
@ -26,8 +29,8 @@ const tabs = [
|
|||
icon: Icon.FiKey,
|
||||
children: [
|
||||
//
|
||||
{ name: "password", href: "/settings/security" },
|
||||
{ name: "2fa_auth", href: "/settings/security" },
|
||||
{ name: "password", href: "/settings/security/password" },
|
||||
{ name: "2fa_auth", href: "/settings/security/two-factor-auth" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -88,9 +91,35 @@ export default function SettingsLayout({
|
|||
/>
|
||||
</VerticalTabs>
|
||||
}>
|
||||
<div className="flex-1 [&>*]:flex-1">{children}</div>
|
||||
<div className="flex-1 [&>*]:flex-1">
|
||||
<ShellHeader />
|
||||
{children}
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export const getLayout = (page: React.ReactElement) => <SettingsLayout>{page}</SettingsLayout>;
|
||||
|
||||
function ShellHeader() {
|
||||
const { meta } = useMeta();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
return (
|
||||
<header className="block justify-between px-4 pt-8 sm:flex sm:px-6 md:px-8">
|
||||
<div className="mb-8 w-full">
|
||||
{meta.title && isLocaleReady ? (
|
||||
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
|
||||
{t(meta.title)}
|
||||
</h1>
|
||||
) : (
|
||||
<div className="mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200" />
|
||||
)}
|
||||
{meta.description && isLocaleReady ? (
|
||||
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t(meta.description)}</p>
|
||||
) : (
|
||||
<div className="mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200" />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user