Feature/add signup (#5)

* update prisma model to support user

* add signup page

* add email verification

* remove seed user

* update README and Dockerfile to support signup and the removal of seed data
This commit is contained in:
Matthias Nannt
2022-07-13 18:04:02 +02:00
committed by GitHub
parent 4192461f5f
commit 0aa931287f
19 changed files with 700 additions and 103 deletions

View File

@@ -2,8 +2,12 @@
SECRET=RANDOM_STRING
DATABASE_URL='postgresql://user@localhost:5432/snoopforms?schema=public'
NEXTAUTH_URL=http://localhost:3000
ADMIN_EMAIL=user@example.com
ADMIN_PASSWORD='admin123'
MAIL_FROM=noreply@example.com
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=smtpUser
SMTP_PASSWORD=smtpPassword
# For Docker Setup use this Database URL:
# DATABASE_URL='postgresql://postgres:postgres@postgres:5432/snoopforms?schema=public'

View File

@@ -12,7 +12,6 @@ WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN yarn prisma generate
RUN yarn tsc prisma/seed.ts
RUN yarn build && yarn install --production --ignore-scripts --prefer-offline
# Production image, copy all the files and run next

View File

@@ -69,7 +69,7 @@ yarn install
cp .env.example .env
```
4. Use the code editor of your choice to edit the .env file.
4. Use the code editor of your choice to edit the .env file. You need to change all fields according to your setup. The SMTP-credentials are essential for verification emails to work during user signup.
5. Make sure your PostgreSQL Database Server is running. Then let prisma set up the database for you:
@@ -101,7 +101,7 @@ git clone https://github.com/snoopForms/snoopforms.git && cd snoopforms
```
Create a `.env` file based on `.env.example` and change it according to your setup.
Create a `.env` file based on `.env.example` and change all fields according to your setup. The SMTP-credentials are essential for verification emails to work during user signup.
```

View File

@@ -9,12 +9,7 @@ services:
snoopforms:
build: .
command:
[
sh,
-c,
"yarn prisma migrate deploy && yarn prisma db seed && yarn start",
]
command: [sh, -c, "yarn prisma migrate deploy && yarn start"]
depends_on:
- postgres
ports:

48
lib/email.ts Normal file
View File

@@ -0,0 +1,48 @@
import jwt from "jsonwebtoken";
const nodemailer = require("nodemailer");
interface sendEmailData {
to: string;
subject: string;
text?: string;
html: string;
}
export const sendEmail = async (emailData: sendEmailData) => {
let transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_SECURE_ENABLED || false, // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
const emailDefaults = {
from: process.env.MAIL_FROM || "noreply@snoopforms.com",
};
await transporter.sendMail({ ...emailDefaults, ...emailData });
};
export const sendVerificationEmail = async (user) => {
const token = jwt.sign({ id: user.id }, process.env.SECRET + user.email, {
expiresIn: "1d",
});
const verifyLink = `${
process.env.NEXTAUTH_URL
}/auth/verify?token=${encodeURIComponent(token)}`;
const verificationRequestLink = `${
process.env.NEXTAUTH_URL
}/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
await sendEmail({
to: user.email,
subject: "Welcome to snoopForms",
html: `Welcome to snoopForms!<br/><br/>To verify your email address and start using snoopForms please click this link:<br/>
<a href="${verifyLink}">${verifyLink}</a><br/>
<br/>
The link is valid for one day. If it has expired please request a new token here:<br/>
<a href="${verificationRequestLink}">${verificationRequestLink}</a><br/>
<br/>
Your snoopForms Team`,
});
};

43
lib/users.ts Normal file
View File

@@ -0,0 +1,43 @@
import { hashPassword } from "./auth";
export const createUser = async (firstname, lastname, email, password) => {
const hashedPassword = await hashPassword(password);
try {
const res = await fetch(`/api/public/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
firstname,
lastname,
email,
password: hashedPassword,
}),
});
if (res.status !== 200) {
const json = await res.json();
throw Error(json.error);
}
return await res.json();
} catch (error) {
throw Error(`${error.message}`);
}
};
export const resendVerificationEmail = async (email) => {
try {
const res = await fetch(`/api/public/users/verfication-email`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
}),
});
if (res.status !== 200) {
const json = await res.json();
throw Error(json.error);
}
return await res.json();
} catch (error) {
throw Error(`${error.message}`);
}
};

View File

@@ -6,8 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"seed:prod": "node prisma/seed.js"
"lint": "next lint"
},
"dependencies": {
"@editorjs/editorjs": "^2.24.3",
@@ -15,7 +14,7 @@
"@editorjs/paragraph": "^2.8.0",
"@headlessui/react": "^1.6.1",
"@heroicons/react": "^1.0.6",
"@prisma/client": "^3.15.1",
"@prisma/client": "^4.0.0",
"@snoopforms/react": "^0.0.3",
"babel-plugin-superjson-next": "^0.4.3",
"bcryptjs": "^2.4.3",
@@ -23,10 +22,11 @@
"editorjs-drag-drop": "^1.1.2",
"editorjs-undo": "^2.0.3",
"json2csv": "^5.0.7",
"jsonwebtoken": "^8.5.1",
"next": "12.1.6",
"next-auth": "^4.3.4",
"nextjs-cors": "^2.1.1",
"nodemailer": "^6.7.5",
"nodemailer": "^6.7.7",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-icons": "^4.4.0",
@@ -45,12 +45,9 @@
"eslint": "8.15.0",
"eslint-config-next": "12.1.6",
"postcss": "^8.4.13",
"prisma": "^3.15.1",
"prisma": "^4.0.0",
"tailwindcss": "^3.0.24",
"ts-node": "^10.7.0",
"typescript": "4.6.4"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}

View File

@@ -1,4 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next";
import jwt from "jsonwebtoken";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { prisma } from "../../../lib/prisma";
@@ -8,6 +9,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
return await NextAuth(req, res, {
providers: [
CredentialsProvider({
id: "credentials",
// The name to display on the sign in form (e.g. "Sign in with...")
name: "Credentials",
// The credentials is used to generate a suitable form on the sign in page.
@@ -65,7 +67,86 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
};
},
}),
CredentialsProvider({
id: "token",
// The name to display on the sign in form (e.g. "Sign in with...")
name: "Token",
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
token: {
label: "Verification Token",
type: "string",
},
},
async authorize(credentials, _req) {
let user;
try {
const { id } = await jwt.decode(credentials?.token);
user = await prisma.user.findUnique({
where: {
id: id,
},
});
} catch (e) {
console.error(e);
throw Error("Internal server error. Please try again later");
}
if (!user) {
throw new Error("User not found");
}
if (user.emailVerified) {
throw new Error("Email already verified");
}
const isValid = await new Promise((resolve) => {
jwt.verify(
credentials?.token,
process.env.SECRET + user.email,
(err) => {
if (err) resolve(false);
if (!err) resolve(true);
}
);
});
if (!isValid) {
throw new Error("Token is not valid or expired");
}
user = await prisma.user.update({
where: {
id: user.id,
},
data: { emailVerified: new Date().toISOString() },
});
return {
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.firstname,
emailVerified: user.emailVerified,
};
},
}),
],
callbacks: {
async signIn({ user }) {
if (user.emailVerified) {
return true;
} else {
// Return false to display a default error message or you can return a URL to redirect to
return `/auth/verification-requested?email=${encodeURIComponent(
user.email
)}`;
}
},
},
secret: process.env.SECRET,
pages: {
signIn: "/auth/signin",

View File

@@ -23,7 +23,7 @@ export default async function handle(
},
include: {
owner: {
select: { name: true },
select: { firstname: true },
},
_count: {
select: { submissionSessions: true },

View File

@@ -0,0 +1,45 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../lib/prisma";
import { sendVerificationEmail } from "../../../../lib/email";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
// POST /api/public/users
// Creates a new user
// Required fields in body: email, password (hashed)
// Optional fields in body: firstname, lastname
if (req.method === "POST") {
const user = req.body;
// create user in database
try {
const userData = await prisma.user.create({
data: {
...user,
},
});
await sendVerificationEmail(userData);
res.json(userData);
} catch (e) {
if (e.code === "P2002") {
return res.status(409).json({
error: "user with this email address already exists",
errorCode: e.code,
});
} else {
return res.status(500).json({
error: e.message,
errorCode: e.code,
});
}
}
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
}
}

View File

@@ -0,0 +1,44 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../lib/prisma";
import { sendVerificationEmail } from "../../../../lib/email";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
// POST /api/public/users
// Sends a new verification email to a user with a specific email address
// Required fields in body: email
if (req.method === "POST") {
const { email } = req.body;
// create user in database
try {
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
return res.status(404).json({
error: "No user with this email address found",
});
}
if (user.emailVerified) {
return res.status(400).json({
error: "Email address has already been verified",
});
}
await sendVerificationEmail(user);
res.json(user);
} catch (e) {
return res.status(500).json({
error: e.message,
});
}
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
}
}

View File

@@ -3,6 +3,7 @@ import { useRouter } from "next/router";
import { XCircleIcon } from "@heroicons/react/solid";
import { GetServerSideProps } from "next";
import Image from "next/image";
import Link from "next/link";
interface props {
csrfToken: string;
@@ -101,10 +102,15 @@ export default function SignIn({ csrfToken }: props) {
>
Sign in
</button>
<div className="text-center">
<a href="" className="text-xs text-red hover:text-red-600">
Create an account
</a>
<div className="mt-3 text-center">
<Link href="/auth/signup">
<a
href=""
className="text-xs text-red hover:text-red-600"
>
Create an account
</a>
</Link>
</div>
</div>
</form>

183
pages/auth/signup.tsx Normal file
View File

@@ -0,0 +1,183 @@
import { getCsrfToken } from "next-auth/react";
import { XCircleIcon } from "@heroicons/react/solid";
import { GetServerSideProps } from "next";
import Image from "next/image";
import Link from "next/link";
import { createUser } from "../../lib/users";
import { useState } from "react";
import { useRouter } from "next/router";
interface props {
csrfToken: string;
}
export default function SignIn({ csrfToken }: props) {
const router = useRouter();
const [error, setError] = useState<string>("");
const handleSubmit = async (e) => {
e.preventDefault();
try {
await createUser(
e.target.elements.firstname.value,
e.target.elements.lastname.value,
e.target.elements.email.value,
e.target.elements.password.value
);
router.push(
`/auth/verification-requested?email=${encodeURIComponent(
e.target.elements.email.value
)}`
);
} catch (e) {
setError(e.message);
}
};
return (
<div className="flex min-h-screen bg-ui-gray-light">
<div className="flex flex-col justify-center flex-1 px-4 py-12 mx-auto sm:px-6 lg:flex-none lg:px-20 xl:px-24">
{error && (
<div className="absolute p-4 rounded-md top-10 bg-red-50">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
className="w-5 h-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
An error occurred when logging you in
</h3>
<div className="mt-2 text-sm text-red-700">
<p className="space-y-1 whitespace-pre-wrap">{error}</p>
</div>
</div>
</div>
</div>
)}
<div className="w-full max-w-sm p-8 mx-auto bg-white rounded-xl shadow-cont lg:w-96">
<div>
<Image
src="/img/snoopforms-logo.svg"
alt="snoopForms logo"
width={500}
height={89}
/>
</div>
<div className="mt-8">
<p className="text-sm text-center text-gray-600">
Create your own forms and collect submissions by creating a
snoopForms account.
</p>
<div className="mt-6">
<form onSubmit={handleSubmit} className="space-y-6">
<input
name="csrfToken"
type="hidden"
defaultValue={csrfToken}
/>
<div>
<label
htmlFor="firstname"
className="block text-sm font-medium text-ui-gray-dark"
>
First name
</label>
<div className="mt-1">
<input
id="firstname"
name="firstname"
type="text"
autoComplete="given-name"
required
className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-ui-gray-medium border-ui-gray-medium focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
/>
</div>
</div>
<div>
<label
htmlFor="lastname"
className="block text-sm font-medium text-ui-gray-dark"
>
Last name
</label>
<div className="mt-1">
<input
id="lastname"
name="lastname"
type="text"
autoComplete="family-name"
required
className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-ui-gray-medium border-ui-gray-medium focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
/>
</div>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-ui-gray-dark"
>
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-ui-gray-medium border-ui-gray-medium focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
/>
</div>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-ui-gray-dark"
>
Password
</label>
<div className="mt-1">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="block w-full px-3 py-2 border rounded-md shadow-sm appearance-none placeholder-ui-gray-medium border-ui-gray-medium focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
/>
</div>
</div>
<div>
<button
type="submit"
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-red hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Sign up
</button>
<div className="mt-3 text-xs text-center text-gray-600">
Already have an account?{" "}
<Link href="/auth/signin">
<a className="text-red hover:text-red-600">Log in.</a>
</Link>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const csrfToken = await getCsrfToken(context);
return {
props: { csrfToken },
};
};

View File

@@ -0,0 +1,68 @@
import Image from "next/image";
import { useRouter } from "next/router";
import { toast } from "react-toastify";
import { resendVerificationEmail } from "../../lib/users";
interface props {
csrfToken: string;
}
export default function SignIn({}: props) {
const router = useRouter();
const email = router.query.email;
const requestVerificationEmail = async () => {
try {
await resendVerificationEmail(email);
toast("Verification email successfully sent. Please check your inbox.");
} catch (e) {
toast.error(`Error: ${e.message}`);
}
};
return (
<div className="flex min-h-screen bg-ui-gray-light">
<div className="flex flex-col justify-center flex-1 px-4 py-12 mx-auto sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="w-full max-w-sm p-8 mx-auto bg-white rounded-xl shadow-cont lg:w-96">
<div>
<Image
src="/img/snoopforms-logo.svg"
alt="snoopForms logo"
width={500}
height={89}
/>
</div>
<div className="mt-8">
{email ? (
<>
<h1 className="mb-4 font-bold text-center leading-2">
Please verify your email address
</h1>
<p className="text-center">
We have sent you an email to the address{" "}
<span className="italic">{router.query.email}</span>. Please
click the link in the email to activate your account.
</p>
<hr className="my-4" />
<p className="text-xs text-center">
You didn&apos;t receive an email or your link expired?
<br />
Click the button below to request a new email.
</p>
<button
type="button"
onClick={() => requestVerificationEmail()}
className="flex justify-center w-full px-4 py-2 mt-5 text-sm font-medium text-gray-600 bg-white border border-gray-400 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Request a new verification mail
</button>{" "}
</>
) : (
<p className="text-center">No E-Mail Address provided</p>
)}
</div>
</div>
</div>
</div>
);
}

27
pages/auth/verify.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function Verify() {
const router = useRouter();
const token = router.query.token?.toString();
useEffect(() => {
if (token) {
signIn("token", {
token,
callbackUrl: `/forms`,
});
}
}, [token]);
return (
<div className="flex min-h-screen bg-ui-gray-light">
<div className="flex flex-col justify-center flex-1 px-4 py-12 mx-auto sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="w-full max-w-sm p-8 mx-auto bg-white rounded-xl shadow-cont lg:w-96">
<p className="text-center">
{!token ? "No Token provided" : "Verifying..."}
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
/*
Warnings:
- You are about to drop the column `name` on the `users` table. All the data in the column will be lost.
- You are about to drop the `verification_requests` table. If the table is not empty, all the data it contains will be lost.
- Made the column `email` on table `users` required. This step will fail if there are existing NULL values in that column.
- Made the column `password` on table `users` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Form" DROP CONSTRAINT "Form_ownerId_fkey";
-- AlterTable
ALTER TABLE "users" DROP COLUMN "name",
ADD COLUMN "firstname" TEXT,
ADD COLUMN "lastname" TEXT,
ALTER COLUMN "email" SET NOT NULL,
ALTER COLUMN "password" SET NOT NULL;
-- DropTable
DROP TABLE "verification_requests";
-- AddForeignKey
ALTER TABLE "Form" ADD CONSTRAINT "Form_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -20,7 +20,7 @@ model Form {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id])
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId Int
formType FormType @default(NOCODE)
name String @default("")
@@ -73,24 +73,14 @@ model SessionEvent {
model User {
id Int @id @default(autoincrement())
name String?
email String? @unique
firstname String?
lastname String?
email String @unique
emailVerified DateTime? @map(name: "email_verified")
password String?
password String
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
forms Form[]
@@map(name: "users")
}
model VerificationRequest {
id Int @id @default(autoincrement())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "verification_requests")
}

View File

@@ -1,42 +0,0 @@
import { PrismaClient, Prisma } from "@prisma/client";
import { hash } from "bcryptjs";
const prisma = new PrismaClient();
async function main() {
console.log(`Start seeding ...`);
if (process.env.ADMIN_PASSWORD) {
const passwordHash = await hash(process.env.ADMIN_PASSWORD, 12);
if (typeof passwordHash === "string") {
const users: Prisma.UserCreateInput[] = [
{
name: "Admin",
email: process.env.ADMIN_EMAIL,
password: passwordHash,
},
];
for (const user of users) {
const userRes = await prisma.user.upsert({
where: {
email: user.email,
},
update: {},
create: user,
});
console.log(`Created user with id: ${userRes.id}`);
}
console.log(`Seeding finished.`);
}
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

129
yarn.lock
View File

@@ -230,22 +230,22 @@
resolved "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.2.tgz"
integrity sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==
"@prisma/client@^3.15.1":
version "3.15.1"
resolved "https://registry.npmjs.org/@prisma/client/-/client-3.15.1.tgz"
integrity sha512-Lsk7oupvO9g99mrIs07iE6BIMouHs46Yq/YY8itTsUQNKfecsPuZvVYvcKci0pqRQ0neOpvIvoA/ouZmIMBCrQ==
"@prisma/client@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.0.0.tgz#ed2f46930a1da0d8ae88d7965485973576b04270"
integrity sha512-g1h2OGoRo7anBVQ9Cw3gsbjwPtvf7i0pkGxKeZICtwkvE5CZXW+xZF4FZdmrViYkKaAShbISL0teNpu9ecpf4g==
dependencies:
"@prisma/engines-version" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
"@prisma/engines-version" "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11"
"@prisma/engines-version@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz"
integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==
"@prisma/engines-version@3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11":
version "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11.tgz#4b5efe5eee2feef12910e4627a572cd96ed83236"
integrity sha512-PiZhdD624SrYEjyLboI0X7OugNbxUzDJx9v/6ldTKuqNDVUCmRH/Z00XwDi/dgM4FlqOSO+YiUsSiSKjxxG8cw==
"@prisma/engines@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
resolved "https://registry.npmjs.org/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz"
integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==
"@prisma/engines@3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11":
version "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11.tgz#82f0018153cffa05d61422f9c0c7b0479b180f75"
integrity sha512-u/rG4lDHALolWBLr3yebZ+N2qImp3SDMcu7bHNJuRDaYvYEXy/MqfNRNEgd9GoPsXL3gofYf0VzJf2AmCG3YVw==
"@rushstack/eslint-patch@^1.1.3":
version "1.1.3"
@@ -574,6 +574,11 @@ browserslist@^4.20.3:
node-releases "^2.0.5"
picocolors "^1.0.0"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
@@ -800,6 +805,13 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
editorjs-drag-drop@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/editorjs-drag-drop/-/editorjs-drag-drop-1.1.2.tgz"
@@ -1508,6 +1520,22 @@ jsonparse@^1.3.1:
resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1:
version "3.3.0"
resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz"
@@ -1516,6 +1544,23 @@ jsonparse@^1.3.1:
array-includes "^3.1.4"
object.assign "^4.1.2"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz"
@@ -1559,16 +1604,46 @@ lodash.get@^4.4.2:
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz"
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
@@ -1693,10 +1768,10 @@ node-releases@^2.0.5:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz"
integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==
nodemailer@^6.7.5:
version "6.7.5"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.5.tgz"
integrity sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg==
nodemailer@^6.7.7:
version "6.7.7"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.7.tgz#e522fbd7507b81c51446d3f79c4603bf00083ddd"
integrity sha512-pOLC/s+2I1EXuSqO5Wa34i3kXZG3gugDssH+ZNCevHad65tc8vQlCQpOLaUjopvkRQKm2Cki2aME7fEOPRy3bA==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
@@ -1967,12 +2042,12 @@ pretty-format@^3.8.0:
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz"
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
prisma@^3.15.1:
version "3.15.1"
resolved "https://registry.npmjs.org/prisma/-/prisma-3.15.1.tgz"
integrity sha512-MLO3JUGJpe5+EVisA/i47+zlyF8Ug0ivvGYG4B9oSXQcPiUHB1ccmnpxqR7o0Up5SQgmxkBiEU//HgR6UuIKOw==
prisma@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.0.0.tgz#4ddb8fcd4f64d33aff8c198a6986dcce66dc8152"
integrity sha512-Dtsar03XpCBkcEb2ooGWO/WcgblDTLzGhPcustbehwlFXuTMliMDRzXsfygsgYwQoZnAUKRd1rhpvBNEUziOVw==
dependencies:
"@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
"@prisma/engines" "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11"
prop-types@^15.8.1:
version "15.8.1"
@@ -2111,6 +2186,11 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
safe-buffer@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz"
@@ -2119,6 +2199,11 @@ scheduler@^0.20.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"