Merge branch 'main' into feature/zoom-integration
commit
7ecb7f22e3
|
@ -10,7 +10,7 @@ export const UsernameInput = React.forwardRef( (props, ref) => (
|
|||
<span className="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm">
|
||||
{typeof window !== "undefined" && window.location.hostname}/
|
||||
</span>
|
||||
<input ref={ref} type="text" name="username" id="username" autoComplete="username" required defaultValue={props.defaultValue}
|
||||
<input ref={ref} type="text" name="username" id="username" autoComplete="username" required {...props}
|
||||
className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
|
||||
import nodemailer from 'nodemailer';
|
||||
import { serverConfig } from "../serverConfig";
|
||||
import { CalendarEvent } from "../calendarClient";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import {serverConfig} from "../serverConfig";
|
||||
import {CalendarEvent} from "../calendarClient";
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
|
@ -11,8 +10,8 @@ dayjs.extend(localizedFormat);
|
|||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export default function createConfirmBookedEmail(calEvent: CalendarEvent, uid: String, options: any = {}) {
|
||||
return sendEmail(calEvent, uid, {
|
||||
export default function createConfirmBookedEmail(calEvent: CalendarEvent, cancelLink: string, rescheduleLink: string, options: any = {}) {
|
||||
return sendEmail(calEvent, cancelLink, rescheduleLink, {
|
||||
provider: {
|
||||
transport: serverConfig.transport,
|
||||
from: serverConfig.from,
|
||||
|
@ -21,7 +20,7 @@ export default function createConfirmBookedEmail(calEvent: CalendarEvent, uid: S
|
|||
});
|
||||
}
|
||||
|
||||
const sendEmail = (calEvent: CalendarEvent, uid: String, {
|
||||
const sendEmail = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: string, {
|
||||
provider,
|
||||
}) => new Promise( (resolve, reject) => {
|
||||
|
||||
|
@ -34,8 +33,8 @@ const sendEmail = (calEvent: CalendarEvent, uid: String, {
|
|||
from: `${calEvent.organizer.name} <${from}>`,
|
||||
replyTo: calEvent.organizer.email,
|
||||
subject: `Confirmed: ${calEvent.type} with ${calEvent.organizer.name} on ${inviteeStart.format('dddd, LL')}`,
|
||||
html: html(calEvent, uid),
|
||||
text: text(calEvent, uid),
|
||||
html: html(calEvent, cancelLink, rescheduleLink),
|
||||
text: text(calEvent, cancelLink, rescheduleLink),
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
|
@ -47,10 +46,7 @@ const sendEmail = (calEvent: CalendarEvent, uid: String, {
|
|||
)
|
||||
});
|
||||
|
||||
const html = (calEvent: CalendarEvent, uid: String) => {
|
||||
const cancelLink = process.env.BASE_URL + '/cancel/' + uid;
|
||||
const rescheduleLink = process.env.BASE_URL + '/reschedule/' + uid;
|
||||
|
||||
const html = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: string) => {
|
||||
const inviteeStart: Dayjs = <Dayjs>dayjs(calEvent.startTime).tz(calEvent.attendees[0].timeZone);
|
||||
return `
|
||||
<div>
|
||||
|
@ -71,4 +67,4 @@ const html = (calEvent: CalendarEvent, uid: String) => {
|
|||
`;
|
||||
};
|
||||
|
||||
const text = (evt: CalendarEvent, uid: String) => html(evt, uid).replace('<br />', "\n").replace(/<[^>]+>/g, '');
|
||||
const text = (evt: CalendarEvent, cancelLink: string, rescheduleLink: string) => html(evt, cancelLink, rescheduleLink).replace('<br />', "\n").replace(/<[^>]+>/g, '');
|
|
@ -21,7 +21,9 @@ const sendEmail = (invitation: any, {
|
|||
{
|
||||
from: `Calendso <${from}>`,
|
||||
to: invitation.toEmail,
|
||||
subject: `${invitation.from} invited you to join ${invitation.teamName}`,
|
||||
subject: (
|
||||
invitation.from ? invitation.from + ' invited you' : 'You have been invited'
|
||||
) + ` to join ${invitation.teamName}`,
|
||||
html: html(invitation),
|
||||
text: text(invitation),
|
||||
},
|
||||
|
@ -34,44 +36,51 @@ const sendEmail = (invitation: any, {
|
|||
});
|
||||
});
|
||||
|
||||
const html = (invitation: any) => `
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<center>
|
||||
<table style="width: 640px; border: 1px solid gray; padding: 15px; margin: 0 auto; text-align: left;">
|
||||
<tr>
|
||||
const html = (invitation: any) => {
|
||||
let url: string = process.env.BASE_URL + "/settings/teams";
|
||||
if (invitation.token) {
|
||||
url = `${process.env.BASE_URL}/auth/signup?token=${invitation.token}&callbackUrl=${url}`;
|
||||
}
|
||||
|
||||
return `
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
Hi,<br />
|
||||
<br />
|
||||
${invitation.from} invited you to join the team "${invitation.teamName}" in Calendso.<br />
|
||||
<br />
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<center>
|
||||
<table style="width: 640px; border: 1px solid gray; padding: 15px; margin: 0 auto; text-align: left;">
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${process.env.BASE_URL}/settings/teams" style="height:40px;v-text-anchor:middle;width:130px;" arcsize="5%" strokecolor="#19cca3" fillcolor="#19cca3;width: 130;">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Helvetica, sans-serif;font-size:18px; font-weight: 600;">Join team</center>
|
||||
</v:roundrect>
|
||||
|
||||
<![endif]-->
|
||||
<a href="${process.env.BASE_URL}/settings/teams" style="display: inline-block; mso-hide:all; background-color: #19cca3; color: #FFFFFF; border:1px solid #19cca3; border-radius: 6px; line-height: 220%; width: 200px; font-family: Helvetica, sans-serif; font-size:18px; font-weight:600; text-align: center; text-decoration: none; -webkit-text-size-adjust:none; " target="_blank">Join team</a>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
Hi,<br />
|
||||
<br />` +
|
||||
(invitation.from ? invitation.from + ' invited you' : 'You have been invited' )
|
||||
+ ` to join the team "${invitation.teamName}" in Calendso.<br />
|
||||
<br />
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${url}" style="height:40px;v-text-anchor:middle;width:130px;" arcsize="5%" strokecolor="#19cca3" fillcolor="#19cca3;width: 130;">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Helvetica, sans-serif;font-size:18px; font-weight: 600;">Join team</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<a href="${url}" style="display: inline-block; mso-hide:all; background-color: #19cca3; color: #FFFFFF; border:1px solid #19cca3; border-radius: 6px; line-height: 220%; width: 200px; font-family: Helvetica, sans-serif; font-size:18px; font-weight:600; text-align: center; text-decoration: none; -webkit-text-size-adjust:none; " target="_blank">Join team</a>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table><br />
|
||||
If you prefer not to use "${invitation.toEmail}" as your Calendso email or already have a Calendso account, please request another invitation to that email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
</td>
|
||||
</tr>
|
||||
</table><br />
|
||||
If you prefer not to use "${invitation.toEmail}" as your Calendso email or already have a Calendso account, please request another invitation to that email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
// just strip all HTML and convert <br /> to \n
|
||||
const text = (evt: any) => html(evt).replace('<br />', "\n").replace(/<[^>]+>/g, '');
|
|
@ -2,55 +2,71 @@ import prisma from '../../../lib/prisma';
|
|||
import { hashPassword } from "../../../lib/auth";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return;
|
||||
}
|
||||
if (req.method !== 'POST') {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = req.body;
|
||||
const { username, email, password } = data;
|
||||
const data = req.body;
|
||||
const { username, email, password } = data;
|
||||
|
||||
if (!username) {
|
||||
res.status(422).json({message: 'Invalid username'});
|
||||
return;
|
||||
}
|
||||
if (!username) {
|
||||
res.status(422).json({message: 'Invalid username'});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
res.status(422).json({message: 'Invalid email'});
|
||||
return;
|
||||
}
|
||||
if (!email || !email.includes('@')) {
|
||||
res.status(422).json({message: 'Invalid email'});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.trim().length < 7) {
|
||||
res.status(422).json({message: 'Invalid input - password should be at least 7 characters long.'});
|
||||
return;
|
||||
}
|
||||
if (!password || password.trim().length < 7) {
|
||||
res.status(422).json({message: 'Invalid input - password should be at least 7 characters long.'});
|
||||
return;
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
username: username
|
||||
},
|
||||
{
|
||||
email: email
|
||||
}
|
||||
]
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
username: username
|
||||
},
|
||||
{
|
||||
email: email
|
||||
}
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
res.status(422).json({message: 'A user exists with that username or email address'});
|
||||
return;
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword
|
||||
],
|
||||
AND: [
|
||||
{
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
}
|
||||
});
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({message: 'Created user'});
|
||||
if (existingUser) {
|
||||
let message: string = (
|
||||
existingUser.email !== email
|
||||
) ? 'Username already taken' : 'Email address is already registered';
|
||||
|
||||
return res.status(409).json({message});
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email, },
|
||||
update: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(Date.now()),
|
||||
},
|
||||
create: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({message: 'Created user'});
|
||||
}
|
|
@ -56,7 +56,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
title: req.body.eventName + ' with ' + req.body.name,
|
||||
};
|
||||
|
||||
const hashUID = translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
|
||||
const hashUID: string = translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
|
||||
const cancelLink: string = process.env.BASE_URL + '/cancel/' + hashUID;
|
||||
const rescheduleLink:string = process.env.BASE_URL + '/reschedule/' + hashUID;
|
||||
const appendLinksToEvents = (event: CalendarEvent) => {
|
||||
const eventCopy = {...event};
|
||||
eventCopy.description += "\n\n"
|
||||
+ "Need to change this event?\n"
|
||||
+ "Cancel: " + cancelLink + "\n"
|
||||
+ "Reschedule:" + rescheduleLink;
|
||||
|
||||
return eventCopy;
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
|
@ -92,7 +103,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
// Use all integrations
|
||||
results = await async.mapLimit(calendarCredentials, 5, async (credential) => {
|
||||
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||
return await updateEvent(credential, bookingRefUid, evt)
|
||||
return await updateEvent(credential, bookingRefUid, appendLinksToEvents(evt))
|
||||
});
|
||||
|
||||
//TODO: Reschedule with videoCredentials as well
|
||||
|
@ -125,7 +136,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
} else {
|
||||
// Schedule event
|
||||
results.concat(await async.mapLimit(calendarCredentials, 5, async (credential) => {
|
||||
const response = await createEvent(credential, evt);
|
||||
const response = await createEvent(credential, appendLinksToEvents(evt));
|
||||
return {
|
||||
type: credential.type,
|
||||
response
|
||||
|
@ -171,7 +182,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
// If one of the integrations allows email confirmations or no integrations are added, send it.
|
||||
if (currentUser.credentials.length === 0 || !results.every((result) => result.disableConfirmationEmail)) {
|
||||
await createConfirmBookedEmail(
|
||||
evt, hashUID
|
||||
evt, cancelLink, rescheduleLink
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import prisma from '../../../../lib/prisma';
|
||||
import createInvitationEmail from "../../../../lib/emails/invitation";
|
||||
import {getSession} from "next-auth/client";
|
||||
import {randomBytes} from "crypto";
|
||||
import {create} from "domain";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
|
@ -34,8 +36,48 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
});
|
||||
|
||||
if (!invitee) {
|
||||
return res.status(400).json({
|
||||
message: `Invite failed because there is no corresponding user for ${req.body.usernameOrEmail}`});
|
||||
// liberal email match
|
||||
const isEmail = (str: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
|
||||
|
||||
if (!isEmail(req.body.usernameOrEmail)) {
|
||||
return res.status(400).json({
|
||||
message: `Invite failed because there is no corresponding user for ${req.body.usernameOrEmail}`
|
||||
});
|
||||
}
|
||||
// valid email given, create User
|
||||
const createUser = await prisma.user.create(
|
||||
{
|
||||
data: {
|
||||
email: req.body.usernameOrEmail,
|
||||
}
|
||||
})
|
||||
.then( (invitee) => prisma.membership.create(
|
||||
{
|
||||
data: {
|
||||
teamId: parseInt(req.query.team),
|
||||
userId: invitee.id,
|
||||
role: req.body.role,
|
||||
},
|
||||
}));
|
||||
|
||||
const token: string = randomBytes(32).toString("hex");
|
||||
|
||||
const createVerificationRequest = await prisma.verificationRequest.create({
|
||||
data: {
|
||||
identifier: req.body.usernameOrEmail,
|
||||
token,
|
||||
expires: new Date((new Date()).setHours(168)) // +1 week
|
||||
}
|
||||
});
|
||||
|
||||
createInvitationEmail({
|
||||
toEmail: req.body.usernameOrEmail,
|
||||
from: session.user.name,
|
||||
teamName: team.name,
|
||||
token,
|
||||
});
|
||||
|
||||
return res.status(201).json({});
|
||||
}
|
||||
|
||||
// create provisional membership
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
import Head from 'next/head';
|
||||
import {useRouter} from "next/router";
|
||||
import {signIn} from 'next-auth/client'
|
||||
import ErrorAlert from "../../components/ui/alerts/Error";
|
||||
import {useState} from "react";
|
||||
import {UsernameInput} from "../../components/ui/UsernameInput";
|
||||
import prisma from "../../lib/prisma";
|
||||
|
||||
export default function Signup(props) {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [ hasErrors, setHasErrors ] = useState(false);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const handleErrors = async (resp) => {
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const signUp = (e) => {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (e.target.password.value !== e.target.passwordcheck.value) {
|
||||
throw new Error("Password mismatch");
|
||||
}
|
||||
|
||||
const email: string = e.target.email.value;
|
||||
const password: string = e.target.password.value;
|
||||
|
||||
fetch('/api/auth/signup',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
username: e.target.username.value,
|
||||
password,
|
||||
email,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
.then(handleErrors)
|
||||
.then(
|
||||
() => signIn('Calendso', { callbackUrl: (router.query.callbackUrl || '') as string })
|
||||
)
|
||||
.catch( (err) => {
|
||||
setHasErrors(true);
|
||||
setErrorMessage(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<Head>
|
||||
<title>Sign up</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h2 className="text-center text-3xl font-extrabold text-gray-900">
|
||||
Create your account
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 shadow mx-2 sm:rounded-lg sm:px-10">
|
||||
<form method="POST" onSubmit={signUp} className="bg-white space-y-6">
|
||||
{hasErrors && <ErrorAlert message={errorMessage} />}
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<UsernameInput required />
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input type="email" name="email" id="email" placeholder="jdoe@example.com" disabled={!!props.email} readOnly={!!props.email} value={props.email} className="bg-gray-100 mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" />
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input type="password" name="password" id="password" required placeholder="•••••••••••••" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="passwordcheck" className="block text-sm font-medium text-gray-700">Confirm password</label>
|
||||
<input type="password" name="passwordcheck" id="passwordcheck" required placeholder="•••••••••••••" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-4 flex">
|
||||
<input type="submit" value="Create Account"
|
||||
className="btn btn-primary w-7/12 mr-2 inline-flex justify-center rounded-md border border-transparent cursor-pointer shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm" />
|
||||
<a onClick={() => signIn('Calendso', { callbackUrl: (router.query.callbackUrl || '') as string })}
|
||||
className="w-5/12 inline-flex justify-center text-sm text-gray-500 font-medium border px-4 py-2 rounded btn cursor-pointer">Login instead</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(ctx) {
|
||||
if (!ctx.query.token) {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
const verificationRequest = await prisma.verificationRequest.findUnique({
|
||||
where: {
|
||||
token: ctx.query.token,
|
||||
}
|
||||
});
|
||||
|
||||
// for now, disable if no verificationRequestToken given or token expired
|
||||
if ( ! verificationRequest || verificationRequest.expires < new Date() ) {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
email: verificationRequest.identifier
|
||||
},
|
||||
{
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return { redirect: { permanent: false, destination: '/auth/login?callbackUrl=' + ctx.query.callbackUrl } };
|
||||
}
|
||||
|
||||
return { props: { email: verificationRequest.identifier } };
|
||||
}
|
|
@ -36,6 +36,7 @@ model User {
|
|||
username String?
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
password String?
|
||||
bio String?
|
||||
avatar String?
|
||||
|
@ -73,6 +74,16 @@ model Membership {
|
|||
@@id([userId,teamId])
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
model BookingReference {
|
||||
id Int @default(autoincrement()) @id
|
||||
type String
|
||||
|
|
Loading…
Reference in New Issue