diff --git a/apps/web/pages/api/auth/setup.ts b/apps/web/pages/api/auth/setup.ts new file mode 100644 index 0000000000..e87953ec0a --- /dev/null +++ b/apps/web/pages/api/auth/setup.ts @@ -0,0 +1,58 @@ +import { IdentityProvider } from "@prisma/client"; +import { NextApiRequest, NextApiResponse } from "next"; +import z from "zod"; + +import { isPasswordValid } from "@calcom/lib/auth"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +import { hashPassword } from "@lib/auth"; +import prisma from "@lib/prisma"; +import slugify from "@lib/slugify"; + +const querySchema = z.object({ + username: z.string().min(1), + fullname: z.string(), + email: z.string().email({ message: "Please enter a valid email" }), + password: z.string().refine((val) => isPasswordValid(val.trim()), { + message: + "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters", + }), +}); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const userCount = await prisma.user.count(); + if (userCount !== 0) { + throw new HttpError({ statusCode: 400, message: "No setup needed." }); + } + + const parsedQuery = querySchema.safeParse(req.body); + if (!parsedQuery.success) { + throw new HttpError({ statusCode: 422, message: parsedQuery.error.message }); + } + + const username = slugify(parsedQuery.data.username); + const userEmail = parsedQuery.data.email.toLowerCase(); + + const hashedPassword = await hashPassword(parsedQuery.data.password); + + await prisma.user.create({ + data: { + username, + email: userEmail, + password: hashedPassword, + role: "ADMIN", + name: parsedQuery.data.fullname, + emailVerified: new Date(), + locale: "en", // TODO: We should revisit this + plan: "PRO", + identityProvider: IdentityProvider.CAL, + }, + }); + + res.status(201).json({ message: "First admin user created successfuly." }); +} + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 1558bbbaa3..54c4132eaf 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -8,13 +8,14 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import { EmailField, Form, PasswordField } from "@calcom/ui/form/fields"; +import prisma from "@calcom/web/lib/prisma"; import { ErrorCode, getSession } from "@lib/auth"; import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants"; -import { useLocale } from "@lib/hooks/useLocale"; import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@lib/saml"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -217,6 +218,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } + const userCount = await prisma.user.count(); + if (userCount === 0) { + // Proceed to new onboarding to create first admin user + return { + redirect: { + destination: "/auth/setup", + permanent: false, + }, + }; + } + return { props: { csrfToken: await getCsrfToken(context), diff --git a/apps/web/pages/auth/setup.tsx b/apps/web/pages/auth/setup.tsx new file mode 100644 index 0000000000..c11def4ba6 --- /dev/null +++ b/apps/web/pages/auth/setup.tsx @@ -0,0 +1,293 @@ +import { CheckIcon } from "@heroicons/react/solid"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/router"; +import { Controller, useForm } from "react-hook-form"; +import * as z from "zod"; + +import { isPasswordValid } from "@calcom/lib/auth"; +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { inferSSRProps } from "@calcom/types/inferSSRProps"; +import WizardForm from "@calcom/ui/WizardForm"; +import { Input } from "@calcom/ui/form/fields"; +import { Form } from "@calcom/ui/form/fields"; + +import prisma from "@lib/prisma"; + +const schema = z.object({ + username: z.string().min(1), + email: z.string().email({ message: "Please enter a valid email" }), + fullname: z.string(), + password: z.string().refine((val) => isPasswordValid(val.trim()), { + message: + "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters", + }), +}); + +const StepDone = () => { + const { t } = useLocale(); + + return ( +
{currentStep.description}
+