Admin Wizard Choose License (#6574)
* Implementation * i18n * More i18n * extracted i18n, needs api to get most recent price, added hint: update later * Fixing i18n var * Fix booking filters not working for admin (#6576) * fix: react-select overflow issue in some modals. (#6587) * feat: add a disable overflow prop * feat: use the disable overflow prop * Tailwind Merge (#6596) * Tailwind Merge * Fix merge classNames * [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561) * [CAL-808] /availability/single - UI issue on buttons beside time inputs * Update apps/web/public/static/locales/en/common.json * Update packages/features/schedules/components/Schedule.tsx * create new translation for tooltip Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> * Bye bye submodules (#6585) * WIP * Uses ssh instead * Update .gitignore * Update .gitignore * Update Makefile * Update git-setup.sh * Update git-setup.sh * Replaced Makefile with bash script * Update package.json * fix: show button on empty string (#6601) Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: add delete in dropdown (#6599) Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Update README.md * Update README.md * Changed a neutral- classes to gray (#6603) * Changed a neutral- classes to gray * Changed all border-1 to border * Update package.json * Test fixes * Yarn lock fixes * Fix string equality check in git-setup.sh * [CAL-811] Avatar icon not redirecting user back to the main page (#6586) * Remove cursor-pointer, remove old Avatar* files * Fixed styling for checkedSelect + some cleanup Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> * Harsh/add member invite (#6598) Co-authored-by: Guest <guest@pop-os.localdomain> Co-authored-by: root <harsh.singh@gocomet.com> * Regenerated lockfile without upgrade (#6610) * fix: remove annoying outline when <Button /> clicked (#6537) * fix: remove annoying outline when <Button /> clicked * Delete yarn.lock * remove 1 on 1 icon (#6609) * removed 1-on-1 badge * changed user to users for group events * fix: case-sensitivity in apps path (#6552) * fix: lowercase slug * fix: make fallback blocking * Fix FAB (#6611) * feat: add LocationSelect component (#6571) * feat: add LocationSelect component Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: type error Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * chore: type error Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Update booking filters design (#6543) * Update booking filters * Add filter on YOUR bookings * Fix pending members showing up in list * Reduce the avatar size to 'sm' for now * Bugfix/dropdown menu trigger as child remove class names (#6614) * Fix UsernameTextfield to take right height * Remove className side-effect * Incorrect resolution version fixed * Converted mobile DropdownMenuTrigger styles into Button * v2.5.3 * fix: use items-center (#6618) * fix tooltip and modal stacking issues (#6491) * fix tooltip and modal stacking issues * use z-index in larger screens and less Co-authored-by: Alex van Andel <me@alexvanandel.com> * Temporary fix (#6626) * Fix Ga4 tracking (#6630) * generic <UpgradeScreen> component (#6594) * first attempt of <UpgradeScreen> * changes to icons * reverted changes back to initial state, needs fix: teams not showing * WIP * Fix weird reactnode error * Fix loading text * added upgradeTip to routing forms * icon colors * create and use hook to check if user has team plan * use useTeamPlan for upgradeTeamsBadge * replace huge svg with compressed jpeg * responsive fixes * Update packages/ui/components/badge/UpgradeTeamsBadge.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Give team plan features to E2E tests * Allow option to make a user part of team int ests * Remove flash of paywall for team user * Add team user for typeform tests as well Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> * Removing env var to rely on db * Restoring i18n keys, set loading moved * Fixing tailwind-preset glob * Wizard width fix for md+ screens * Converting licenses options to radix radio * Applying feedback + other tweaks * Reverting this, not this PR related * Unneeded code removal * Reverting unneeded style change * Applying feedback * Removing licenseType * Upgrades typescript * Update yarn lock * Typings * Hotfix: ping,riverside,whereby and around not showing up in list (#6712) * Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713) * Adds deployment settings to DB (#6706) * WIP * Adds DeploymentTheme * Add missing migrations * Adds client extensions for deployment * Cleanup * Delete migration.sql * Relying on both, env var and new model * Restoring env example doc for backward compat * Maximum call stack size exceeded fix? * Revert upgrade * Update index.ts * Delete index.ts * Not exposing license key, fixed radio behavior * Covering undefined env var * Self contained checkLicense * Feedback * Moar feedback * Feedback * Feedback * Feedback * Cleanup --------- Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com> Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com> Co-authored-by: Guest <guest@pop-os.localdomain> Co-authored-by: root <harsh.singh@gocomet.com> Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com> Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>pull/6915/merge
parent
8c150264f4
commit
a9af2fb255
|
@ -1,20 +1,21 @@
|
|||
# ********** INDEX **********
|
||||
#
|
||||
# - LICENSE
|
||||
# - LICENSE (DEPRECATED)
|
||||
# - DATABASE
|
||||
# - SHARED
|
||||
# - NEXTAUTH
|
||||
# - E-MAIL SETTINGS
|
||||
|
||||
# - LICENSE *************************************************************************************************
|
||||
# https://github.com/calendso/calendso/blob/main/LICENSE
|
||||
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||
#
|
||||
# Summary of terms:
|
||||
# - The codebase has to stay open source, whether it was modified or not
|
||||
# - You can not repackage or sell the codebase
|
||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||
#
|
||||
# To enable enterprise-only features, fill your license key in here.
|
||||
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
|
||||
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||
# @see https://console.cal.com
|
||||
CALCOM_LICENSE_KEY=
|
||||
# ***********************************************************************************************************
|
||||
|
|
|
@ -1,17 +1,32 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import classNames from "classnames";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
import { FiUserCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
const router = useRouter();
|
||||
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={props.onSuccess}>
|
||||
<EmptyScreen
|
||||
Icon={FiUserCheck}
|
||||
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({
|
||||
|
@ -45,11 +60,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||
});
|
||||
|
||||
const onError = () => {
|
||||
props.setIsLoading(false);
|
||||
props.onError();
|
||||
};
|
||||
|
||||
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
||||
props.setIsLoading(true);
|
||||
props.onSubmit();
|
||||
const response = await fetch("/api/auth/setup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
|
@ -69,9 +84,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||
email: data.email_address.toLowerCase(),
|
||||
password: data.password,
|
||||
});
|
||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||
props.onSuccess();
|
||||
} else {
|
||||
router.replace("/auth/setup");
|
||||
props.onError();
|
||||
}
|
||||
}, onError);
|
||||
|
||||
|
@ -186,5 +201,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupFormStep1;
|
|
@ -0,0 +1,76 @@
|
|||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
const ENTERPRISE_BOOKING_FEE = "$99"; // TODO: get this from a new API endpoint
|
||||
|
||||
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(
|
||||
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||
value === "FREE" && "ring-2 ring-black"
|
||||
)}>
|
||||
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
|
||||
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
|
||||
<p className="text-gray-500">{t("forever_open_and_free")}</p>
|
||||
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||
<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">
|
||||
<div
|
||||
className={classNames(
|
||||
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||
value === "EE" && "ring-2 ring-black"
|
||||
)}>
|
||||
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
|
||||
<p className="font-medium text-green-800">
|
||||
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
|
||||
</p>
|
||||
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
|
||||
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||
<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>
|
||||
</RadioGroup.Item>
|
||||
</RadioGroup.Root>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChooseLicense;
|
|
@ -0,0 +1,144 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { noop } from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Button, TextField } from "@calcom/ui";
|
||||
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
|
||||
|
||||
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="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<Button
|
||||
className="w-full justify-center text-lg"
|
||||
EndIcon={FiExternalLink}
|
||||
href="https://console.cal.com"
|
||||
target="_blank">
|
||||
{t("purchase_license")}
|
||||
</Button>
|
||||
<div className="relative flex justify-center">
|
||||
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
|
||||
<span className="absolute mt-[22px] bg-white 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(
|
||||
"mb-0 group-hover:border-gray-400",
|
||||
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
|
||||
)}
|
||||
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
|
||||
labelSrOnly={true}
|
||||
value={value}
|
||||
addOnFilled={false}
|
||||
addOnClassname={classNames(
|
||||
"hover:border-gray-300",
|
||||
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
|
||||
)}
|
||||
addOnSuffix={
|
||||
checkLicenseLoading ? (
|
||||
<FiLoader className="h-5 w-5 animate-spin" />
|
||||
) : errors.licenseKey === undefined && isDirty ? (
|
||||
<FiCheck 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;
|
|
@ -1,20 +1,26 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { FiCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
const StepDone = () => {
|
||||
const StepDone = (props: {
|
||||
currentStep: number;
|
||||
nextStepPath: string;
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<form
|
||||
id="wizard-step-1"
|
||||
name="wizard-step-1"
|
||||
className="space-y-4"
|
||||
id={`wizard-step-${props.currentStep}`}
|
||||
name={`wizard-step-${props.currentStep}`}
|
||||
className="flex justify-center space-y-4"
|
||||
onSubmit={(e) => {
|
||||
props.setIsLoading(true);
|
||||
e.preventDefault();
|
||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||
router.replace(props.nextStepPath);
|
||||
}}>
|
||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
|
@ -360,7 +360,7 @@ export default NextAuth({
|
|||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
const calendsoSession: Session = {
|
||||
...session,
|
||||
hasValidLicense,
|
||||
|
|
|
@ -1,40 +1,125 @@
|
|||
import { UserPermissionRole } from "@prisma/client";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { WizardForm } from "@calcom/ui";
|
||||
import { Meta, WizardForm } from "@calcom/ui";
|
||||
|
||||
import SetupFormStep1 from "./steps/SetupFormStep1";
|
||||
import StepDone from "./steps/StepDone";
|
||||
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
|
||||
import ChooseLicense from "@components/setup/ChooseLicense";
|
||||
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
||||
const shouldDisable = props.userCount !== 0;
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
|
||||
const isFreeLicense = value === "FREE";
|
||||
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
|
||||
const setStep = (newStep: number) => {
|
||||
router.replace(`/auth/setup?step=${newStep || 1}`, undefined, { shallow: true });
|
||||
};
|
||||
|
||||
const steps = [
|
||||
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
|
||||
{
|
||||
title: t("administrator_user"),
|
||||
description: t("lets_create_first_administrator_user"),
|
||||
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
||||
isLoading: isLoadingStep1,
|
||||
content: (setIsLoading) => (
|
||||
<AdminUser
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(2);
|
||||
}}
|
||||
onError={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
userCount={props.userCount}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("enable_apps"),
|
||||
description: t("enable_apps_description"),
|
||||
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
|
||||
isLoading: false,
|
||||
title: t("choose_a_license"),
|
||||
description: t("choose_license_description"),
|
||||
content: (setIsLoading) => {
|
||||
return (
|
||||
<ChooseLicense
|
||||
id="wizard-step-2"
|
||||
name="wizard-step-2"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!isFreeLicense) {
|
||||
steps.push({
|
||||
title: t("step_enterprise_license"),
|
||||
description: t("step_enterprise_license_description"),
|
||||
content: (setIsLoading) => {
|
||||
const currentStep = 3;
|
||||
return (
|
||||
<EnterpriseLicense
|
||||
id={`wizard-step-${currentStep}`}
|
||||
name={`wizard-step-${currentStep}`}
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(currentStep + 1);
|
||||
}}
|
||||
onSuccessValidate={() => {
|
||||
setIsEnabledEE(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
isEnabled: isEnabledEE,
|
||||
});
|
||||
}
|
||||
|
||||
steps.push({
|
||||
title: t("enable_apps"),
|
||||
description: t("enable_apps_description"),
|
||||
contentClassname: "!pb-0 mb-[-1px]",
|
||||
content: (setIsLoading) => {
|
||||
const currentStep = isFreeLicense ? 3 : 4;
|
||||
return (
|
||||
<AdminAppsList
|
||||
classNames={{
|
||||
form: "mb-4 rounded-md bg-white px-0 pt-0 md:max-w-full",
|
||||
appCategoryNavigationContainer: " max-h-[400px] overflow-y-auto md:p-4",
|
||||
verticalTabsItem: "!w-48 md:p-4",
|
||||
}}
|
||||
baseURL={`/auth/setup?step=${currentStep}`}
|
||||
useQueryParam={true}
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
router.replace("/");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="flex items-center bg-gray-100 print:h-full">
|
||||
<Meta title={t("setup")} description={t("setup_description")} />
|
||||
<main className="flex items-center bg-gray-100 print:h-full md:h-screen">
|
||||
<WizardForm
|
||||
href="/auth/setup"
|
||||
steps={steps}
|
||||
|
@ -42,6 +127,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
finishLabel={t("finish")}
|
||||
prevLabel={t("prev_step")}
|
||||
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
|
||||
containerClassname="md:w-[800px]"
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
|
@ -49,6 +135,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const userCount = await prisma.user.count();
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
|
@ -62,8 +149,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
};
|
||||
}
|
||||
|
||||
let deploymentKey = await getDeploymentKey(prisma);
|
||||
|
||||
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
|
||||
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
|
||||
await prisma.deployment.upsert({
|
||||
where: { id: 1 },
|
||||
update: {
|
||||
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
||||
agreedLicenseAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
||||
agreedLicenseAt: new Date(),
|
||||
},
|
||||
});
|
||||
deploymentKey = await getDeploymentKey(prisma);
|
||||
}
|
||||
|
||||
const isFreeLicense = deploymentKey === "";
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: ssr.dehydrate(),
|
||||
isFreeLicense,
|
||||
userCount,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -105,7 +105,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||
</p>
|
||||
))}
|
||||
</header>
|
||||
<Steps maxSteps={steps.length} currentStep={currentStepIndex} navigateToStep={goToIndex} />
|
||||
<Steps maxSteps={steps.length} currentStep={currentStepIndex + 1} navigateToStep={goToIndex} />
|
||||
</div>
|
||||
<StepCard>
|
||||
{currentStep === "user-settings" && <UserSettings user={user} nextStep={() => goToIndex(1)} />}
|
||||
|
|
|
@ -515,6 +515,8 @@
|
|||
"admin": "Admin",
|
||||
"administrator_user": "Administrator user",
|
||||
"lets_create_first_administrator_user": "Let's create the first administrator user.",
|
||||
"admin_user_created": "Administrator user setup",
|
||||
"admin_user_created_description": "You have already created an administrator user. You can now log in to your account.",
|
||||
"new_member": "New Member",
|
||||
"invite": "Invite",
|
||||
"add_team_members": "Add team members",
|
||||
|
@ -796,7 +798,7 @@
|
|||
"number_apps_one": "{{count}} App",
|
||||
"number_apps_other": "{{count}} Apps",
|
||||
"trending_apps": "Trending Apps",
|
||||
"most_popular":"Most Popular",
|
||||
"most_popular": "Most Popular",
|
||||
"explore_apps": "{{category}} apps",
|
||||
"installed_apps": "Installed Apps",
|
||||
"free_to_use_apps": "Free",
|
||||
|
@ -841,7 +843,7 @@
|
|||
"connect_metamask": "Connect Metamask",
|
||||
"create_events_on": "Create events on",
|
||||
"enterprise_license": "This is an enterprise feature",
|
||||
"enterprise_license_description": "To enable this feature, get a deployment key at {{consoleUrl}} console and add it to your .env as CALCOM_LICENSE_KEY. If your team already has a license, please contact {{supportMail}} for help.",
|
||||
"enterprise_license_description": "To enable this feature, have an administrator go to {{setupUrl}} to enter a license key. If a license key is already in place, please contact {{supportMail}} for help.",
|
||||
"missing_license": "Missing License",
|
||||
"signup_requires": "Commercial license required",
|
||||
"signup_requires_description": "{{companyName}} currently does not offer a free open source version of the sign up page. To receive full access to the signup components you need to acquire a commercial license. For personal use we recommend the Prisma Data Platform or any other Postgres interface to create accounts.",
|
||||
|
@ -1114,8 +1116,8 @@
|
|||
"close": "Close",
|
||||
"upgrade": "Upgrade",
|
||||
"upgrade_to_access_recordings_title": "Upgrade to access recordings",
|
||||
"upgrade_to_access_recordings_description":"Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
|
||||
"recordings_are_part_of_the_teams_plan":"Recordings are part of the teams plan",
|
||||
"upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
|
||||
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
|
||||
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
|
||||
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
|
||||
"show_eventtype_on_profile": "Show on Profile",
|
||||
|
@ -1414,7 +1416,7 @@
|
|||
"team_url_required": "Must enter a team URL",
|
||||
"team_url_taken": "This URL is already taken",
|
||||
"team_publish": "Publish team",
|
||||
"number_sms_notifications": "Phone number (SMS\u00a0notifications)",
|
||||
"number_sms_notifications": "Phone number (SMS notifications)",
|
||||
"attendee_email_variable": "Attendee email",
|
||||
"attendee_email_info": "The person booking's email",
|
||||
"kbar_search_placeholder": "Type a command or search...",
|
||||
|
@ -1451,6 +1453,9 @@
|
|||
"disabled_calendar": "If you have another calendar installed new bookings will be added to it. If not then connect a new calendar so you do not miss any new bookings.",
|
||||
"enable_apps": "Enable Apps",
|
||||
"enable_apps_description": "Enable apps that users can integrate with Cal.com",
|
||||
"purchase_license": "Purchase a License",
|
||||
"already_have_key": "I already have a key:",
|
||||
"already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.",
|
||||
"app_is_enabled": "{{appName}} is enabled",
|
||||
"app_is_disabled": "{{appName}} is disabled",
|
||||
"keys_have_been_saved": "Keys have been saved",
|
||||
|
@ -1496,11 +1501,11 @@
|
|||
"not_verified": "Not yet verified",
|
||||
"no_availability_in_month": "No availability in {{month}}",
|
||||
"view_next_month": "View next month",
|
||||
"send_code" : "Send code",
|
||||
"send_code": "Send code",
|
||||
"number_verified": "Number Verified",
|
||||
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
|
||||
"create_webhook_team_event_type": "Create a webhook for this team event type",
|
||||
"disable_success_page":"Disable Success Page (only works if you have a redirect URL)",
|
||||
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
|
||||
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters",
|
||||
"change_password_admin": "Change Password to gain admin access",
|
||||
"username_already_taken": "Username is already taken",
|
||||
|
@ -1526,6 +1531,25 @@
|
|||
"reporting_feature": "See all incoming from data and download it as a CSV",
|
||||
"teams_plan_required": "Teams plan required",
|
||||
"routing_forms_are_a_great_way": "Routing forms are a great way to route your incoming leads to the right person. Upgrade to a Teams plan to access this feature.",
|
||||
"choose_a_license": "Choose a license",
|
||||
"choose_license_description": "Cal.com comes with an accessible and free AGPLv3 license which some limitations which can be upgraded to an Enterprise license at any time. You can upgrade at anytime later.",
|
||||
"license": "License",
|
||||
"agplv3_license": "AGPLv3 License",
|
||||
"ee_enterprise_license": "“/ee” Enterprise License",
|
||||
"enterprise_booking_fee": "Starting at {{enterprise_booking_fee}}/month",
|
||||
"enterprise_license_includes": "Everything for a commercial use case",
|
||||
"no_need_to_keep_your_code_open_source": "No need to keep your code open-source",
|
||||
"repackage_rebrand_resell": "Repackage, rebrand and resell easily",
|
||||
"a_vast_suite_of_enterprise_features": "A vast suite of enterprise features",
|
||||
"free_license_fee": "$0.00/month",
|
||||
"forever_open_and_free": "Forever Open & Free",
|
||||
"required_to_keep_your_code_open_source": "Required to keep your code open source",
|
||||
"cannot_repackage_and_resell": "Cannot repackage, rebrand and resell easily",
|
||||
"no_enterprise_features": "No enterprise features",
|
||||
"step_enterprise_license": "Enterprise License",
|
||||
"step_enterprise_license_description": "Everything for a commercial use case with private hosting, repackaging, rebranding and reselling and access exclusive enterprise components.",
|
||||
"setup": "Setup",
|
||||
"setup_description": "Setup Cal.com instance",
|
||||
"configure": "Configure",
|
||||
"sso_configuration": "Single Sign-On",
|
||||
"sso_configuration_description": "Configure SAML/OIDC SSO and allow team members to login using an Identity Provider",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { classNames as cs } from "@calcom/lib";
|
||||
import { HorizontalTabs, VerticalTabs } from "@calcom/ui";
|
||||
|
||||
import getAppCategories from "../_utils/getAppCategories";
|
||||
|
@ -11,33 +11,39 @@ const AppCategoryNavigation = ({
|
|||
children,
|
||||
containerClassname,
|
||||
className,
|
||||
fromAdmin,
|
||||
classNames,
|
||||
useQueryParam = false,
|
||||
}: {
|
||||
baseURL: string;
|
||||
children: React.ReactNode;
|
||||
containerClassname: string;
|
||||
/** @deprecated use classNames instead */
|
||||
containerClassname?: string;
|
||||
/** @deprecated use classNames instead */
|
||||
className?: string;
|
||||
fromAdmin?: boolean;
|
||||
classNames?: {
|
||||
root?: string;
|
||||
container?: string;
|
||||
verticalTabsItem?: string;
|
||||
};
|
||||
useQueryParam?: boolean;
|
||||
}) => {
|
||||
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const appCategories = useMemo(() => getAppCategories(baseURL, useQueryParam), [baseURL, useQueryParam]);
|
||||
|
||||
return (
|
||||
<div className={classNames("flex flex-col gap-x-6 p-2 md:p-0 xl:flex-row", className)}>
|
||||
<div className={cs("flex flex-col gap-x-6 p-2 md:p-0 xl:flex-row", classNames?.root ?? className)}>
|
||||
<div className="hidden xl:block">
|
||||
<VerticalTabs
|
||||
tabs={appCategories}
|
||||
sticky
|
||||
linkProps={{ shallow: true }}
|
||||
itemClassname={classNames(fromAdmin && "w-60")}
|
||||
itemClassname={classNames?.verticalTabsItem}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4 block overflow-x-scroll xl:hidden">
|
||||
<HorizontalTabs tabs={appCategories} linkProps={{ shallow: true }} />
|
||||
</div>
|
||||
<main className={containerClassname} ref={animationRef}>
|
||||
<main className={classNames?.container ?? containerClassname} ref={animationRef}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@ module.exports = {
|
|||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/app-store/**/{components,pages}/**/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/app-store/**/*{components,pages}/**/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/features/**/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/ui/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AppCategories } from "@prisma/client";
|
||||
import { noop } from "lodash";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useReducer, FC } from "react";
|
||||
import { FC, useReducer, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||
import { classNames as cs } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
|
@ -32,9 +34,9 @@ import {
|
|||
} from "@calcom/ui";
|
||||
import {
|
||||
FiAlertCircle,
|
||||
FiCheckCircle,
|
||||
FiEdit,
|
||||
FiMoreHorizontal,
|
||||
FiCheckCircle,
|
||||
FiXCircle,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
|
@ -148,26 +150,39 @@ const AdminAppsList = ({
|
|||
baseURL,
|
||||
className,
|
||||
useQueryParam = false,
|
||||
classNames,
|
||||
onSubmit = noop,
|
||||
...rest
|
||||
}: {
|
||||
baseURL: string;
|
||||
classNames?: {
|
||||
form?: string;
|
||||
appCategoryNavigationRoot?: string;
|
||||
appCategoryNavigationContainer?: string;
|
||||
verticalTabsItem?: string;
|
||||
};
|
||||
className?: string;
|
||||
useQueryParam?: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
onSubmit?: () => void;
|
||||
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">) => {
|
||||
return (
|
||||
<form
|
||||
id="wizard-step-2"
|
||||
name="wizard-step-2"
|
||||
{...rest}
|
||||
className={
|
||||
classNames?.form ?? "max-w-80 mb-4 rounded-md bg-white px-0 pt-0 md:max-w-full md:px-8 md:pt-10"
|
||||
}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
router.replace("/");
|
||||
onSubmit();
|
||||
}}>
|
||||
<AppCategoryNavigation
|
||||
baseURL={baseURL}
|
||||
fromAdmin
|
||||
useQueryParam={useQueryParam}
|
||||
containerClassname="min-w-0 w-full"
|
||||
className={className}>
|
||||
classNames={{
|
||||
root: className,
|
||||
verticalTabsItem: classNames?.verticalTabsItem,
|
||||
container: cs("min-w-0 w-full", classNames?.appCategoryNavigationContainer ?? "max-w-[500px]"),
|
||||
}}>
|
||||
<AdminAppsListContainer />
|
||||
</AppCategoryNavigation>
|
||||
</form>
|
||||
|
|
|
@ -43,16 +43,8 @@ const ApiKeyListItem = ({
|
|||
<div>
|
||||
<p className="font-medium"> {apiKey?.note ? apiKey.note : t("api_key_no_note")}</p>
|
||||
<div className="flex items-center space-x-3.5">
|
||||
{!neverExpires && isExpired && (
|
||||
<Badge className="-p-2" variant="red">
|
||||
{t("expired")}
|
||||
</Badge>
|
||||
)}
|
||||
{!isExpired && (
|
||||
<Badge className="-p-2" variant="green">
|
||||
{t("active")}
|
||||
</Badge>
|
||||
)}
|
||||
{!neverExpires && isExpired && <Badge variant="red">{t("expired")}</Badge>}
|
||||
{!isExpired && <Badge variant="green">{t("active")}</Badge>}
|
||||
<p className="text-xs text-gray-600">
|
||||
{" "}
|
||||
{neverExpires ? (
|
||||
|
|
|
@ -7,7 +7,7 @@ import DOMPurify from "dompurify";
|
|||
import { useSession } from "next-auth/react";
|
||||
import React, { AriaRole, ComponentType, Fragment } from "react";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { FiAlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
@ -46,6 +46,7 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) =
|
|||
consoleUrl: `<a href="https://go.cal.com/console" target="_blank" class="underline">
|
||||
${APP_NAME}
|
||||
</a>`,
|
||||
setupUrl: `<a href="${WEBAPP_URL}/auth/setup">/auth/setup</a>`,
|
||||
supportMail: `<a href="mailto:sales@cal.com" class="underline">
|
||||
sales@cal.com
|
||||
</a>`,
|
||||
|
|
|
@ -2,7 +2,7 @@ import DOMPurify from "dompurify";
|
|||
import { useSession } from "next-auth/react";
|
||||
import React, { AriaRole, ComponentType, Fragment } from "react";
|
||||
|
||||
import { APP_NAME, CONSOLE_URL, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
import { APP_NAME, CONSOLE_URL, SUPPORT_MAIL_ADDRESS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { FiAlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
@ -36,9 +36,9 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) =
|
|||
consoleUrl: `<a href="${CONSOLE_URL}" target="_blank" rel="noopener noreferrer" class="underline">
|
||||
${APP_NAME}
|
||||
</a>`,
|
||||
setupUrl: `<a href="${WEBAPP_URL}/auth/setup" class="underline">/auth/setup</a>`,
|
||||
supportMail: `<a href="mailto:${SUPPORT_MAIL_ADDRESS}" class="underline">
|
||||
${SUPPORT_MAIL_ADDRESS}
|
||||
</a>`,
|
||||
${SUPPORT_MAIL_ADDRESS}</a>`,
|
||||
})
|
||||
),
|
||||
}}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { PrismaClient } from "@prisma/client";
|
||||
import cache from "memory-cache";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -18,10 +19,21 @@ const schemaLicenseKey = z
|
|||
: v;
|
||||
});
|
||||
|
||||
async function checkLicense(license: string): Promise<boolean> {
|
||||
async function checkLicense(
|
||||
/** The prisma client to use (necessary for public API to handle custom prisma instances) */
|
||||
prisma: PrismaClient
|
||||
): Promise<boolean> {
|
||||
/** We skip for E2E testing */
|
||||
if (!!process.env.NEXT_PUBLIC_IS_E2E) return true;
|
||||
if (!license) return false;
|
||||
const url = `${CONSOLE_URL}/api/license?key=${schemaLicenseKey.parse(license)}`;
|
||||
/** We check first on env */
|
||||
let licenseKey = process.env.CALCOM_LICENSE_KEY;
|
||||
if (!licenseKey) {
|
||||
/** We try to check on DB only if env is undefined */
|
||||
const deployment = await prisma.deployment.findFirst({ where: { id: 1 } });
|
||||
licenseKey = deployment?.licenseKey ?? undefined;
|
||||
}
|
||||
if (!licenseKey) return false;
|
||||
const url = `${CONSOLE_URL}/api/license?key=${schemaLicenseKey.parse(licenseKey)}`;
|
||||
const cachedResponse = cache.get(url);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
export async function getDeploymentKey(prisma: PrismaClient) {
|
||||
const deployment = await prisma.deployment.findUnique({
|
||||
where: { id: 1 },
|
||||
select: { licenseKey: true },
|
||||
});
|
||||
return deployment?.licenseKey || process.env.CALCOM_LICENSE_KEY || "";
|
||||
}
|
|
@ -209,6 +209,14 @@ export const KBarRoot = ({ children }: { children: React.ReactNode }) => {
|
|||
keywords: "user impersonation",
|
||||
perform: () => router.push("/settings/security/impersonation"),
|
||||
},
|
||||
{
|
||||
id: "license",
|
||||
name: "choose_a_license",
|
||||
section: "admin",
|
||||
shortcut: ["u", "l"],
|
||||
keywords: "license",
|
||||
perform: () => router.push("/auth/setup?step=1"),
|
||||
},
|
||||
{
|
||||
id: "webhooks",
|
||||
name: "Webhooks",
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
FiChevronRight,
|
||||
FiPlus,
|
||||
FiMenu,
|
||||
FiExternalLink,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
const tabs: VerticalTabItemProps[] = [
|
||||
|
@ -89,6 +90,7 @@ const tabs: VerticalTabItemProps[] = [
|
|||
icon: FiLock,
|
||||
children: [
|
||||
//
|
||||
{ name: "license", href: "/auth/setup?step=1" },
|
||||
{ name: "impersonation", href: "/settings/admin/impersonation" },
|
||||
{ name: "apps", href: "/settings/admin/apps/calendar" },
|
||||
{ name: "users", href: "https://console.cal.com" },
|
||||
|
|
|
@ -41,6 +41,7 @@ import { appsRouter } from "./viewer/apps";
|
|||
import { authRouter } from "./viewer/auth";
|
||||
import { availabilityRouter } from "./viewer/availability";
|
||||
import { bookingsRouter } from "./viewer/bookings";
|
||||
import { deploymentSetupRouter } from "./viewer/deploymentSetup";
|
||||
import { eventTypesRouter } from "./viewer/eventTypes";
|
||||
import { slotsRouter } from "./viewer/slots";
|
||||
import { ssoRouter } from "./viewer/sso";
|
||||
|
@ -1150,6 +1151,7 @@ export const viewerRouter = mergeRouters(
|
|||
loggedInViewerRouter,
|
||||
public: publicViewerRouter,
|
||||
auth: authRouter,
|
||||
deploymentSetup: deploymentSetupRouter,
|
||||
bookings: bookingsRouter,
|
||||
eventTypes: eventTypesRouter,
|
||||
availability: availabilityRouter,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { router, authedAdminProcedure } from "../../trpc";
|
||||
|
||||
export const deploymentSetupRouter = router({
|
||||
update: authedAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
licenseKey: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const data = {
|
||||
agreedLicenseAt: new Date(),
|
||||
licenseKey: input.licenseKey,
|
||||
};
|
||||
|
||||
await prisma.deployment.upsert({ where: { id: 1 }, create: data, update: data });
|
||||
|
||||
return;
|
||||
}),
|
||||
});
|
|
@ -1,7 +1,5 @@
|
|||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/** Needed to enable enterprise-only features */
|
||||
readonly CALCOM_LICENSE_KEY: string | undefined;
|
||||
readonly CALCOM_TELEMETRY_DISABLED: string | undefined;
|
||||
readonly CALENDSO_ENCRYPTION_KEY: string | undefined;
|
||||
readonly DATABASE_URL: string | undefined;
|
||||
|
|
|
@ -211,7 +211,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
|
|||
<StartIcon
|
||||
className={classNames(
|
||||
variant === "icon" && "h-4 w-4",
|
||||
variant === "button" && "h-4 w-4 stroke-[1.5px] ltr:mr-2 rtl:ml-2"
|
||||
variant === "button" && "h-4 w-4 stroke-[1.5px] ltr:ml-2 rtl:mr-2"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -117,7 +117,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
|
|||
</Skeleton>
|
||||
)}
|
||||
{addOnLeading || addOnSuffix ? (
|
||||
<div className="relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
|
||||
<div className="group relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
|
||||
{addOnLeading && (
|
||||
<Addon
|
||||
isFilled={addOnFilled}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
|
||||
type DefaultStep = {
|
||||
|
@ -16,12 +17,13 @@ function Stepper<T extends DefaultStep>(props: {
|
|||
steps,
|
||||
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
|
||||
} = props;
|
||||
const [stepperRef] = useAutoAnimate<HTMLOListElement>();
|
||||
return (
|
||||
<>
|
||||
{steps.length > 1 && (
|
||||
<nav className="flex items-center justify-center" aria-label="Progress">
|
||||
<p className="text-sm font-medium">{stepLabel(props.step, steps.length)}</p>
|
||||
<ol role="list" className="ml-8 flex items-center space-x-5">
|
||||
<ol role="list" className="ml-8 flex items-center space-x-5" ref={stepperRef}>
|
||||
{steps.map((mapStep, index) => (
|
||||
<li key={mapStep.title}>
|
||||
<Link
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
interface ISteps {
|
||||
maxSteps: number;
|
||||
currentStep: number;
|
||||
navigateToStep: (step: number) => void;
|
||||
stepLabel?: (currentStep: number, maxSteps: number) => string;
|
||||
}
|
||||
|
||||
const Steps = (props: ISteps) => {
|
||||
const { maxSteps, currentStep, navigateToStep } = props;
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
maxSteps,
|
||||
currentStep,
|
||||
navigateToStep,
|
||||
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
|
||||
} = props;
|
||||
return (
|
||||
<div className="mt-6 space-y-2">
|
||||
<p className="text-xs font-medium text-gray-500 dark:text-white">
|
||||
{t("current_step_of_total", { currentStep: currentStep + 1, maxSteps })}
|
||||
</p>
|
||||
<p className="text-xs font-medium text-gray-500 dark:text-white">{stepLabel(currentStep, maxSteps)}</p>
|
||||
<div className="flex w-full space-x-2 rtl:space-x-reverse">
|
||||
{new Array(maxSteps).fill(0).map((_s, index) => {
|
||||
return index <= currentStep ? (
|
||||
return index <= currentStep - 1 ? (
|
||||
<div
|
||||
key={`step-${index}`}
|
||||
onClick={() => navigateToStep(index)}
|
||||
className={classNames(
|
||||
"h-1 w-full rounded-[1px] bg-black dark:bg-white",
|
||||
index < currentStep ? "cursor-pointer" : ""
|
||||
index < currentStep - 1 ? "cursor-pointer" : ""
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { noop } from "lodash";
|
||||
import { useRouter } from "next/router";
|
||||
import { ComponentProps } from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
import { Button, Stepper } from "../../..";
|
||||
import { Button, Steps } from "../../..";
|
||||
|
||||
type DefaultStep = {
|
||||
title: string;
|
||||
containerClassname?: string;
|
||||
contentClassname?: string;
|
||||
description: string;
|
||||
content: JSX.Element;
|
||||
enabled?: boolean;
|
||||
isLoading: boolean;
|
||||
content?: ((setIsLoading: Dispatch<SetStateAction<boolean>>) => JSX.Element) | JSX.Element;
|
||||
isEnabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
function WizardForm<T extends DefaultStep>(props: {
|
||||
|
@ -21,7 +24,7 @@ function WizardForm<T extends DefaultStep>(props: {
|
|||
prevLabel?: string;
|
||||
nextLabel?: string;
|
||||
finishLabel?: string;
|
||||
stepLabel?: ComponentProps<typeof Stepper>["stepLabel"];
|
||||
stepLabel?: React.ComponentProps<typeof Steps>["stepLabel"];
|
||||
}) {
|
||||
const { href, steps, nextLabel = "Next", finishLabel = "Finish", prevLabel = "Back", stepLabel } = props;
|
||||
const router = useRouter();
|
||||
|
@ -30,55 +33,54 @@ function WizardForm<T extends DefaultStep>(props: {
|
|||
const setStep = (newStep: number) => {
|
||||
router.replace(`${href}?step=${newStep || 1}`, undefined, { shallow: true });
|
||||
};
|
||||
const [currentStepIsLoading, setCurrentStepIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentStepIsLoading(false);
|
||||
}, [currentStep]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-4 print:w-full">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img className="mx-auto mb-8 h-8" src="https://cal.com/logo.svg" alt="Cal.com Logo" />
|
||||
<div
|
||||
className={classNames(
|
||||
"mb-8 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow print:divide-transparent print:shadow-transparent",
|
||||
props.containerClassname
|
||||
)}>
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
<div className={classNames("overflow-hidden md:mb-2 md:w-[700px]", props.containerClassname)}>
|
||||
<div className="px-6 py-5 sm:px-14">
|
||||
<h1 className="font-cal text-2xl text-gray-900">{currentStep.title}</h1>
|
||||
<p className="text-sm text-gray-500">{currentStep.description}</p>
|
||||
{!props.disableNavigation && (
|
||||
<Steps maxSteps={steps.length} currentStep={step} navigateToStep={noop} stepLabel={stepLabel} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames("mb-8 overflow-hidden md:w-[700px]", props.containerClassname)}>
|
||||
<div className={classNames("print:p-none max-w-3xl px-8 py-5 sm:p-6", currentStep.contentClassname)}>
|
||||
{typeof currentStep.content === "function"
|
||||
? currentStep.content(setCurrentStepIsLoading)
|
||||
: currentStep.content}
|
||||
</div>
|
||||
|
||||
<div className="print:p-none max-w-3xl px-4 py-5 sm:p-6">{currentStep.content}</div>
|
||||
{!props.disableNavigation && (
|
||||
<>
|
||||
{currentStep.enabled !== false && (
|
||||
<div className="flex justify-end px-4 py-4 print:hidden sm:px-6">
|
||||
{step > 1 && (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setStep(step - 1);
|
||||
}}>
|
||||
{prevLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
tabIndex={0}
|
||||
loading={currentStep.isLoading}
|
||||
type="submit"
|
||||
color="primary"
|
||||
form={`wizard-step-${step}`}
|
||||
className="relative ml-2">
|
||||
{step < steps.length ? nextLabel : finishLabel}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-end px-4 py-4 print:hidden sm:px-6">
|
||||
{step > 1 && (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setStep(step - 1);
|
||||
}}>
|
||||
{prevLabel}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
<Button
|
||||
tabIndex={0}
|
||||
loading={currentStepIsLoading}
|
||||
type="submit"
|
||||
color="primary"
|
||||
form={`wizard-step-${step}`}
|
||||
disabled={currentStep.isEnabled === false}
|
||||
className="relative ml-2">
|
||||
{step < steps.length ? nextLabel : finishLabel}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!props.disableNavigation && (
|
||||
<div className="print:hidden">
|
||||
<Stepper href={href} step={step} steps={steps} disableSteps stepLabel={stepLabel} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export interface NavTabProps {
|
|||
itemClassname?: string;
|
||||
}
|
||||
|
||||
const NavTabs = function ({ tabs, className = "", sticky, linkProps, ...props }: NavTabProps) {
|
||||
const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassname, ...props }: NavTabProps) {
|
||||
return (
|
||||
<nav
|
||||
className={classNames(
|
||||
|
@ -26,7 +26,7 @@ const NavTabs = function ({ tabs, className = "", sticky, linkProps, ...props }:
|
|||
{sticky && <div className="pt-6" />}
|
||||
{props.children}
|
||||
{tabs.map((tab, idx) => (
|
||||
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={props.itemClassname} />
|
||||
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={itemClassname} />
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue