feat: adds auth/signup for ee customers

pull/9078/head
Agusti Fernandez Pardo 2022-05-10 19:52:59 +02:00
parent 08eeb36d47
commit 4c131fbcdd
8 changed files with 280 additions and 10 deletions

8
.gitignore vendored
View File

@ -72,4 +72,10 @@ dist
lint-results
# Yarn
yarn-error.log
yarn-error.log
.turbo
.next
.husky
.vscode
.env

15
auth/README.md Normal file
View File

@ -0,0 +1,15 @@
# Signup for self-hosted EE
In order to open the signup, please replace the contents of the file in
`calcom/apps/web/pages/auth/signup.tsx`
with the contents of the new file provided here in the @calcom/api repo at the folder:
`/auth/signup.tsx`
Once you do, if you run your cal.com self-hosted instance and go to:
`http://localhost:3000/auth/signup`
You should see the signup page, and your users should be able to create new accounts.
Please, keep in mind this will create git conflict resolutions if you try to git pull from main, where the signup.tsx file will still be different. We will find a better workaround for this soon in order to make it pain free.

182
auth/signup.tsx Normal file
View File

@ -0,0 +1,182 @@
import { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { EmailField, PasswordField, TextField } from "@calcom/ui/form/fields";
import { HeadSeo } from "@calcom/web/components/seo/head-seo";
import { asStringOrNull } from "@calcom/web/lib/asStringOrNull";
import { WEBSITE_URL, WEBAPP_URL } from "@calcom/web/lib/config/constants";
import prisma from "@calcom/web/lib/prisma";
import { isSAMLLoginEnabled } from "@calcom/web/lib/saml";
import { IS_GOOGLE_LOGIN_ENABLED } from "@calcom/web/server/lib/constants";
import { ssrInit } from "@calcom/web/server/lib/ssr";
type FormValues = {
username: string;
email: string;
password: string;
passwordcheck: string;
apiError: string;
};
export default function Signup() {
const { t } = useLocale();
const router = useRouter();
const methods = useForm<FormValues>();
const {
register,
formState: { errors, isSubmitting },
} = methods;
const handleErrors = async (resp: Response) => {
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.message);
}
};
const signUp: SubmitHandler<FormValues> = async (data) => {
await fetch("/api/auth/signup", {
body: JSON.stringify({
...data,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
})
.then(handleErrors)
.then(
async () =>
await signIn("Cal.com", {
callbackUrl: (`${WEBSITE_URL}/${router.query.callbackUrl}` || "") as string,
})
)
.catch((err) => {
methods.setError("apiError", { message: err.message });
});
};
return (
<div
className="flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="font-cal text-center text-3xl font-extrabold text-gray-900">
{t("create_your_account")}
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="mx-2 bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(signUp)} className="space-y-6 bg-white">
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
<div className="space-y-2">
<TextField
addOnLeading={
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span>
}
labelProps={{ className: "block text-sm font-medium text-gray-700" }}
className="block w-full min-w-0 flex-grow rounded-none rounded-r-sm border-gray-300 lowercase focus:border-black focus:ring-black sm:text-sm"
{...register("username")}
required
/>
<EmailField
{...register("email")}
className="mt-1 block w-full rounded-md border border-gray-300 bg-gray-100 px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
}}
{...register("password")}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
<PasswordField
label={t("confirm_password")}
labelProps={{
className: "block text-sm font-medium text-gray-700",
}}
{...register("passwordcheck", {
validate: (value) =>
value === methods.watch("password") || (t("error_password_mismatch") as string),
})}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
</div>
<div className="flex space-x-2 rtl:space-x-reverse">
<Button loading={isSubmitting} className="w-7/12 justify-center">
{t("create_account")}
</Button>
<Button
color="secondary"
className="w-5/12 justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: (`${WEBAPP_URL}/${router.query.callbackUrl}` || "") as string,
})
}>
{t("login_instead")}
</Button>
</div>
</form>
</FormProvider>
</div>
</div>
</div>
);
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const ssr = await ssrInit(ctx);
const token = asStringOrNull(ctx.query.token);
if (token) {
const verificationToken = await prisma.verificationToken.findUnique({
where: {
token,
},
});
if (verificationToken) {
const existingUser = await prisma.user.findFirst({
where: {
AND: [
{
email: verificationToken?.identifier,
},
{
emailVerified: {
not: null,
},
},
],
},
});
if (existingUser) {
return {
redirect: {
permanent: false,
destination: "/auth/login?callbackUrl=" + `${WEBAPP_URL}/${ctx.query.callbackUrl}`,
},
};
}
}
}
return {
props: {
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
trpcState: ssr.dehydrate(),
},
};
};

View File

@ -13,7 +13,7 @@
"build": "next build",
"lint": "next lint",
"lint-fix": "next lint --fix && prettier --write .",
"test": "jest --detectOpenHandles",
"test": "jest --detectOpenHandles --passWithNoTests",
"type-check": "tsc --pretty --noEmit",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},

View File

@ -64,11 +64,11 @@ export async function bookingReferenceById(
* 404:
* description: BookingReference was not found
*/
console.log(safeQuery.data.id);
// console.log(safeQuery.data.id);
const bookingReference = await prisma.bookingReference.findFirst().catch((error: Error) => {
console.log("hoerr:", error);
});
console.log(bookingReference);
// console.log(bookingReference);
if (!bookingReference) res.status(404).json({ message: "Booking reference not found" });
else res.status(200).json({ booking_reference: bookingReference });
@ -92,6 +92,23 @@ export async function bookingReferenceById(
* patch:
* operationId: editBookingReferenceById
* summary: Edit an existing booking reference
* requestBody:
* description: Edit an existing booking reference related to one of your bookings
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* days:
* type: array
* example: email@example.com
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* parameters:
* - in: path
* name: id
@ -112,7 +129,8 @@ export async function bookingReferenceById(
const safeBody = schemaBookingEditBodyParams.safeParse(body);
if (!safeBody.success) {
res.status(401).json({ message: "Invalid request body", error: safeBody.error });
console.log(safeBody.error);
res.status(400).json({ message: "Invalid request body", error: safeBody.error });
return;
// throw new Error("Invalid request body");
}

View File

@ -30,6 +30,7 @@ export async function bookingById(
* /bookings/{id}:
* get:
* summary: Find a booking
* operationId: getBookingById
* parameters:
* - in: path
* name: id
@ -64,6 +65,24 @@ export async function bookingById(
* /bookings/{id}:
* patch:
* summary: Edit an existing booking
* operationId: editBookingById
* requestBody:
* description: Edit an existing booking related to one of your event-types
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* example: 15min
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* parameters:
* - in: path
* name: id
@ -84,14 +103,17 @@ export async function bookingById(
case "PATCH":
const safeBody = schemaBookingEditBodyParams.safeParse(body);
if (!safeBody.success) {
throw new Error("Invalid request body");
console.log(safeBody.error);
res.status(400).json({ message: "Bad request", error: safeBody.error });
return;
// throw new Error("Invalid request body");
}
await prisma.booking
.update({
where: { id: safeQuery.data.id },
data: safeBody.data,
})
.then((data) => schemaBookingReadPublic.parse(data))
// .then((data) => schemaBookingReadPublic.parse(data))
.then((booking) => res.status(200).json({ booking }))
.catch((error: Error) =>
res.status(404).json({
@ -105,6 +127,7 @@ export async function bookingById(
* /bookings/{id}:
* delete:
* summary: Remove an existing booking
* operationId: removeBookingById
* parameters:
* - in: path
* name: id

View File

@ -16,6 +16,7 @@ async function createOrlistAllBookings(
* /bookings:
* get:
* summary: Find all bookings
* operationId: listBookings
* tags:
* - bookings
* responses:
@ -41,6 +42,24 @@ async function createOrlistAllBookings(
* /bookings:
* post:
* summary: Creates a new booking
* operationId: addBooking
* requestBody:
* description: Edit an existing booking related to one of your event-types
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* example: 15min
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* tags:
* - bookings
* responses:
@ -52,18 +71,25 @@ async function createOrlistAllBookings(
* description: Authorization information is missing or invalid.
*/
const safe = schemaBookingCreateBodyParams.safeParse(body);
if (!safe.success) throw new Error("Invalid request body");
if (!safe.success) {
console.log(safe.error);
res.status(400).json({ message: "Bad request. Booking body is invalid." });
return;
// throw new Error("Invalid request body");
}
const data = await prisma.booking.create({ data: { ...safe.data, userId } });
const booking = schemaBookingReadPublic.parse(data);
if (booking) res.status(201).json({ booking, message: "Booking created successfully" });
else
(error: Error) =>
(error: Error) => {
console.log(error);
res.status(400).json({
message: "Could not create new booking",
error,
});
};
} else res.status(405).json({ message: `Method ${method} not allowed` });
}

View File

@ -15,5 +15,5 @@
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules", "templates"]
"exclude": ["node_modules", "templates", "auth"]
}