Implemented email verification

This commit is contained in:
Yevhen Odynets 2024-04-12 13:52:16 +03:00
parent 78107d4ec7
commit b1ad7b5c3e
51 changed files with 604 additions and 213 deletions

View File

@ -1,3 +1,37 @@
{
"extends": "next/core-web-vitals"
"extends": [
"next/core-web-vitals"
],
"plugins": [
"validate-filename"
],
"rules": {
"validate-filename/naming-rules": [
"error",
{
"rules": [
{
"case": "kebab",
"target": "**/components/**",
"patterns": "^[a-z0-9-]+.tsx$"
},
{
"case": "kebab",
"target": "**/app/**",
"patterns": "^(default|page|layout|loading|error|not-found|route|template).(tsx|ts)$"
},
{
"case": "camel",
"target": "**/hooks/**",
"patterns": "^use"
},
{
"case": "camel",
"target": "**/providers/**",
"patterns": "^[a-zA-Z]*Provider"
}
]
}
]
}
}

6
.idea/tailwindcss.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TailwindSettings">
<option name="lspConfiguration" value="{&#10; &quot;includeLanguages&quot;: {&#10; &quot;ftl&quot;: &quot;html&quot;,&#10; &quot;jinja&quot;: &quot;html&quot;,&#10; &quot;jinja2&quot;: &quot;html&quot;,&#10; &quot;smarty&quot;: &quot;html&quot;,&#10; &quot;tmpl&quot;: &quot;gohtml&quot;,&#10; &quot;cshtml&quot;: &quot;html&quot;,&#10; &quot;vbhtml&quot;: &quot;html&quot;,&#10; &quot;razor&quot;: &quot;html&quot;&#10; },&#10; &quot;files&quot;: {&#10; &quot;exclude&quot;: [&#10; &quot;**/.git/**&quot;,&#10; &quot;**/node_modules/**&quot;,&#10; &quot;**/.hg/**&quot;,&#10; &quot;**/.svn/**&quot;&#10; ]&#10; },&#10; &quot;emmetCompletions&quot;: true,&#10; &quot;classAttributes&quot;: [&quot;class&quot;, &quot;className&quot;, &quot;ngClass&quot;],&#10; &quot;colorDecorators&quot;: true,&#10; &quot;showPixelEquivalents&quot;: true,&#10; &quot;rootFontSize&quot;: 16,&#10; &quot;hovers&quot;: true,&#10; &quot;suggestions&quot;: true,&#10; &quot;codeActions&quot;: true,&#10; &quot;validate&quot;: true,&#10; &quot;lint&quot;: {&#10; &quot;invalidScreen&quot;: &quot;error&quot;,&#10; &quot;invalidVariant&quot;: &quot;error&quot;,&#10; &quot;invalidTailwindDirective&quot;: &quot;error&quot;,&#10; &quot;invalidApply&quot;: &quot;error&quot;,&#10; &quot;invalidConfigPath&quot;: &quot;error&quot;,&#10; &quot;cssConflict&quot;: &quot;warning&quot;,&#10; &quot;recommendedVariantOrder&quot;: &quot;warning&quot;&#10; },&#10; &quot;experimental&quot;: {&#10; &quot;configFile&quot;: null,&#10; &quot;classRegex&quot;: []&#10; }&#10;}" />
</component>
</project>

View File

@ -5,7 +5,7 @@ import bcrypt from 'bcryptjs'
import { RegisterSchema } from '@/schemas'
import { PASSWORD_SALT_LENGTH } from '@/config/validation'
import { db } from '@/lib/db'
import db from '@/lib/db'
import { getUserByEmail } from '@/data/user'
import { sendVerificationEmail } from '@/actions/send-verification-email'

View File

@ -1,18 +1,16 @@
'use server'
import mailer from '@/lib/mailer'
import { env } from 'process'
import { AUTH_EMAIL_VERIFICATION_URL } from '@/config/routes'
import { AUTH_USER_VERIFICATION_URL } from '@/config/routes'
import { generateVerificationToken } from '@/lib/tokens'
import { env } from '@/lib/utils'
const sendVerificationEmail = async (email: string, name?: string | null) => {
const verificationToken = await generateVerificationToken(email)
const confirmLink: string = [env.SITE_URL, AUTH_EMAIL_VERIFICATION_URL, '?token=', verificationToken].join('')
const confirmLink: string = [env('SITE_URL'), AUTH_USER_VERIFICATION_URL, verificationToken.token].join('')
const { isOk, code, info, error } = await mailer({
to: name ? [
{ name: name?.toString(), address: verificationToken.email },
`test-xyhy2bvhj@srv1.mail-tester.com`] : verificationToken.email,
to: name ? { name: name?.toString(), address: verificationToken.email } : verificationToken.email,
subject: 'Complete email verification for A-Naklejka',
html: `<p>Click <a href="${confirmLink}">here</a> to confirm email</p>`,
})
@ -20,7 +18,7 @@ const sendVerificationEmail = async (email: string, name?: string | null) => {
if (isOk) {
return { success: code === 250 ? 'auth.email.success.confirmation_email_sent' : info?.response }
} else {
return { error: env.DEBUG === 'true' ? error?.response : 'auth.email.error.verification_email_sending_error' }
return { error: env('DEBUG') === 'true' ? error?.response : 'auth.email.error.verification_email_sending_error' }
}
}

View File

@ -0,0 +1,41 @@
'use server'
import db from '@/lib/db'
import { getVerificationTokenByToken } from '@/data/verification-token'
import { getUserByEmail } from '@/data/user'
export const userVerification = async (token: string) => {
const existingToken = await getVerificationTokenByToken(token)
if (!existingToken) return { error: 'No verification token found!' }
const tokenHasExpired: boolean = new Date(existingToken.expires) < new Date()
if (tokenHasExpired) return { error: 'Unfortunately your token has expired!' }
const existingUser = await getUserByEmail(existingToken.email)
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: 'Could not update user data! Please, try again by reloading page!' }
}
try {
await db.verificationToken.delete({
where: { id: existingToken.id },
})
} catch (e) {
// TODO: log error on disc or db
console.error(e)
}
return { success: 'User verified!' }
}

View File

@ -1,23 +1,14 @@
'use client'
import type { Metadata } from 'next'
import mailer from '@/lib/mailer'
export default function AboutPage () {
const onClick = () => {
mailer({
to: [
{ name: 'Yevhen', address: 'it@amok.space' },
{ name: 'Євген', address: 'yevhen.odynets@gmail.com' },
],
subject: 'ПОСИЛЕННЯ МОБІЛІЗАЦІЇ В УКРАЇНІ',
html: `<div>Коли Рада <strong>розгляне</strong> законопроєкт про мобілізацію у <del>другому</del> читанні</div>`,
}).catch(console.error)
}
return (
<button onClick={onClick}
className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent">
sendmail
</button>
)
export const metadata: Metadata = {
title: 'key',
description: '...',
}
const AboutPage = () => {
return <>About
<hr/>
</>
}
export default AboutPage

View File

@ -1,3 +1,6 @@
/**
* @type {JSX.Element}
*/
export default function AboutUsPage () {
return (<div>AboutUsPage</div>)
}

View File

@ -1,9 +1,8 @@
import { Poppins } from 'next/font/google'
import { getScopedI18n } from '@/locales/server'
import { cn } from '@/lib/utils'
import { cn, env } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import LoginButton from '@/components/auth/LoginButton'
import { bg as bgg } from '@/config/layout'
import LoginButton from '@/components/auth/login-button'
const font = Poppins({
subsets: ['latin'], weight: ['600'],
@ -22,6 +21,7 @@ export default async function Home () {
<div>
<LoginButton>
<Button variant="secondary" size="lg">{t('sign_in')}</Button>
</LoginButton>
</div>
</div>

View File

@ -1,4 +1,4 @@
import ErrorCard from '@/components/auth/ErrorCard'
import ErrorCard from '@/components/auth/error-card'
const AuthErrorPage = () => {
return (

View File

@ -1,7 +1,7 @@
'use client'
import { ReactElement } from 'react'
import Navbar from '@/components/auth/Navbar'
import Navbar from '@/components/auth/navbar'
type Props = {
//params: { locale: string };

View File

@ -1,4 +1,4 @@
import { LoginForm } from '@/components/auth/LoginForm'
import { LoginForm } from '@/components/auth/login-form'
const LoginPage = () => {
return (

View File

@ -1,4 +1,4 @@
import { RegisterForm } from '@/components/auth/RegisterForm'
import { RegisterForm } from '@/components/auth/register-form'
const RegisterPage = () => {
return (

View File

@ -0,0 +1,5 @@
import UserVerificationForm from '@/components/auth/user-verification-form'
export default function TokenVerificationPage ({ params }: { params: { token: string } }) {
return <UserVerificationForm token={params.token}/>
}

View File

@ -2,23 +2,21 @@ import type { NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import Google from 'next-auth/providers/google'
import Github from 'next-auth/providers/github'
//import Facebook from 'next-auth/providers/facebook'
//import Twitter from 'next-auth/providers/twitter'
import { LoginSchema } from '@/schemas'
import bcrypt from 'bcryptjs'
import { getUserByEmail } from '@/data/user'
import { env } from 'process'
import { env } from '@/lib/utils'
export default {
secret: env.AUTH_SECRET,
secret: env('AUTH_SECRET'),
providers: [
Google({
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
clientId: env('GOOGLE_CLIENT_ID'),
clientSecret: env('GOOGLE_CLIENT_SECRET'),
}),
Github({
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
clientId: env('GITHUB_CLIENT_ID'),
clientSecret: env('GITHUB_CLIENT_SECRET'),
}),
//Twitter({}),
/*Facebook({

View File

@ -1,26 +0,0 @@
import { useI18n } from '@/locales/client'
type Props = {
message: string
}
const _ = (message: string): string => {
const t = useI18n()
if (message.startsWith('["')) {
const data = JSON.parse(message)
if (data.length > 1) {
message = data.shift()
// @ts-ignore
return t(message, ...data)
}
}
// @ts-ignore
return t(message)
}
const TranslateClientFragment = ({ message }: Props) => {
return <>{_(message)}</>
}
export default TranslateClientFragment

View File

@ -1,8 +1,8 @@
'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/BackButton'
import { Header } from '@/components/auth/header'
import { Social } from '@/components/auth/social'
import { BackButton } from '@/components/auth/back-button'
type Props = {
children: React.ReactNode
@ -25,7 +25,7 @@ export const CardWrapper = ({
}: Props) => {
return (
<Card
className="max-w-[414px] w-[100%] shadow-md md:min-w-[414px] sm:w-full">
className={`max-w-[430px] w-[100%] shadow-md md:min-w-[430px] sm:w-full`}>
<CardHeader>
<Header label={headerLabel} title={headerTitle}/>
</CardHeader>

View File

@ -1,6 +1,6 @@
'use client'
import { CardWrapper } from '@/components/auth/CardWrapper'
import { CardWrapper } from '@/components/auth/card-wrapper'
import { AUTH_LOGIN_URL } from '@/config/routes'
import { useI18n } from '@/locales/client'
import { TriangleAlert } from 'lucide-react'

View File

@ -14,11 +14,11 @@ import {
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { CardWrapper } from '@/components/auth/CardWrapper'
import { CardWrapper } from '@/components/auth/card-wrapper'
import { useI18n } from '@/locales/client'
import { Button } from '@/components/ui/button'
import FormError from '@/components/FormError'
import FormSuccess from '@/components/FormSuccess'
import FormError from '@/components/form-error'
import FormSuccess from '@/components/form-success'
import { login } from '@/actions/login'
import { LoginSchema } from '@/schemas'
import { AUTH_REGISTER_URL } from '@/config/routes'

View File

@ -1,6 +1,4 @@
'use client'
//import { useScopedI18n } from '@/locales/client'
import LocaleSwitcher from '@/components/LocaleSwitcher'
import LocaleSwitcher from '@/components/locale-switcher'
export default function Navbar () {
//const t = useScopedI18n('navbar')

View File

@ -13,11 +13,11 @@ import {
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { CardWrapper } from '@/components/auth/CardWrapper'
import { CardWrapper } from '@/components/auth/card-wrapper'
import { useI18n } from '@/locales/client'
import { Button } from '@/components/ui/button'
import FormError from '@/components/FormError'
import FormSuccess from '@/components/FormSuccess'
import FormError from '@/components/form-error'
import FormSuccess from '@/components/form-success'
import { register } from '@/actions/register'
import { RegisterSchema } from '@/schemas'

View File

@ -0,0 +1,43 @@
'use client'
import { CardWrapper } from '@/components/auth/card-wrapper'
import { AUTH_LOGIN_URL } from '@/config/routes'
import { useI18n } from '@/locales/client'
import { useCallback, useEffect, useState } from 'react'
import { userVerification } from '@/actions/user-verification'
import FormSuccess from '@/components/form-success'
import FormError from '@/components/form-error'
import { Bars } from 'react-loader-spinner'
const UserVerificationForm = ({ token }: { token: string }) => {
const [error, setError] = useState<string | undefined>(undefined)
const [success, setSuccess] = useState<string | undefined>(undefined)
const onSubmit = useCallback(() => {
userVerification(token).then(data => {
setSuccess(data?.success)
setError(data?.error)
}).catch(() => {
setError('something went wrong')
})
}, [token])
useEffect(() => onSubmit(), [onSubmit])
const t = useI18n()
return (<CardWrapper
headerTitle={t('auth.title')}
headerLabel={t('auth.form.verification.header_label')}
backButtonLabel={t('auth.form.verification.back_button_label')}
backButtonHref={AUTH_LOGIN_URL}
>
<div className="w-full flex items-center justify-center">
<Bars visible={!success && !error} color="hsl(var(--primary))" ariaLabel="loading" wrapperClass="opacity-50"/>
<FormSuccess message={success}/>
<FormError message={error}/>
</div>
</CardWrapper>)
}
export default UserVerificationForm

View File

@ -2,7 +2,7 @@
import { useChangeLocale, useCurrentLocale } from '@/locales/client'
import { LC, type loc } from '@/config/locales'
import { ChangeEvent } from 'react'
import styles from '@/styles/LocaleSwitcher.module.scss'
import styles from '@/styles/locale-switcher.module.scss'
export default function LocaleSwitcher () {
const changeLocale = useChangeLocale()

View File

@ -0,0 +1,23 @@
import { useI18n } from '@/locales/client'
export const __ = (key: any, params?: any): React.ReactNode => {
const t = useI18n()
if (key.startsWith('["')) {
const data = JSON.parse(key)
if (data.length > 1) {
key = data.shift()
// @ts-ignore
return t(key, ...data)
}
}
return t(key, params)
}
const TranslateClientFragment = ({ message, args }: { message: any, args?: any }) => {
return <>{__(message, args)}</>
}
export default TranslateClientFragment

View File

@ -10,9 +10,9 @@ import {
useFormContext,
} from 'react-hook-form'
import { cn } from '@/lib/utils'
import { cn, env } from '@/lib/utils'
import { Label } from '@/components/ui/label'
import TranslateClientFragment from '@/components/TranslateClientFragment'
import TranslateClientFragment from '@/components/translate-client-fragment'
const Form = FormProvider
@ -132,8 +132,7 @@ const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<
className={cn('text-sm font-medium text-destructive', className)}
{...props}
>
{!process.env.IS_SERVER_FLAG && typeof body === 'string' &&
body.includes('schema.message')
{!env('IS_SERVER_FLAG') && typeof body === 'string' && body.match(/^(|\[")schema\./)
? <TranslateClientFragment message={body}/>
: body}
</p>)

View File

@ -1,7 +1,7 @@
import NextAuth from 'next-auth'
import { UserRole } from '@prisma/client'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { db } from '@/lib/db'
import db from '@/lib/db'
import authConfig from '@/auth.config'
import { getUserById } from '@/data/user'
import { AUTH_ERROR_URL, AUTH_LOGIN_URL } from '@/config/routes'

View File

@ -1 +0,0 @@
export const bg: string = 'bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-sky-400 to-blue-800'

View File

@ -1,13 +1,23 @@
// @https://www.localeplanet.com/icu/index.html
type loc = ('uk' | 'en')
const defaultLocale = 'uk'
type Locale = {
id: string,
java: string,
iso: string,
code: loc,
name: string,
originalName: string,
}
export type loc = ('uk' | 'en')
const defaultLocale: loc = 'uk'
const fallbackLocale: loc = 'en'
const importLocales = {
uk: () => import('@/locales/uk'), en: () => import('@/locales/en'),
}
const LC = [
} as const
const LC: Locale[] = [
{
id: 'uk_UA',
java: 'uk-UA',
@ -23,8 +33,8 @@ const LC = [
code: 'en',
name: 'English',
originalName: 'English',
}]
}] as const
const locales = LC.map(locale => locale.code)
const locales: loc[] = LC.map((locale: Locale) => locale.code)
export { locales, defaultLocale, LC, importLocales }
export { locales, defaultLocale, fallbackLocale, LC, importLocales, type loc }

View File

@ -1,16 +1,16 @@
import { env } from 'process'
import SMTPTransport from 'nodemailer/lib/smtp-transport'
import { env } from '@/lib/utils'
export const from: string = `"${env.MAIL_SERVER_SENDER_NAME}" <${env.MAIL_SERVER_USERNAME}>`
export const from: string = `"${env('MAIL_SERVER_SENDER_NAME')}" <${env('MAIL_SERVER_USERNAME')}>`
export const transportOptions: SMTPTransport | SMTPTransport.Options | string = {
host: env.MAIL_SERVER_HOST,
debug: env.MAIL_SERVER_DEBUG === 'true',
logger: env.MAIL_SERVER_LOG === 'true',
port: parseInt(env.MAIL_SERVER_PORT as string),
secure: env.MAIL_SERVER_PORT === '465', // Use `true` for port 465, `false` for all other ports
host: env('MAIL_SERVER_HOST'),
debug: env('MAIL_SERVER_DEBUG') === 'true' && env('NODE_ENV') !== 'production',
logger: env('MAIL_SERVER_LOG') === 'true' && env('NODE_ENV') !== 'production',
port: parseInt(env('MAIL_SERVER_PORT')),
secure: env('MAIL_SERVER_PORT') === '465', // Use `true` for port 465, `false` for all other ports
auth: {
user: env.MAIL_SERVER_USERNAME, pass: env.MAIL_SERVER_PASSWORD,
user: env('MAIL_SERVER_USERNAME'), pass: env('MAIL_SERVER_PASSWORD'),
},
}

View File

@ -1,10 +1,11 @@
import { locales } from '@/config/locales'
import { UUID_V4_REGEX } from '@/config/validation'
export const USER_PROFILE_URL: string = '/cabinet'
export const AUTH_LOGIN_URL: string = '/auth/login'
export const AUTH_REGISTER_URL: string = '/auth/register'
export const AUTH_ERROR_URL: string = '/auth/error'
export const AUTH_EMAIL_VERIFICATION_URL: string = '/auth/email-verification'
export const AUTH_USER_VERIFICATION_URL: string = '/auth/user-verification/'
/**
* An array of routes that accessible to the public.
@ -12,7 +13,7 @@ export const AUTH_EMAIL_VERIFICATION_URL: string = '/auth/email-verification'
* @type {string[]}
*/
export const publicRoutes: string[] = [
'/', '/about']
'/', '/about', `${AUTH_USER_VERIFICATION_URL}${UUID_V4_REGEX}`]
/**
* An array of routes that are used for authentication.
@ -20,7 +21,7 @@ export const publicRoutes: string[] = [
* @type {string[]}
*/
export const authRoutes: string[] = [
AUTH_LOGIN_URL, AUTH_REGISTER_URL, AUTH_ERROR_URL, AUTH_EMAIL_VERIFICATION_URL]
AUTH_LOGIN_URL, AUTH_REGISTER_URL, AUTH_ERROR_URL]
/**
* The prefix for API authentication routes.
@ -39,6 +40,5 @@ export const testPathnameRegex = (
pages: string[], pathName: string): boolean => {
const pattern: string = `^(/(${locales.join('|')}))?(${pages.flatMap(
(p) => (p === '/' ? ['', '/'] : p)).join('|')})/?$`
return RegExp(pattern, 'is').test(pathName)
}

View File

@ -1,2 +1,8 @@
export const MIN_PASSWORD_LENGTH: number = 6
export const MAX_PASSWORD_LENGTH: number = 15
export const PASSWORD_SALT_LENGTH: number = 10
export const UUID_V4_REGEX: string = '[\x30-\x39\x61-\x66]{8}-[\x30-\x39\x61-\x66]{4}-4[\x30-\x39\x61-\x66]{3}-[\x30-\x39\x61-\x66]{4}-[\x30-\x39\x61-\x66]{12}'
export const PASSWORD_STRENGTH_ACME: string = `(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E])` //.{${MIN_PASSWORD_LENGTH},${MAX_PASSWORD_LENGTH}
export const PASSWORD_STRENGTH_STRONG: string = `^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=|.*?[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E])$`

View File

@ -1,5 +1,5 @@
import { User } from '@prisma/client'
import { db } from '@/lib/db'
import db from '@/lib/db'
export const getUserByEmail = async (email: string): Promise<User | null> => {
try {

View File

@ -1,4 +1,5 @@
import { db } from '@/lib/db'
import db from '@/lib/db'
import { VerificationToken } from '@prisma/client'
export const getVerificationTokenByToken = async (token: string) => {
try {
@ -8,7 +9,7 @@ export const getVerificationTokenByToken = async (token: string) => {
}
}
export const getVerificationTokenByEmail = async (email: string) => {
export const getVerificationTokenByEmail = async (email: string): Promise<VerificationToken | null> => {
try {
return await db.verificationToken.findFirst({ where: { email } })
} catch {

View File

@ -1,10 +1,17 @@
import { PrismaClient } from '@prisma/client'
import * as process from 'process'
import { env } from '@/lib/utils'
declare global {
var prisma: PrismaClient | undefined
const prismaClientSingleton = () => {
return new PrismaClient()
}
export const db = globalThis.prisma || new PrismaClient()
declare global {
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>
}
const db = globalThis.prismaGlobal ?? prismaClientSingleton()
export default db
if (env('NODE_ENV') !== 'production') globalThis.prismaGlobal = db
if (process.env.NODE_ENV !== 'production') globalThis.prisma = db

View File

@ -2,7 +2,7 @@ import { v4 as uuid } from 'uuid'
import {
VERIFICATION_TOKEN_EXPIRATION_DURATION,
} from '@/config/auth'
import { db } from '@/lib/db'
import db from '@/lib/db'
import { getVerificationTokenByEmail } from '@/data/verification-token'
export const generateVerificationToken = async (email: string) => {

29
lib/translate.ts Normal file
View File

@ -0,0 +1,29 @@
import { type loc, locales, fallbackLocale } from '@/config/locales'
export const __c = async (key: string | null | undefined, locale?: loc) => {
key = (key ?? '').trim()
if (key.length === 0) return key
if (!locales.includes(locale ??= fallbackLocale)) {
locale = fallbackLocale
}
let data: any = await import(`@/locales/custom.${locale}`).then(({ default: data }) => data).catch(() => false)
if (data === false) return key
const x = key.split('.')
let c: number = x.length
if (c === 1) {
return data.hasOwn(x[0]) && typeof data[x[0]] === 'string' ? data[x[0]] : key
}
for (let i in x) {
if (data.hasOwn(x[i])) {
data = data[x[i]]
c--
}
}
return c === 0 ? data : key
}

View File

@ -1,7 +1,8 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { LC } from '@/config/locales'
import bcrypt from 'bcryptjs'
import { env as dotEnv } from 'process'
export function cn (...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@ -10,3 +11,11 @@ export function cn (...inputs: ClassValue[]) {
export function lc (locale: string) {
return LC.filter(lc => locale === lc.code)[0]
}
export function env (variable: string, defaultValue?: string | ''): string {
return (dotEnv[variable] ?? defaultValue ?? '')
}
export function tr (el: React.ReactNode, params: object) {
}

10
locales/custom.en.ts Normal file
View File

@ -0,0 +1,10 @@
export default {
single: 'I am the only one',
a: {
b: {
c: {
d: 'I am custom english {man}',
},
},
},
} as const

10
locales/custom.uk.ts Normal file
View File

@ -0,0 +1,10 @@
export default {
single: 'Я єдиний',
a: {
b: {
c: {
d: 'Я звичайний український {man}',
},
},
},
} as const

View File

@ -15,6 +15,10 @@ export default {
header_label: 'Create an account',
back_button_label: 'Already have an account?',
},
verification: {
header_label: 'Confirming your account',
back_button_label: 'Back to login',
},
error: {
email_in_use: 'Email already in use with different provider!',
header_label: 'Oops! Something went wrong!',
@ -35,11 +39,21 @@ export default {
},
},
schema: {
message: {
email_required: 'Invalid email address',
password_required: `Password is required`,
name_required: `Name is required`,
password_min: `Password must be at least {min} characters`,
password: {
required: 'Password is required',
strength: {
acme: 'Password must contain at least a single lowercase, uppercase, digit and special character. The length must be between {min} and {max} characters.',
},
length: {
min: 'Password must be at least {min} characters',
max: 'Password must be maximally {max} characters',
},
},
email: {
required: 'Invalid email address',
},
name: {
required: `Name is required`,
},
},
form: {

View File

@ -15,6 +15,11 @@ export default {
header_label: 'Реєстрація облікового запису',
back_button_label: 'Вже маєте обліковий запис?',
},
verification: {
header_label: 'Підтвердження вашого облікового запису',
back_button_label: 'Повернутися до форми авторизації',
},
error: {
email_in_use: 'Електронна пошта вже використовується з іншим логін-провайдером!',
header_label: 'Отакої! Щось пішло не так!',
@ -35,11 +40,21 @@ export default {
},
},
schema: {
message: {
email_required: 'Невірна адреса електронної пошти',
password_required: `Необхідно ввести пароль`,
name_required: `Необхідно вказати ім'я`,
password_min: `Пароль має містити принаймні {min} символів`,
password: {
required: 'Необхідно ввести пароль',
length: {
min: 'Пароль має містити принаймні {min} символів',
max: 'Максимальна кількість символів у паролі: {max}',
},
strength: {
acme: 'Пароль повинен містити принаймні один малий, приписний, цифровий та спеціальний символ. Довжина паролю має бути від {min} до {max} символів.',
},
},
email: {
required: 'Невірна адреса електронної пошти',
},
name: {
required: `Необхідно вказати ім'я`,
},
},
form: {

View File

@ -17,15 +17,12 @@ interface AppRouteHandlerFnContext {
params?: Record<string, string | string[]>;
}
export const middleware = (
request: NextRequest,
event: AppRouteHandlerFnContext): NextResponse | null => {
export const middleware = (request: NextRequest, event: AppRouteHandlerFnContext): NextResponse | null => {
return NextAuth(authConfig).auth((request): any => {
const { nextUrl }: { nextUrl: NextURL } = request
const isLoggedIn: boolean = !!request.auth
const isApiAuthRoute: boolean = nextUrl.pathname.startsWith(apiAuthPrefix)
const isPublicRoute: boolean = testPathnameRegex(publicRoutes,
nextUrl.pathname)
const isPublicRoute: boolean = testPathnameRegex(publicRoutes, nextUrl.pathname)
const isAuthRoute: boolean = testPathnameRegex(authRoutes, nextUrl.pathname)
if (isApiAuthRoute) {
@ -54,7 +51,6 @@ export const middleware = (
export const config = {
matcher: [
'/((?!.+\\.[\\w]+$|_next).*)',
'/(api|static|trpc)(.*)'],
'/((?!.+\\.[\\w]+$|_next).*)', '/(api|static|trpc)(.*)'],
}

275
package-lock.json generated
View File

@ -27,6 +27,7 @@
"react-dom": "^18",
"react-hook-form": "^7.51.2",
"react-icons": "^5.0.1",
"react-loader-spinner": "^6.1.6",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1",
@ -42,11 +43,15 @@
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.4",
"eslint-plugin-validate-filename": "^0.0.4",
"postcss": "^8",
"prisma": "^5.12.1",
"sass": "^1.74.1",
"tailwindcss": "^3.3.0",
"typescript": "^5"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -121,6 +126,24 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
"integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
},
"node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
},
"node_modules/@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -1212,6 +1235,11 @@
"@types/react": "*"
}
},
"node_modules/@types/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw=="
},
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
@ -1246,6 +1274,58 @@
}
}
},
"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",
@ -1276,58 +1356,6 @@
"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",
@ -1823,10 +1851,18 @@
"node": ">= 6"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001606",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz",
"integrity": "sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==",
"version": "1.0.30001608",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz",
"integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==",
"funding": [
{
"type": "opencollective",
@ -1975,6 +2011,24 @@
"node": ">= 8"
}
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"engines": {
"node": ">=4"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -2687,6 +2741,22 @@
"semver": "bin/semver.js"
}
},
"node_modules/eslint-plugin-validate-filename": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-validate-filename/-/eslint-plugin-validate-filename-0.0.4.tgz",
"integrity": "sha512-K3IDwvWIVPqOJCdYcnxQg2e9sOcr8nNmxCN6d1i84Fp2K2rnDiy+/lszW6cVAqxO91kuEcmpQppg7Ph4UmrXvw==",
"dev": true,
"dependencies": {
"micromatch": "^4.0.5",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"eslint": ">=7.0.0 <9.0.0"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@ -4746,6 +4816,27 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"node_modules/react-loader-spinner": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz",
"integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==",
"dependencies": {
"react-is": "^18.2.0",
"styled-components": "^6.1.2"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"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=="
},
"node_modules/react-remove-scroll": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
@ -5100,6 +5191,11 @@
"node": ">= 0.4"
}
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -5352,6 +5448,70 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/styled-components": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz",
"integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==",
"dependencies": {
"@emotion/is-prop-valid": "1.2.1",
"@emotion/unitless": "0.8.0",
"@types/stylis": "4.2.0",
"css-to-react-native": "3.2.0",
"csstype": "3.1.2",
"postcss": "8.4.31",
"shallowequal": "1.1.0",
"stylis": "4.3.1",
"tslib": "2.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/styled-components/node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/styled-components/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/styled-components/node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
@ -5374,6 +5534,11 @@
}
}
},
"node_modules/stylis": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz",
"integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ=="
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",

View File

@ -6,8 +6,15 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"browserslist:update": "npx update-browserslist-db@latest",
"browserslist": "npx browserslist"
},
"browserslist": [
">0.25%",
"not dead",
"not op_mini all"
],
"dependencies": {
"@auth/prisma-adapter": "^1.5.2",
"@hookform/resolvers": "^3.3.4",
@ -28,6 +35,7 @@
"react-dom": "^18",
"react-hook-form": "^7.51.2",
"react-icons": "^5.0.1",
"react-loader-spinner": "^6.1.6",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1",
@ -43,10 +51,14 @@
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.4",
"eslint-plugin-validate-filename": "^0.0.4",
"postcss": "^8",
"prisma": "^5.12.1",
"sass": "^1.74.1",
"tailwindcss": "^3.3.0",
"typescript": "^5"
},
"engines": {
"node": ">=18"
}
}

View File

@ -1,26 +1,21 @@
import { MIN_PASSWORD_LENGTH } from '@/config/validation'
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, PASSWORD_STRENGTH_ACME } from '@/config/validation'
import { object, string } from 'zod'
const passwordMessage = JSON.stringify(
['schema.message.password_min', { min: MIN_PASSWORD_LENGTH }])
const minPasswordMessage = JSON.stringify(['schema.password.length.min', { min: MIN_PASSWORD_LENGTH }])
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 }])
export const LoginSchema = object({
email: string().
trim().
email({ message: 'schema.message.email_required' }).
toLowerCase(),
password: string().
trim().
min(1, { message: 'schema.message.password_required' }),
email: string().trim().toLowerCase().email({ message: 'schema.email.required' }),
password: string().trim().min(1, { message: 'schema.password.required' }),
})
export const RegisterSchema = object({
email: string().
email({ message: 'schema.message.email_required' }).
toLowerCase(),
password: string().
trim().
min(MIN_PASSWORD_LENGTH, { message: passwordMessage }),
name: string().trim().min(1, { message: 'schema.message.name_required' }),
email: string().email({ message: 'schema.email.required' }).toLowerCase(),
password: string().trim().regex(new RegExp(PASSWORD_STRENGTH_ACME, 'mg'), { message: maxPasswordStrength }).
min(MIN_PASSWORD_LENGTH, { message: minPasswordMessage }).max(MAX_PASSWORD_LENGTH, { message: maxPasswordMessage }),
name: string().trim().min(1, { message: 'schema.name.required' }),
})
// Password must contain at least a single lowercase, uppercase, digit and special character.