From 0aa931287f3c406cb9d4b59ecd602455451f0b81 Mon Sep 17 00:00:00 2001 From: Matthias Nannt Date: Wed, 13 Jul 2022 18:04:02 +0200 Subject: [PATCH] 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 --- .env.example | 8 +- Dockerfile | 1 - README.md | 4 +- docker-compose.yml | 7 +- lib/email.ts | 48 +++++ lib/users.ts | 43 ++++ package.json | 13 +- pages/api/auth/[...nextauth].ts | 81 ++++++++ pages/api/forms/index.ts | 2 +- pages/api/public/users/index.tsx | 45 +++++ pages/api/public/users/verfication-email.tsx | 44 +++++ pages/auth/signin.tsx | 14 +- pages/auth/signup.tsx | 183 ++++++++++++++++++ pages/auth/verification-requested.tsx | 68 +++++++ pages/auth/verify.tsx | 27 +++ .../20220713132158_add_signup/migration.sql | 24 +++ prisma/schema.prisma | 20 +- prisma/seed.ts | 42 ---- yarn.lock | 129 +++++++++--- 19 files changed, 700 insertions(+), 103 deletions(-) create mode 100644 lib/email.ts create mode 100644 lib/users.ts create mode 100644 pages/api/public/users/index.tsx create mode 100644 pages/api/public/users/verfication-email.tsx create mode 100644 pages/auth/signup.tsx create mode 100644 pages/auth/verification-requested.tsx create mode 100644 pages/auth/verify.tsx create mode 100644 prisma/migrations/20220713132158_add_signup/migration.sql delete mode 100644 prisma/seed.ts diff --git a/.env.example b/.env.example index fa03747963..f7045d3f19 100644 --- a/.env.example +++ b/.env.example @@ -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' diff --git a/Dockerfile b/Dockerfile index b4d18046ae..6af7c3ffec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 847723e8b1..9d0cdb4b78 100644 --- a/README.md +++ b/README.md @@ -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. ``` diff --git a/docker-compose.yml b/docker-compose.yml index c83f902ad9..982bfaa55e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/lib/email.ts b/lib/email.ts new file mode 100644 index 0000000000..d4aae5946a --- /dev/null +++ b/lib/email.ts @@ -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!

To verify your email address and start using snoopForms please click this link:
+ ${verifyLink}
+
+ The link is valid for one day. If it has expired please request a new token here:
+ ${verificationRequestLink}
+
+ Your snoopForms Team`, + }); +}; diff --git a/lib/users.ts b/lib/users.ts new file mode 100644 index 0000000000..74cbf067e5 --- /dev/null +++ b/lib/users.ts @@ -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}`); + } +}; diff --git a/package.json b/package.json index c7a47eb7a5..8fe6ebd3af 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 9976a9ccd0..f75b6f22cf 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -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 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", diff --git a/pages/api/forms/index.ts b/pages/api/forms/index.ts index d49dec70ff..ae969dc076 100644 --- a/pages/api/forms/index.ts +++ b/pages/api/forms/index.ts @@ -23,7 +23,7 @@ export default async function handle( }, include: { owner: { - select: { name: true }, + select: { firstname: true }, }, _count: { select: { submissionSessions: true }, diff --git a/pages/api/public/users/index.tsx b/pages/api/public/users/index.tsx new file mode 100644 index 0000000000..c0ba0d3a5f --- /dev/null +++ b/pages/api/public/users/index.tsx @@ -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.` + ); + } +} diff --git a/pages/api/public/users/verfication-email.tsx b/pages/api/public/users/verfication-email.tsx new file mode 100644 index 0000000000..93270e4fb5 --- /dev/null +++ b/pages/api/public/users/verfication-email.tsx @@ -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.` + ); + } +} diff --git a/pages/auth/signin.tsx b/pages/auth/signin.tsx index fcb70a80bd..0b47369b59 100644 --- a/pages/auth/signin.tsx +++ b/pages/auth/signin.tsx @@ -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 -
- - Create an account - +
+ + + Create an account + +
diff --git a/pages/auth/signup.tsx b/pages/auth/signup.tsx new file mode 100644 index 0000000000..77124b0358 --- /dev/null +++ b/pages/auth/signup.tsx @@ -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(""); + + 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 ( +
+
+ {error && ( +
+
+
+
+
+

+ An error occurred when logging you in +

+
+

{error}

+
+
+
+
+ )} + +
+
+ snoopForms logo +
+ +
+

+ Create your own forms and collect submissions by creating a + snoopForms account. +

+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ Already have an account?{" "} + + Log in. + +
+
+
+
+
+
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const csrfToken = await getCsrfToken(context); + return { + props: { csrfToken }, + }; +}; diff --git a/pages/auth/verification-requested.tsx b/pages/auth/verification-requested.tsx new file mode 100644 index 0000000000..5283493621 --- /dev/null +++ b/pages/auth/verification-requested.tsx @@ -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 ( +
+
+
+
+ snoopForms logo +
+ +
+ {email ? ( + <> +

+ Please verify your email address +

+

+ We have sent you an email to the address{" "} + {router.query.email}. Please + click the link in the email to activate your account. +

+
+

+ You didn't receive an email or your link expired? +
+ Click the button below to request a new email. +

+ {" "} + + ) : ( +

No E-Mail Address provided

+ )} +
+
+
+
+ ); +} diff --git a/pages/auth/verify.tsx b/pages/auth/verify.tsx new file mode 100644 index 0000000000..a91da42f2d --- /dev/null +++ b/pages/auth/verify.tsx @@ -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 ( +
+
+
+

+ {!token ? "No Token provided" : "Verifying..."} +

+
+
+
+ ); +} diff --git a/prisma/migrations/20220713132158_add_signup/migration.sql b/prisma/migrations/20220713132158_add_signup/migration.sql new file mode 100644 index 0000000000..a0324a7cf5 --- /dev/null +++ b/prisma/migrations/20220713132158_add_signup/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9a9716c6f9..5d6aad9857 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") } \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts deleted file mode 100644 index 79d79b0490..0000000000 --- a/prisma/seed.ts +++ /dev/null @@ -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(); - }); diff --git a/yarn.lock b/yarn.lock index 84a50868ef..16470351a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"