diff --git a/.gitignore b/.gitignore index 8c7f27b..31f5294 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,5 @@ fabric.properties .idea/caches/build_file_checksums.ser /prisma/_____migrations___/ /resources/images/ +/crib.md +/**/**/*.log diff --git a/actions/logger.ts b/actions/logger.ts new file mode 100644 index 0000000..f143027 --- /dev/null +++ b/actions/logger.ts @@ -0,0 +1,45 @@ +import pino from 'pino' + +const pinoConfigProd: pino.LoggerOptions = { + transport: { + targets: [ + { + target: 'pino/file', options: { + destination: './production.log', mkdir: true, minLength: 4096, sync: false, + }, + }, + ], + }, + level: 'error', + redact: { + paths: ['password', '*.password'], remove: true, + }, +} + +const pinoConfigDev: pino.LoggerOptions = { + redact: { + paths: ['password', '*.password'], remove: false, + }, + // formatters: { + // bindings: (bindings) => { + // return { pid: bindings.pid, host: bindings.hostname, node_version: process.version } + // }, + // }, + transport: { + targets: [ + { + //target: 'pino/file', + target: 'pino-pretty', options: { destination: `./pretty.log`, mkdir: true, colorize: false }, level: 'error', + }, { + target: 'pino-pretty', level: 'trace', + }], + }, +} + +const journal = process.env.NODE_ENV === 'production' + ? pino(pinoConfigProd) + : pino(pinoConfigDev) + +export default journal + +// TODO: wait for newer version of https://betterstack.com/docs/logs/javascript/pino/ \ No newline at end of file diff --git a/actions/login.ts b/actions/login.ts index d3795ac..903dcd4 100644 --- a/actions/login.ts +++ b/actions/login.ts @@ -6,7 +6,15 @@ import { signIn } from '@/config/auth' import { DEFAULT_LOGIN_REDIRECT } from '@/config/routes' import { AuthError } from 'next-auth' import { getUserByEmail } from '@/data/user' -import { sendVerificationEmail } from '@/actions/send-verification-email' +import { sendTwoFactorTokenEmail, sendVerificationEmail } from '@/actions/send-verification-email' +import { generateTwoFactorToken } from '@/lib/tokens' +import { deleteTwoFactorToken, getTwoFactorTokenByEmail } from '@/data/two-factor-token' +import { + createTwoFactoComfirmation, + deleteTwoFactoComfirmation, + getTwoFactorConfirmationByUserId, +} from '@/data/two-factor-confirmation' +import journal from '@/actions/logger' export const login = async (values: zInfer) => { const validatedFields = LoginSchema.safeParse(values) @@ -15,7 +23,7 @@ export const login = async (values: zInfer) => { return { error: 'auth.form.error.invalid_fields' } } - const { email, password } = validatedFields.data + const { email, password, code } = validatedFields.data const existingUser = await getUserByEmail(email) @@ -27,6 +35,40 @@ export const login = async (values: zInfer) => { return await sendVerificationEmail(existingUser.email, existingUser.name) } + if (existingUser.isTwoFactorEnabled && existingUser.email) { + if (code) { + const twoFactorToken = await getTwoFactorTokenByEmail(existingUser.email) + if (!twoFactorToken || twoFactorToken.token !== code) { + return { error: 'auth.form.error.invalid_code' } + } + + const hasExpired = new Date(twoFactorToken.expires) < new Date() + if (hasExpired) { + return { error: 'auth.form.error.expired_token' } + } + + await deleteTwoFactorToken(twoFactorToken.id) + + const existingConfirmation = await getTwoFactorConfirmationByUserId(existingUser.id) + + if (existingConfirmation) { + await deleteTwoFactoComfirmation(existingConfirmation.id) + } + + await createTwoFactoComfirmation(existingUser.id) + + } else { + const twoFactorToken = await generateTwoFactorToken(existingUser.email) + + if (twoFactorToken) { + const isOk = await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token, existingUser.name) + return { twoFactor: isOk } + } + console.error('ERROR.TYPE: could not send token') + return { error: 'common.something_went_wrong' } + } + } + try { await signIn('credentials', { email, password, redirectTo: DEFAULT_LOGIN_REDIRECT, diff --git a/actions/send-verification-email.ts b/actions/send-verification-email.ts index aad876f..f8845c7 100644 --- a/actions/send-verification-email.ts +++ b/actions/send-verification-email.ts @@ -2,11 +2,32 @@ import mailer from '@/lib/mailer' import { AUTH_NEW_PASSWORD_URL, AUTH_USER_VERIFICATION_URL } from '@/config/routes' -import { generatePasswordResetToken, generateVerificationToken } from '@/lib/tokens' +import { generatePasswordResetToken, generateTwoFactorToken, generateVerificationToken } from '@/lib/tokens' import { env } from '@/lib/utils' import { __ct } from '@/lib/translate' import { body } from '@/templates/email/send-verification-email' +export const sendTwoFactorTokenEmail = async (email: string, token: string, name?: string | null) => { + const { isOk, code, info, error } = await mailer({ + to: name ? { name: name?.toString(), address: email } : email, + subject: await __ct({ + key: 'mailer.subject.send_2FA_code', + params: { site_name: env('SITE_NAME') }, + }), + text: `Your 2FA code: ${token}`, + html: `

Your 2FA code: ${token}

`, + }) + + return isOk + // TODO: Log this action + // if (isOk && code === 250) { + // //return //'auth.email.success._2FA_email_sent' + // return { success: code === 250 ? 'auth.email.success._2FA_email_sent' : info?.response } + // } else { + // return { error: env('DEBUG') === 'true' ? error?.response : 'auth.email.error._2FA_email_sending_error' } + // } +} + const sendVerificationEmail = async ( email: string, name?: string | null, diff --git a/actions/user-verification.ts b/actions/user-verification.ts index 7f047f1..32ea401 100644 --- a/actions/user-verification.ts +++ b/actions/user-verification.ts @@ -1,8 +1,7 @@ 'use server' -import db from '@/lib/db' -import { getVerificationTokenByToken } from '@/data/verification-token' -import { getUserByEmail } from '@/data/user' +import { deleteVerificationToken, getVerificationTokenByToken } from '@/data/verification-token' +import { getUserByEmail, updateUserEmailVerified } from '@/data/user' export const userVerification = async (token: string) => { const existingToken = await getVerificationTokenByToken(token) @@ -17,25 +16,9 @@ export const userVerification = async (token: string) => { if (!existingUser) return { error: 'Email associated with token not found!' } - try { - await db.user.update({ - where: { id: existingUser.id }, data: { - email: existingToken.email, emailVerified: new Date(), - }, - }) - } catch (e) { - console.error(e) - return { error: 'db.error.update.user_data' } - } + await updateUserEmailVerified(existingUser.id, existingToken.email) - try { - await db.verificationToken.delete({ - where: { id: existingToken.id }, - }) - } catch (e) { - // TODO: log error on disc or db - console.error(e) - } + await deleteVerificationToken(existingToken.id) return { success: 'User verified!' } } \ No newline at end of file diff --git a/app/[locale]/(protected)/cabinet/page.tsx b/app/[locale]/(protected)/cabinet/page.tsx index f2563dd..e4ef8af 100644 --- a/app/[locale]/(protected)/cabinet/page.tsx +++ b/app/[locale]/(protected)/cabinet/page.tsx @@ -2,6 +2,7 @@ import { auth, signOut } from '@/config/auth' const CabinetPage = async () => { const session = await auth() + return (
{JSON.stringify(session)} diff --git a/app/[locale]/(root)/(routes)/about/page.tsx b/app/[locale]/(root)/(routes)/about/page.tsx index be7d52c..5b72fd5 100644 --- a/app/[locale]/(root)/(routes)/about/page.tsx +++ b/app/[locale]/(root)/(routes)/about/page.tsx @@ -1,10 +1,3 @@ -import type { Metadata } from 'next' - -export const metadata: Metadata = { - title: 'key', - description: '...', -} - const AboutPage = () => { return <>ABOUT } diff --git a/app/[locale]/(root)/page.tsx b/app/[locale]/(root)/page.tsx index 1099127..0224344 100644 --- a/app/[locale]/(root)/page.tsx +++ b/app/[locale]/(root)/page.tsx @@ -5,7 +5,6 @@ import { Button } from '@/components/ui/button' import LoginButton from '@/components/auth/login-button' import Image from 'next/image' import wolf from '@/img/Gray wolf portrait.jpg' -import { Grid } from 'react-loader-spinner' const font = Poppins({ subsets: ['latin'], weight: ['600'], diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 68255ed..9de5407 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -1,10 +1,9 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' -import './globals.css' import { ReactElement } from 'react' import { I18nProviderClient } from '@/locales/client' import { lc } from '@/lib/utils' -import { Loading } from '@/components/loading' +import './globals.css' const inter = Inter({ subsets: ['cyrillic'] }) @@ -12,21 +11,17 @@ export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', } -type Props = { +type RootLayoutProps = { params: { locale: string }; children: ReactElement; } -export default function RootLayout ({ - params: { locale }, children, -}: Readonly) { +export default function RootLayout ({ params: { locale }, children }: Readonly) { return ( - {/*}>*/} - }> + {children} - {/**/} ) } diff --git a/components/auth/.PasswordInput.tsx.todo b/components/auth/.PasswordInput.tsx.todo deleted file mode 100644 index a860f8c..0000000 --- a/components/auth/.PasswordInput.tsx.todo +++ /dev/null @@ -1,65 +0,0 @@ - -'use client' -//https://gist.github.com/mjbalcueva/b21f39a8787e558d4c536bf68e267398 - -import { forwardRef, useState } from 'react' -import { EyeIcon, EyeOffIcon } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Input, InputProps } from '@/components/ui/input' -import { cn } from '@/lib/utils' -import { FormControl } from '@/components/ui/form' - -const PasswordInput = forwardRef( - ({ className, ...props }, ref) => { - const [showPassword, setShowPassword] = useState(false) - const disabled = props.value === '' || props.value === undefined || - props.disabled - - return (
- - - - {/* hides browsers password toggles */} - -
- ) - }, -) -PasswordInput.displayName = 'PasswordInput' - -export { PasswordInput } - diff --git a/components/auth/card-wrapper.tsx b/components/auth/card-wrapper.tsx index 58d1cfc..5e13294 100644 --- a/components/auth/card-wrapper.tsx +++ b/components/auth/card-wrapper.tsx @@ -1,10 +1,9 @@ 'use client' + import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card' import { Header } from '@/components/auth/header' import { Social } from '@/components/auth/social' import { BackButton } from '@/components/auth/back-button' -import { Suspense } from 'react' -import { Loading } from '@/components/loading' type Props = { children: React.ReactNode @@ -26,31 +25,29 @@ export const CardWrapper = ({ continueWithLabel, }: Props) => { return ( - }> - - -
- - - {children} - - {showSocial && -
-
- -
-
+ + +
+ + + {children} + + {showSocial && +
+
+ +
+
{continueWithLabel} -
- -
} - - - - - +
+ + } + + + + ) } \ No newline at end of file diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index 0857040..51b1296 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -5,14 +5,7 @@ import { useState, useTransition } from 'react' import { useForm } from 'react-hook-form' import { useSearchParams } from 'next/navigation' import { zodResolver } from '@hookform/resolvers/zod' -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form' +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { CardWrapper } from '@/components/auth/card-wrapper' import { useI18n } from '@/locales/client' @@ -28,10 +21,9 @@ export const LoginForm = () => { const t = useI18n() const searchParams = useSearchParams() - const urlError = searchParams.get('error') === 'OAuthAccountNotLinked' - ? t('auth.form.error.email_in_use') - : '' + const urlError = searchParams.get('error') === 'OAuthAccountNotLinked' ? t('auth.form.error.email_in_use') : '' + const [showTwoFactor, setShowTwoFactor] = useState(false) const [error, setError] = useState('') const [success, setSuccess] = useState('') const [isPending, startTransition] = useTransition() @@ -48,10 +40,26 @@ export const LoginForm = () => { startTransition(() => { login(values).then((data) => { - // @ts-ignore - setError(t(data?.error)) - // @ts-ignore - setSuccess(t(data?.success)) + //@ts-ignore + if (data?.error) { + form.reset() //@ts-ignore + setError(t(data?.error)) + } + + //@ts-ignore + if (data?.success) { + form.reset() //@ts-ignore + setSuccess(t(data?.success)) + } + + //@ts-ignore + if (data?.twoFactor) { //@ts-ignore + setShowTwoFactor(data?.twoFactor) + } + }).catch((err) => { + setError('auth.common.something_went_wrong') + //TODO: do logging + console.log(err) }) }) } @@ -67,48 +75,62 @@ export const LoginForm = () => {
- ( - {t('form.label.email')} - - - - - )}/> - {/*Password*/} - ( - {t('form.label.password')} - - - - - - )}/> + {showTwoFactor && ( + ( + {t('form.label.two_factor')} + + + + + )}/> + )} + {!showTwoFactor && (<> + ( + {t('form.label.email')} + + + + + )}/> + ( + {t('form.label.password')} + + + + + + )}/> + )}
- diff --git a/components/auth/new-password-form.tsx b/components/auth/new-password-form.tsx index 9f8b28f..7e8e0b6 100644 --- a/components/auth/new-password-form.tsx +++ b/components/auth/new-password-form.tsx @@ -58,7 +58,7 @@ export const NewPasswordForm = ({ token }: { token: string }) => {
{
- - - ) -} \ No newline at end of file diff --git a/components/locale-switcher.tsx b/components/locale-switcher.tsx index 1fb4ebb..ee48414 100644 --- a/components/locale-switcher.tsx +++ b/components/locale-switcher.tsx @@ -1,4 +1,5 @@ 'use client' + import { useChangeLocale, useCurrentLocale } from '@/locales/client' import { LC, type loc } from '@/config/locales' import { ChangeEvent } from 'react' @@ -7,18 +8,14 @@ import styles from '@/styles/locale-switcher.module.scss' export default function LocaleSwitcher () { const changeLocale = useChangeLocale() const locale = useCurrentLocale() - const selectHandler = (e: ChangeEvent) => changeLocale( - e.target.value as loc) + const selectHandler = (e: ChangeEvent) => changeLocale(e.target.value as loc) - return ( - //@ts-ignore - - ) + return (//@ts-ignore + ) } \ No newline at end of file diff --git a/config/auth.ts b/config/auth.ts index 890445a..b55ced2 100644 --- a/config/auth.ts +++ b/config/auth.ts @@ -7,6 +7,7 @@ import { getUserById } from '@/data/user' import { AUTH_ERROR_URL, AUTH_LOGIN_URL } from '@/config/routes' import { getCurrentLocale } from '@/locales/server' import { type loc } from '@/config/locales' +import { getTwoFactorConfirmationByUserId } from '@/data/two-factor-confirmation' declare module 'next-auth' { interface Session { @@ -44,7 +45,15 @@ export const { // Prevent sign in without email verification if (!existingUser?.emailVerified) return false - // TODO: Add 2FA check + if (existingUser.isTwoFactorEnabled) { + const twoFactorConfirmation = await getTwoFactorConfirmationByUserId(existingUser.id) + if (!twoFactorConfirmation) return false + + // Delete 2FA for the next sign in + await db.twoFactorComfirmation.delete({ + where: { id: twoFactorConfirmation.id }, + }) + } return true }, diff --git a/data/password-reset-token.ts b/data/password-reset-token.ts index 3575da6..b311c1d 100644 --- a/data/password-reset-token.ts +++ b/data/password-reset-token.ts @@ -1,9 +1,13 @@ +'use server' + import db from '@/lib/db' +import journal from '@/actions/logger' export const getPasswordResetTokenByToken = async (token: string) => { try { return await db.passwordResetToken.findUnique({ where: { token } }) - } catch { + } catch (err) { + journal.error({ getPasswordResetTokenByToken: err, token }) return null } } @@ -11,7 +15,8 @@ export const getPasswordResetTokenByToken = async (token: string) => { export const getPasswordResetTokenByEmail = async (email: string) => { try { return await db.passwordResetToken.findFirst({ where: { email } }) - } catch { + } catch (err) { + journal.error({ getPasswordResetTokenByEmail: err, email }) return null } } \ No newline at end of file diff --git a/data/two-factor-confirmation.ts b/data/two-factor-confirmation.ts new file mode 100644 index 0000000..6b361e7 --- /dev/null +++ b/data/two-factor-confirmation.ts @@ -0,0 +1,33 @@ +'use server' + +import db from '@/lib/db' +import journal from '@/actions/logger' + +export const createTwoFactoComfirmation = async (userId: string) => { + try { + return await db.twoFactorComfirmation.create({ data: { userId } }) + } catch (err) { + journal.error({ createTwoFactoComfirmation: err, userId }) + return null + } +} + +export const getTwoFactorConfirmationByUserId = async (userId: string) => { + try { + return await db.twoFactorComfirmation.findUnique({ + where: { userId }, + }) + } catch (err) { + journal.error({ getTwoFactorConfirmationByUserId: err, userId }) + return null + } +} + +export const deleteTwoFactoComfirmation = async (id: string) => { + try { + return await db.twoFactorComfirmation.delete({ where: { id } }) + } catch (err) { + journal.error({ deleteTwoFactoComfirmation: err, id }) + return null + } +} \ No newline at end of file diff --git a/data/two-factor-token.ts b/data/two-factor-token.ts new file mode 100644 index 0000000..be2474b --- /dev/null +++ b/data/two-factor-token.ts @@ -0,0 +1,33 @@ +'use server' + +import db from '@/lib/db' +import journal from '@/actions/logger' + +export const getTwoFactorTokenByToken = async (token: string) => { + try { + return await db.twoFactorToken.findUnique({ + where: { token }, + }) + } catch (err) { + journal.error({ getTwoFactorTokenByToken: err, token }) + return null + } +} + +export const getTwoFactorTokenByEmail = async (email: string) => { + try { + return await db.twoFactorToken.findFirst({ where: { email } }) + } catch (err) { + journal.error({ getTwoFactorTokenByEmail: err, email }) + return null + } +} + +export const deleteTwoFactorToken = async (id: string) => { + try { + return await db.twoFactorToken.delete({ where: { id } }) + } catch (err) { + journal.error({ deleteTwoFactorToken: err, id }) + return null + } +} \ No newline at end of file diff --git a/data/user.ts b/data/user.ts index 33ad80e..94904a1 100644 --- a/data/user.ts +++ b/data/user.ts @@ -1,10 +1,14 @@ +'use server' + import { User } from '@prisma/client' import db from '@/lib/db' +import journal from '@/actions/logger' export const getUserByEmail = async (email: string): Promise => { try { return await db.user.findUnique({ where: { email } }) - } catch { + } catch (err) { + journal.error({ getUserByEmail: err, email }) return null } } @@ -12,7 +16,22 @@ export const getUserByEmail = async (email: string): Promise => { export const getUserById = async (id: string): Promise => { try { return await db.user.findUnique({ where: { id } }) - } catch { + } catch (err) { + journal.error({ getUserById: err, id }) return null } +} + +export const updateUserEmailVerified = async (id: string, email: string) => { + try { + await db.user.update({ + where: { id }, + data: { + email, emailVerified: new Date(), + }, + }) + } catch (err) { + journal.error({ updateUserEmailVerified: err, id, email }) + return { error: 'db.error.update.user_data' } + } } \ No newline at end of file diff --git a/data/verification-token.ts b/data/verification-token.ts index ca430a1..08e1182 100644 --- a/data/verification-token.ts +++ b/data/verification-token.ts @@ -1,9 +1,13 @@ +'use server' + import db from '@/lib/db' +import journal from '@/actions/logger' export const getVerificationTokenByToken = async (token: string) => { try { return await db.verificationToken.findUnique({ where: { token } }) - } catch { + } catch (err) { + journal.error({ getVerificationTokenByToken: err, token }) return null } } @@ -11,7 +15,18 @@ export const getVerificationTokenByToken = async (token: string) => { export const getVerificationTokenByEmail = async (email: string) => { try { return await db.verificationToken.findFirst({ where: { email } }) - } catch { + } catch (err) { + journal.error({ getVerificationTokenByEmail: err, email }) return null } +} + +export const deleteVerificationToken = async (id: string) => { + try { + await db.verificationToken.delete({ + where: { id }, + }) + } catch (err) { + journal.error({ deleteVerificationToken: err, id }) + } } \ No newline at end of file diff --git a/lib/server.ts b/lib/server.ts index 2ee65d2..e24bfca 100644 --- a/lib/server.ts +++ b/lib/server.ts @@ -3,5 +3,7 @@ import { readdir } from 'fs/promises' export const getDirectories = async (source: string) => { - return (await readdir(source, { withFileTypes: true })).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name) + return (await readdir(source, { withFileTypes: true })) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) } diff --git a/lib/tokens.ts b/lib/tokens.ts index cee7c03..671d8c9 100644 --- a/lib/tokens.ts +++ b/lib/tokens.ts @@ -1,10 +1,28 @@ +import crypto from 'crypto' import { v4 as uuidV4 } from 'uuid' -import { - VERIFICATION_TOKEN_EXPIRATION_DURATION, -} from '@/config/auth' +import { VERIFICATION_TOKEN_EXPIRATION_DURATION } from '@/config/auth' import db from '@/lib/db' import { getVerificationTokenByEmail } from '@/data/verification-token' import { getPasswordResetTokenByEmail } from '@/data/password-reset-token' +import { deleteTwoFactorToken, getTwoFactorTokenByEmail } from '@/data/two-factor-token' + +export const generateTwoFactorToken = async (email: string) => { + const token = crypto.randomInt(100_000, 1_000_000).toString() + const expires = new Date(new Date().getTime() + VERIFICATION_TOKEN_EXPIRATION_DURATION) + + const existingToken = await getTwoFactorTokenByEmail(email) + + if (existingToken) { + await deleteTwoFactorToken(existingToken.id) + } + + try { + return await db.twoFactorToken.create({ data: { email, token, expires } }) + } catch (err) { + console.log(err) + return null + } +} export const generatePasswordResetToken = async (email: string) => { const token = uuidV4() @@ -21,9 +39,7 @@ export const generatePasswordResetToken = async (email: string) => { const passwordResetToken = await db.passwordResetToken.create({ data: { - email, - token, - expires, + email, token, expires, }, }) @@ -46,9 +62,7 @@ export const generateVerificationToken = async (email: string) => { const verificationToken = await db.verificationToken.create({ data: { - email, - token, - expires, + email, token, expires, }, }) diff --git a/lib/utils.ts b/lib/utils.ts index a36843a..8cba2e3 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -21,8 +21,6 @@ export const testPathnameRegex = ( const pattern: string = `^(/(${locales.join('|')}))?(${pages.flatMap( (p) => (p === '/' ? ['', '/'] : p)).join('|')})/?$` - //console.log(pattern) - return RegExp(pattern, 'is').test(pathName) } diff --git a/locales/custom/mailer/en.ts b/locales/custom/mailer/en.ts index 2e7f1ca..45ce529 100644 --- a/locales/custom/mailer/en.ts +++ b/locales/custom/mailer/en.ts @@ -5,6 +5,7 @@ export default { confirmed_email: 'to confirm email', subject: { send_verification_email: 'Complete email verification for site {site_name}', + send_2FA_code: 'Your 2FA code from {site_name}', }, body: { send_verification_email: { diff --git a/locales/custom/mailer/uk.ts b/locales/custom/mailer/uk.ts index e81114c..c406f84 100644 --- a/locales/custom/mailer/uk.ts +++ b/locales/custom/mailer/uk.ts @@ -5,6 +5,7 @@ export default { confirmed_email: 'для підтвердження електронної пошти', subject: { send_verification_email: 'Завершіть верифікацію Вашої електронної пошти для сайту {site_name}', + send_2FA_code: 'Ваш код двофакторної аутентифікації із сайту {site_name}', }, body: { send_verification_email: { diff --git a/locales/en/auth.ts b/locales/en/auth.ts index 3dae0fa..18d4774 100644 --- a/locales/en/auth.ts +++ b/locales/en/auth.ts @@ -45,17 +45,20 @@ export default { missing_token: 'Missing token!', invalid_token: 'Invalid token!', expired_token: 'Token has expired!', + invalid_code: 'Invalid code!', + expired_code: 'Code has expired!', }, }, email: { success: { confirmation_email_sent: 'Confirmation email sent!', reset_email_sent: 'A password reset letter has been sent to the specified email address!', + _2FA_email_sent: '2FA email sent!', }, error: { verification_email_sending_error: 'Could not send verification email!', reset_password_sending_error: 'Could not send reset password email!', - + _2FA_email_sending_error: 'Could not send 2FA email!', }, }, } as const \ No newline at end of file diff --git a/locales/en/form.ts b/locales/en/form.ts index 471cc50..200e135 100644 --- a/locales/en/form.ts +++ b/locales/en/form.ts @@ -5,6 +5,11 @@ export default { confirm_password: 'Confirm password', login: 'Login', name: 'Name', + two_factor: 'Two Factor Authentication Code', + }, + button: { + two_factor: 'Confirm', + login: 'Login', }, placeholder: { email: 'dead.end@acme.com', diff --git a/locales/en/schema.ts b/locales/en/schema.ts index 9e1af2c..22c72d0 100644 --- a/locales/en/schema.ts +++ b/locales/en/schema.ts @@ -15,4 +15,7 @@ export default { name: { required: `Name is required`, }, + two_factor: { + length: 'Code must contain exactly {length} digits', + }, } as const \ No newline at end of file diff --git a/locales/uk/auth.ts b/locales/uk/auth.ts index bb22c98..06d9aaa 100644 --- a/locales/uk/auth.ts +++ b/locales/uk/auth.ts @@ -45,15 +45,19 @@ export default { missing_token: 'Відсутній токен!', invalid_token: 'Недійсний токен!', expired_token: 'Сплив термін дії токена!', + invalid_code: 'Невірний код!', + expired_code: 'Сплив термін дії коду!', }, }, email: { success: { confirmation_email_sent: 'Лист із підтвердженням надіслано!', reset_email_sent: 'Лист для скидання паролю надіслано на вказану електронну адресу', + _2FA_email_sent: 'Код 2FA надіслано на вказану електронну адресу', }, error: { verification_email_sending_error: 'Не вдалося надіслати електронний лист для підтвердження!', + _2FA_email_sending_error: 'Не вдалося надіслати електронний лист з 2FA кодом!', }, }, } as const \ No newline at end of file diff --git a/locales/uk/form.ts b/locales/uk/form.ts index 0256938..c6f0d64 100644 --- a/locales/uk/form.ts +++ b/locales/uk/form.ts @@ -5,6 +5,11 @@ export default { confirm_password: 'Підтвердьте пароль', login: 'Лоґін', name: 'Ім\'я та прізвище', + two_factor: 'Код двофакторної перевірки', + }, + button: { + two_factor: 'Підтвердити', + login: 'Лоґін', }, placeholder: { email: 'dead.end@acme.com', diff --git a/locales/uk/schema.ts b/locales/uk/schema.ts index 177706e..dbec749 100644 --- a/locales/uk/schema.ts +++ b/locales/uk/schema.ts @@ -15,4 +15,7 @@ export default { name: { required: `Необхідно вказати ім'я`, }, + two_factor: { + length: 'Код має містити рівно {length} цифр', + }, } as const \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 2557ec7..aaeb79c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,6 +2,9 @@ import path from 'node:path' /** @type {import('next').NextConfig} */ const nextConfig = { + experimental: { + serverComponentsExternalPackages: ['pino'], + }, sassOptions: { includePaths: [path.join(path.resolve('.'), 'styles')], }, diff --git a/package-lock.json b/package-lock.json index 241229e..796b4eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "a-naklejka", - "version": "0.1.0", + "name": "yo-next-space", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "a-naklejka", - "version": "0.1.0", + "name": "yo-next-space", + "version": "0.1.1", "dependencies": { "@auth/prisma-adapter": "^1.5.2", "@hookform/resolvers": "^3.3.4", @@ -23,6 +23,8 @@ "next-auth": "^5.0.0-beta.16", "next-international": "^1.2.4", "nodemailer": "^6.9.13", + "pino": "^9.0.0", + "pino-http": "^9.0.0", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.2", @@ -46,6 +48,8 @@ "eslint": "^8", "eslint-config-next": "14.1.4", "eslint-plugin-validate-filename": "^0.0.4", + "pino-caller": "^3.4.0", + "pino-pretty": "^11.0.0", "postcss": "^8", "prisma": "^5.12.1", "sass": "^1.74.1", @@ -77,9 +81,9 @@ } }, "node_modules/@auth/core": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.28.2.tgz", - "integrity": "sha512-Rlvu6yKa4bKbhQESMaEm6jHOY5ncIrsrQkC8tcwVQmf+cBLk7ReI9DIJS2O/WkIDoOwvM9PHiXTi5b+b/eyXxw==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.29.0.tgz", + "integrity": "sha512-MdfEjU6WRjUnPG1+XeBWrTIlAsLZU6V0imCIqVDDDPxLI6UZWldXVqAA2EsDazGofV78jqiCLHaN85mJITDqdg==", "dependencies": { "@panva/hkdf": "^1.1.1", "@types/cookie": "0.6.0", @@ -107,11 +111,11 @@ } }, "node_modules/@auth/prisma-adapter": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.5.2.tgz", - "integrity": "sha512-yP331ZtBvzrtKSqsJ7/LeojJVimiSvJ7CKeA4HfvAGtHhKvu2m5IVe8mnWa6yX1xWK8I5VyZMMlWPcaDtNKlwA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.6.0.tgz", + "integrity": "sha512-PQU8/Oi5gfjzb0MkhMGVX0Dg877phPzsQdK54+C7ubukCeZPjyvuSAx1vVtWEYVWp2oQvjgG/C6QiDoeC7S10A==", "dependencies": { - "@auth/core": "0.28.2" + "@auth/core": "0.29.0" }, "peerDependencies": { "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5" @@ -1000,9 +1004,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.12.1.tgz", - "integrity": "sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.13.0.tgz", + "integrity": "sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -1017,48 +1021,48 @@ } }, "node_modules/@prisma/debug": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.12.1.tgz", - "integrity": "sha512-kd/wNsR0klrv79o1ITsbWxYyh4QWuBidvxsXSParPsYSu0ircUmNk3q4ojsgNc3/81b0ozg76iastOG43tbf8A==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.13.0.tgz", + "integrity": "sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.12.1.tgz", - "integrity": "sha512-HQDdglLw2bZR/TXD2Y+YfDMvi5Q8H+acbswqOsWyq9pPjBLYJ6gzM+ptlTU/AV6tl0XSZLU1/7F4qaWa8bqpJA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.13.0.tgz", + "integrity": "sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.12.1", - "@prisma/engines-version": "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab", - "@prisma/fetch-engine": "5.12.1", - "@prisma/get-platform": "5.12.1" + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/fetch-engine": "5.13.0", + "@prisma/get-platform": "5.13.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab.tgz", - "integrity": "sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g==", + "version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b.tgz", + "integrity": "sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.12.1.tgz", - "integrity": "sha512-qSs3KcX1HKcea1A+hlJVK/ljj0PNIUHDxAayGMvgJBqmaN32P9tCidlKz1EGv6WoRFICYnk3Dd/YFLBwnFIozA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.13.0.tgz", + "integrity": "sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.12.1", - "@prisma/engines-version": "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab", - "@prisma/get-platform": "5.12.1" + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/get-platform": "5.13.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.12.1.tgz", - "integrity": "sha512-pgIR+pSvhYHiUcqXVEZS31NrFOTENC9yFUdEAcx7cdQBoZPmHVjtjN4Ss6NzVDMYPrKJJ51U14EhEoeuBlMioQ==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.13.0.tgz", + "integrity": "sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.12.1" + "@prisma/debug": "5.13.0" } }, "node_modules/@radix-ui/number": { @@ -1604,9 +1608,9 @@ } }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz", - "integrity": "sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz", + "integrity": "sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw==", "dev": true }, "node_modules/@swc/helpers": { @@ -1635,9 +1639,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", - "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1659,9 +1663,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.74", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz", - "integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.0.tgz", + "integrity": "sha512-DiUcKjzE6soLyln8NNZmyhcQjVv+WsUIFSqetMN0p8927OztKT4VTfFTqsbAi5oAGIcgOmOajlfBqyptDDjZRw==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -1669,9 +1673,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.24", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.24.tgz", - "integrity": "sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "devOptional": true, "dependencies": { "@types/react": "*" @@ -1716,58 +1720,6 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/scope-manager": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", @@ -1798,6 +1750,58 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/visitor-keys": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", @@ -1821,6 +1825,17 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -2102,6 +2117,14 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -2177,6 +2200,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -2246,6 +2288,35 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2302,9 +2373,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001608", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", - "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", "funding": [ { "type": "opencollective", @@ -2395,9 +2466,9 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -2439,6 +2510,12 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2566,6 +2643,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2685,9 +2771,9 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { - "version": "1.4.729", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz", - "integrity": "sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==", + "version": "1.4.749", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.749.tgz", + "integrity": "sha512-LRMMrM9ITOvue0PoBrvNIraVmuDbJV5QC9ierz/z5VilMdPOVMjOtpICNld3PuXuTZ3CHH/UPxX9gHhAPwi+0Q==", "dev": true }, "node_modules/emoji-regex": { @@ -2695,6 +2781,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", @@ -2790,14 +2885,14 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz", - "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", + "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", @@ -3163,9 +3258,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz", + "integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==", "dev": true, "engines": { "node": ">=10" @@ -3315,6 +3410,22 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execSync": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/execSync/-/execSync-1.0.2.tgz", @@ -3327,6 +3438,12 @@ "node": "*" } }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3371,6 +3488,20 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3529,6 +3660,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -3792,6 +3931,31 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4298,6 +4462,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4446,9 +4619,9 @@ } }, "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.1.tgz", + "integrity": "sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA==", "engines": { "node": "14 || >=16.14" } @@ -4595,11 +4768,11 @@ } }, "node_modules/next-auth": { - "version": "5.0.0-beta.16", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.16.tgz", - "integrity": "sha512-dX2snB+ezN23tFzSes3n3uosT9iBf0eILPYWH/R2fd9n3ZzdMQlRzq7JIOPeS1aLc84IuRlyuyXyx9XmmZB6og==", + "version": "5.0.0-beta.17", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.17.tgz", + "integrity": "sha512-XA/7JtAjOgDfAeotJPFUsFZGGItZwzZrxLt9Gc9fE7EchLk6zydZfuZ22Vvwixs3IilkN644D5IoD5tEOAFGCQ==", "dependencies": { - "@auth/core": "0.28.1" + "@auth/core": "0.30.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", @@ -4621,9 +4794,9 @@ } }, "node_modules/next-auth/node_modules/@auth/core": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.28.1.tgz", - "integrity": "sha512-gvp74mypYZADpTlfGRp6HE0G3pIHWvtJpy+KZ+8FvY0cmlIpHog+jdMOdd29dQtLtN25kF2YbfHsesCFuGUQbg==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.30.0.tgz", + "integrity": "sha512-8AE4m/nk+4EIiVCJwxZAsJeAQuzpEC8M8768mmKVn60CGDdupKQkVhxbRlm5Qh7eNRCoFFME+0DvtaX2aXrYaA==", "dependencies": { "@panva/hkdf": "^1.1.1", "@types/cookie": "0.6.0", @@ -4858,6 +5031,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5005,6 +5186,113 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.0.0.tgz", + "integrity": "sha512-uI1ThkzTShNSwvsUM6b4ND8ANzWURk9zTELMztFkmnCQeR/4wkomJ+echHee5GMWGovoSfjwdeu80DsFIt7mbA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-caller": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pino-caller/-/pino-caller-3.4.0.tgz", + "integrity": "sha512-2aEjlmhLA7J3lGBXKDSxtlfDY+cBzGh5PnLFP6ZUhvyqCnqKfv28ulpSch6uymGIdo7fzxXHK2hvR5FrdzbhTg==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.13" + }, + "engines": { + "node": ">6.0.0" + }, + "peerDependencies": { + "pino": "*" + } + }, + "node_modules/pino-http": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-9.0.0.tgz", + "integrity": "sha512-Q9QDNEz0vQmbJtMFjOVr2c9yL92vHudjmr3s3m6J1hbw3DBGFZJm3TIj9TWyynZ4GEsEA9SOtni4heRUr6lNOg==", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^8.17.1", + "pino-std-serializers": "^6.2.2", + "process-warning": "^3.0.0" + } + }, + "node_modules/pino-http/node_modules/pino": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-pretty": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.0.0.tgz", + "integrity": "sha512-YFJZqw59mHIY72wBnBs7XhLGG6qpJMa4pEQTRgEPEbjIYbng2LXEZZF1DoyDg9CfejEy8uZCyzpcBXXG0oOCwQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -5198,13 +5486,13 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "node_modules/prisma": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.12.1.tgz", - "integrity": "sha512-SkMnb6wyIxTv9ACqiHBI2u9gD6y98qXRoCoLEnZsF6yee5Qg828G+ARrESN+lQHdw4maSZFFSBPPDpvSiVTo0Q==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.13.0.tgz", + "integrity": "sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.12.1" + "@prisma/engines": "5.13.0" }, "bin": { "prisma": "build/index.js" @@ -5213,6 +5501,19 @@ "node": ">=16.13" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5224,6 +5525,16 @@ "react-is": "^16.13.1" } }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5252,10 +5563,15 @@ } ] }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.0.tgz", + "integrity": "sha512-RPutkJftSAldDibyrjuku7q11d3oy6wKOyPe5K1HA/HwwrXcEqBdHsLypkC2FFYjP7bPUa6gbzSBhw4sY2JcDg==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -5264,21 +5580,21 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-zaKdLBftQJnvb7FtDIpZtsAIb2MZU087RM8bRDZU8LVCCFYjPTsDZJNFUWPcVz3HFSN1n/caxi0ca4B/aaVQGQ==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.1" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.0" } }, "node_modules/react-hook-form": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", - "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", + "version": "7.51.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz", + "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==", "engines": { "node": ">=12.22.0" }, @@ -5291,9 +5607,9 @@ } }, "node_modules/react-icons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", - "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.1.0.tgz", + "integrity": "sha512-D3zug1270S4hbSlIRJ0CUS97QE1yNNKDjzQe3HqY0aefp2CBn9VgzgES27sRR2gOvFK+0CNx/BW0ggOESp6fqQ==", "peerDependencies": { "react": "*" } @@ -5321,9 +5637,9 @@ } }, "node_modules/react-loader-spinner/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.0.tgz", + "integrity": "sha512-wRiUsea88TjKDc4FBEn+sLvIDesp6brMbGWnJGjew2waAc9evdhja/2LvePc898HJbHw0L+MTWy7NhpnELAvLQ==" }, "node_modules/react-remove-scroll": { "version": "2.5.5", @@ -5400,6 +5716,21 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5411,6 +5742,14 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -5573,6 +5912,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -5590,10 +5948,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, "node_modules/sass": { - "version": "1.74.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.74.1.tgz", - "integrity": "sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==", + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz", + "integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==", "devOptional": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -5608,13 +5974,19 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.1.tgz", + "integrity": "sha512-5GKS5JGfiah1O38Vfa9srZE4s3wdHbwjlCrvIookrg2FO9aIwKLOJXuJQFlEfNcVSOXuaL2hzDeY20uVXcUtrw==", "dependencies": { "loose-envify": "^1.1.0" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -5801,6 +6173,23 @@ "node": ">=8" } }, + "node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -5809,6 +6198,24 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -5817,6 +6224,14 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -6132,11 +6547,11 @@ } }, "node_modules/tailwind-merge": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz", - "integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", + "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==", "dependencies": { - "@babel/runtime": "^7.24.0" + "@babel/runtime": "^7.24.1" }, "funding": { "type": "github", @@ -6250,6 +6665,14 @@ "node": ">=0.8" } }, + "node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6393,9 +6816,9 @@ } }, "node_modules/typescript": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", - "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -6738,9 +7161,9 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.23.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.4.tgz", + "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 202f34f..3af3010 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "a-naklejka", - "version": "0.1.0", + "name": "yo-next-space", + "version": "0.1.1", "private": true, "scripts": { "dev": "next dev", @@ -31,6 +31,8 @@ "next-auth": "^5.0.0-beta.16", "next-international": "^1.2.4", "nodemailer": "^6.9.13", + "pino": "^9.0.0", + "pino-http": "^9.0.0", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.2", @@ -54,6 +56,8 @@ "eslint": "^8", "eslint-config-next": "14.1.4", "eslint-plugin-validate-filename": "^0.0.4", + "pino-caller": "^3.4.0", + "pino-pretty": "^11.0.0", "postcss": "^8", "prisma": "^5.12.1", "sass": "^1.74.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 608f178..f95032f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,15 +26,17 @@ enum UserRole { } model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - password String? - role UserRole @default(CUSTOMER) - extendedData Json? - accounts Account[] + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + password String? + role UserRole @default(CUSTOMER) + extendedData Json? + accounts Account[] + isTwoFactorEnabled Boolean @default(false) + twoFactorComfirmation TwoFactorComfirmation? } model Account { @@ -73,3 +75,18 @@ model PasswordResetToken { @@unique([email, token]) } + +model TwoFactorToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model TwoFactorComfirmation { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} diff --git a/schemas/index.ts b/schemas/index.ts index 8931554..f059103 100644 --- a/schemas/index.ts +++ b/schemas/index.ts @@ -1,5 +1,8 @@ import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, PASSWORD_STRENGTH_ACME } from '@/config/validation' -import { object, string } from 'zod' +import { object, optional, string } from 'zod' + +const _2FA_CODE_LENGTH = 6 +const _2FA_CODE_REGEX = /^\d{6}$/ // all translations is implemented in '@/components/ui/form' via TranslateClientFragment @@ -7,6 +10,7 @@ const minPasswordMessage = JSON.stringify(['schema.password.length.min', { min: const maxPasswordMessage = JSON.stringify(['schema.password.length.max', { max: MAX_PASSWORD_LENGTH }]) const maxPasswordStrength = JSON.stringify( ['schema.password.strength.acme', { min: MIN_PASSWORD_LENGTH, max: MAX_PASSWORD_LENGTH }]) +const regexCodeMessage = JSON.stringify(['schema.two_factor.length', { length: _2FA_CODE_LENGTH }]) const email = string().trim().toLowerCase().email({ message: 'schema.email.required' }) const password = string().trim().regex(new RegExp(PASSWORD_STRENGTH_ACME, 'mg'), @@ -14,7 +18,13 @@ const password = string().trim().regex(new RegExp(PASSWORD_STRENGTH_ACME, 'mg'), max(MAX_PASSWORD_LENGTH, { message: maxPasswordMessage }) export const LoginSchema = object({ - email, password: string().trim().min(1, { message: 'schema.password.required' }), + email, + password: string().trim().min(1, { message: 'schema.password.required' }), + code: string({ message: regexCodeMessage }). + trim(). + length(_2FA_CODE_LENGTH, { message: regexCodeMessage }). + regex(_2FA_CODE_REGEX, { message: regexCodeMessage }). + optional(), }) export const RegisterSchema = object({