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,191 @@
import { zodResolver } from "@hookform/resolvers/zod";
import classNames from "classnames";
import { signIn } from "next-auth/react";
import React from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
const { t } = useLocale();
if (props.userCount > 0)
return (
<form
id="wizard-step-1"
name="wizard-step-1"
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
props.onSuccess();
}}>
<EmptyScreen
Icon="user-check"
headline={t("admin_user_created")}
description={t("admin_user_created_description")}
/>
</form>
);
return <AdminUser {...props} />;
};
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
const { t } = useLocale();
const formSchema = z.object({
username: z
.string()
.refine((val) => val.trim().length >= 1, { message: t("at_least_characters", { count: 1 }) }),
email_address: z.string().email({ message: t("enter_valid_email") }),
full_name: z.string().min(3, t("at_least_characters", { count: 3 })),
password: z.string().superRefine((data, ctx) => {
const isStrict = true;
const result = isPasswordValid(data, true, isStrict);
Object.keys(result).map((key: string) => {
if (!result[key as keyof typeof result]) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [key],
message: key,
});
}
});
}),
});
type formSchemaType = z.infer<typeof formSchema>;
const formMethods = useForm<formSchemaType>({
mode: "onChange",
resolver: zodResolver(formSchema),
});
const onError = () => {
props.onError();
};
const onSubmit = formMethods.handleSubmit(async (data) => {
props.onSubmit();
const response = await fetch("/api/auth/setup", {
method: "POST",
body: JSON.stringify({
username: data.username.trim(),
full_name: data.full_name,
email_address: data.email_address.toLowerCase(),
password: data.password,
}),
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 200) {
await signIn("credentials", {
redirect: false,
callbackUrl: "/",
email: data.email_address.toLowerCase(),
password: data.password,
});
props.onSuccess();
} else {
props.onError();
}
}, onError);
const longWebsiteUrl = WEBSITE_URL.length > 30;
return (
<FormProvider {...formMethods}>
<form id="wizard-step-1" name="wizard-step-1" className="space-y-4" onSubmit={onSubmit}>
<div>
<Controller
name="username"
control={formMethods.control}
render={({ field: { onBlur, onChange, value } }) => (
<>
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
<span className="block">{t("username")}</span>
{longWebsiteUrl && (
<small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 px-3 py-1">
{process.env.NEXT_PUBLIC_WEBSITE_URL}
</small>
)}
</Label>
<TextField
addOnLeading={
!longWebsiteUrl && (
<span className="text-subtle inline-flex items-center rounded-none px-3 text-sm">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span>
)
}
id="username"
labelSrOnly={true}
value={value || ""}
className={classNames("my-0", longWebsiteUrl && "rounded-t-none")}
onBlur={onBlur}
name="username"
onChange={(e) => onChange(e.target.value)}
/>
</>
)}
/>
</div>
<div>
<Controller
name="full_name"
control={formMethods.control}
render={({ field: { onBlur, onChange, value } }) => (
<TextField
value={value || ""}
onBlur={onBlur}
onChange={(e) => onChange(e.target.value)}
color={formMethods.formState.errors.full_name ? "warn" : ""}
type="text"
name="full_name"
autoCapitalize="none"
autoComplete="name"
autoCorrect="off"
className="my-0"
/>
)}
/>
</div>
<div>
<Controller
name="email_address"
control={formMethods.control}
render={({ field: { onBlur, onChange, value } }) => (
<EmailField
value={value || ""}
onBlur={onBlur}
onChange={(e) => onChange(e.target.value)}
className="my-0"
name="email_address"
/>
)}
/>
</div>
<div>
<Controller
name="password"
control={formMethods.control}
render={({ field: { onBlur, onChange, value } }) => (
<PasswordField
value={value || ""}
onBlur={onBlur}
onChange={(e) => onChange(e.target.value)}
hintErrors={["caplow", "admin_min", "num"]}
name="password"
className="my-0"
autoComplete="off"
/>
)}
/>
</div>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,71 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import classNames from "classnames";
import Link from "next/link";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
const ChooseLicense = (
props: {
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit" | "onChange">
) => {
const { value: initialValue = "FREE", onChange, onSubmit, ...rest } = props;
const [value, setValue] = useState(initialValue);
const { t } = useLocale();
return (
<form
{...rest}
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
onSubmit(value);
}}>
<RadioGroup.Root
defaultValue={initialValue}
value={value}
aria-label={t("choose_a_license")}
className="grid grid-rows-2 gap-4 md:grid-cols-2 md:grid-rows-1"
onValueChange={(value) => {
onChange(value);
setValue(value);
}}>
<RadioGroup.Item value="FREE">
<div
className={classNames(
"bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
value === "FREE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-emphasis text-xl">{t("agplv3_license")}</h2>
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
<p className="text-subtle">{t("forever_open_and_free")}</p>
<ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("required_to_keep_your_code_open_source")}</li>
<li>{t("cannot_repackage_and_resell")}</li>
<li>{t("no_enterprise_features")}</li>
</ul>
</div>
</RadioGroup.Item>
<RadioGroup.Item value="EE" disabled>
<Link href="https://cal.com/sales" target="_blank">
<div className={classNames("bg-default h-full cursor-pointer space-y-2 rounded-md border p-4")}>
<h2 className="font-cal text-emphasis text-xl">{t("custom_plan")}</h2>
<p className="font-medium text-green-800">{t("contact_sales")}</p>
<p className="text-subtle">Build on top of Cal.com</p>
<ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("no_need_to_keep_your_code_open_source")}</li>
<li>{t("repackage_rebrand_resell")}</li>
<li>{t("a_vast_suite_of_enterprise_features")}</li>
</ul>
</div>
</Link>
</RadioGroup.Item>
</RadioGroup.Root>
</form>
);
};
export default ChooseLicense;

View File

@@ -0,0 +1,146 @@
import { zodResolver } from "@hookform/resolvers/zod";
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import { useCallback, useState } from "react";
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
import { z } from "zod";
import { classNames } from "@calcom/lib";
import { CONSOLE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Button, TextField } from "@calcom/ui";
import { Icon } from "@calcom/ui";
type EnterpriseLicenseFormValues = {
licenseKey: string;
};
const makeSchemaLicenseKey = (args: { callback: (valid: boolean) => void; onSuccessValidate: () => void }) =>
z.object({
licenseKey: z
.string()
.uuid({
message: "License key must follow UUID format: 8-4-4-4-12",
})
.superRefine(async (data, ctx) => {
const parse = z.string().uuid().safeParse(data);
if (parse.success) {
args.callback(true);
const response = await fetch(`${CONSOLE_URL}/api/license?key=${data}`);
args.callback(false);
const json = await response.json();
if (!json.valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `License key ${json.message.toLowerCase()}`,
});
} else {
args.onSuccessValidate();
}
}
}),
});
const EnterpriseLicense = (
props: {
licenseKey?: string;
initialValue?: Partial<EnterpriseLicenseFormValues>;
onSuccessValidate: () => void;
onSubmit: (value: EnterpriseLicenseFormValues) => void;
onSuccess?: (
data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
) => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">
) => {
const { onSubmit, onSuccess = noop, onSuccessValidate = noop, ...rest } = props;
const { t } = useLocale();
const [checkLicenseLoading, setCheckLicenseLoading] = useState(false);
const mutation = trpc.viewer.deploymentSetup.update.useMutation({
onSuccess,
});
const schemaLicenseKey = useCallback(
() =>
makeSchemaLicenseKey({
callback: setCheckLicenseLoading,
onSuccessValidate,
}),
[setCheckLicenseLoading, onSuccessValidate]
);
const formMethods = useForm<EnterpriseLicenseFormValues>({
defaultValues: {
licenseKey: props.licenseKey || "",
},
resolver: zodResolver(schemaLicenseKey()),
});
const handleSubmit = formMethods.handleSubmit((values) => {
onSubmit(values);
setCheckLicenseLoading(false);
mutation.mutate(values);
});
const { isDirty, errors } = useFormState(formMethods);
return (
<FormProvider {...formMethods}>
<form {...rest} className="bg-default space-y-4 rounded-md px-8 py-10" onSubmit={handleSubmit}>
<div>
<Button
className="w-full justify-center text-lg"
EndIcon="external-link"
href="https://console.cal.com"
target="_blank">
{t("purchase_license")}
</Button>
<div className="relative flex justify-center">
<hr className="border-subtle my-8 w-full border-[1.5px]" />
<span className="bg-default absolute mt-[22px] px-3.5 text-sm">OR</span>
</div>
{t("already_have_key")}
<Controller
name="licenseKey"
control={formMethods.control}
render={({ field: { onBlur, onChange, value } }) => (
<TextField
{...formMethods.register("licenseKey")}
className={classNames(
"group-hover:border-emphasis mb-0",
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
)}
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
labelSrOnly={true}
value={value}
addOnFilled={false}
addOnClassname={classNames(
"hover:border-default",
errors.licenseKey === undefined && isDirty && "group-hover:border-emphasis"
)}
addOnSuffix={
checkLicenseLoading ? (
<Icon name="loader" className="h-5 w-5 animate-spin" />
) : errors.licenseKey === undefined && isDirty ? (
<Icon name="check" className="h-5 w-5 text-green-700" />
) : undefined
}
color={errors.licenseKey ? "warn" : ""}
onBlur={onBlur}
onChange={async (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
formMethods.setValue("licenseKey", e.target.value);
await formMethods.trigger("licenseKey");
}}
/>
)}
/>
</div>
</form>
</FormProvider>
);
};
export default EnterpriseLicense;

View File

@@ -0,0 +1,40 @@
import { useRouter } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
const StepDone = (props: {
currentStep: number;
nextStepPath: string;
setIsPending: Dispatch<SetStateAction<boolean>>;
}) => {
const router = useRouter();
const { t } = useLocale();
return (
<form
id={`wizard-step-${props.currentStep}`}
name={`wizard-step-${props.currentStep}`}
className="flex justify-center space-y-4"
onSubmit={(e) => {
props.setIsPending(true);
e.preventDefault();
router.replace(props.nextStepPath);
}}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
<Icon
name="check"
className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10"
/>
</div>
<div className="max-w-[420px] text-center">
<h2 className="mb-1 mt-6 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
</div>
</div>
</form>
);
};
export default StepDone;