diff --git a/.eslintrc.json b/.eslintrc.json index bffb357..b28417f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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" + } + ] + } + ] + } } diff --git a/.idea/tailwindcss.xml b/.idea/tailwindcss.xml new file mode 100644 index 0000000..8bca4cd --- /dev/null +++ b/.idea/tailwindcss.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/actions/register.ts b/actions/register.ts index c238bab..89268a4 100644 --- a/actions/register.ts +++ b/actions/register.ts @@ -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' diff --git a/actions/send-verification-email.ts b/actions/send-verification-email.ts index 4aa3899..d2901db 100644 --- a/actions/send-verification-email.ts +++ b/actions/send-verification-email.ts @@ -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: `

Click here to confirm email

`, }) @@ -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' } } } diff --git a/actions/user-verification.ts b/actions/user-verification.ts new file mode 100644 index 0000000..afced33 --- /dev/null +++ b/actions/user-verification.ts @@ -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!' } +} \ No newline at end of file diff --git a/app/[locale]/(root)/(routes)/about/page.tsx b/app/[locale]/(root)/(routes)/about/page.tsx index 94fc942..b269573 100644 --- a/app/[locale]/(root)/(routes)/about/page.tsx +++ b/app/[locale]/(root)/(routes)/about/page.tsx @@ -1,23 +1,14 @@ -'use client' +import type { Metadata } from 'next' -import mailer from '@/lib/mailer' +export const metadata: Metadata = { + title: 'key', + description: '...', +} -export default function AboutPage () { - const onClick = () => { - mailer({ - to: [ - { name: 'Yevhen', address: 'it@amok.space' }, - { name: 'Євген', address: 'yevhen.odynets@gmail.com' }, - ], - subject: 'ПОСИЛЕННЯ МОБІЛІЗАЦІЇ В УКРАЇНІ', - html: `
Коли Рада розгляне законопроєкт про мобілізацію у другому читанні
`, - }).catch(console.error) - } +const AboutPage = () => { + return <>About +
+ +} - return ( - - ) -} \ No newline at end of file +export default AboutPage diff --git a/app/[locale]/(root)/(routes)/about/us/page.tsx b/app/[locale]/(root)/(routes)/about/us/page.tsx index 2ef4c01..65fee63 100644 --- a/app/[locale]/(root)/(routes)/about/us/page.tsx +++ b/app/[locale]/(root)/(routes)/about/us/page.tsx @@ -1,3 +1,6 @@ +/** + * @type {JSX.Element} + */ export default function AboutUsPage () { return (
AboutUsPage
) } \ No newline at end of file diff --git a/app/[locale]/(root)/page.tsx b/app/[locale]/(root)/page.tsx index f5886ac..47fe4bd 100644 --- a/app/[locale]/(root)/page.tsx +++ b/app/[locale]/(root)/page.tsx @@ -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 () {
+
diff --git a/app/[locale]/auth/error/page.tsx b/app/[locale]/auth/error/page.tsx index e731e49..4e2e846 100644 --- a/app/[locale]/auth/error/page.tsx +++ b/app/[locale]/auth/error/page.tsx @@ -1,4 +1,4 @@ -import ErrorCard from '@/components/auth/ErrorCard' +import ErrorCard from '@/components/auth/error-card' const AuthErrorPage = () => { return ( diff --git a/app/[locale]/auth/layout.tsx b/app/[locale]/auth/layout.tsx index af073a5..38739e9 100644 --- a/app/[locale]/auth/layout.tsx +++ b/app/[locale]/auth/layout.tsx @@ -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 }; diff --git a/app/[locale]/auth/login/page.tsx b/app/[locale]/auth/login/page.tsx index d875274..dd91748 100644 --- a/app/[locale]/auth/login/page.tsx +++ b/app/[locale]/auth/login/page.tsx @@ -1,4 +1,4 @@ -import { LoginForm } from '@/components/auth/LoginForm' +import { LoginForm } from '@/components/auth/login-form' const LoginPage = () => { return ( diff --git a/app/[locale]/auth/register/page.tsx b/app/[locale]/auth/register/page.tsx index 988422b..0364618 100644 --- a/app/[locale]/auth/register/page.tsx +++ b/app/[locale]/auth/register/page.tsx @@ -1,4 +1,4 @@ -import { RegisterForm } from '@/components/auth/RegisterForm' +import { RegisterForm } from '@/components/auth/register-form' const RegisterPage = () => { return ( diff --git a/app/[locale]/auth/user-verification/[token]/page.tsx b/app/[locale]/auth/user-verification/[token]/page.tsx new file mode 100644 index 0000000..0ef83c4 --- /dev/null +++ b/app/[locale]/auth/user-verification/[token]/page.tsx @@ -0,0 +1,5 @@ +import UserVerificationForm from '@/components/auth/user-verification-form' + +export default function TokenVerificationPage ({ params }: { params: { token: string } }) { + return +} \ No newline at end of file diff --git a/auth.config.ts b/auth.config.ts index 8f57311..74719ff 100644 --- a/auth.config.ts +++ b/auth.config.ts @@ -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({ diff --git a/components/TranslateClientFragment.tsx b/components/TranslateClientFragment.tsx deleted file mode 100644 index 91b49f2..0000000 --- a/components/TranslateClientFragment.tsx +++ /dev/null @@ -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 diff --git a/components/auth/BackButton.tsx b/components/auth/back-button.tsx similarity index 100% rename from components/auth/BackButton.tsx rename to components/auth/back-button.tsx diff --git a/components/auth/CardWrapper.tsx b/components/auth/card-wrapper.tsx similarity index 84% rename from components/auth/CardWrapper.tsx rename to components/auth/card-wrapper.tsx index b6a4f55..0111f8b 100644 --- a/components/auth/CardWrapper.tsx +++ b/components/auth/card-wrapper.tsx @@ -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 ( + className={`max-w-[430px] w-[100%] shadow-md md:min-w-[430px] sm:w-full`}>
diff --git a/components/auth/ErrorCard.tsx b/components/auth/error-card.tsx similarity index 91% rename from components/auth/ErrorCard.tsx rename to components/auth/error-card.tsx index 994e4b7..4cc38c6 100644 --- a/components/auth/ErrorCard.tsx +++ b/components/auth/error-card.tsx @@ -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' diff --git a/components/auth/Header.tsx b/components/auth/header.tsx similarity index 100% rename from components/auth/Header.tsx rename to components/auth/header.tsx diff --git a/components/auth/LoginButton.tsx b/components/auth/login-button.tsx similarity index 100% rename from components/auth/LoginButton.tsx rename to components/auth/login-button.tsx diff --git a/components/auth/LoginForm.tsx b/components/auth/login-form.tsx similarity index 95% rename from components/auth/LoginForm.tsx rename to components/auth/login-form.tsx index 5af821d..78bdd8e 100644 --- a/components/auth/LoginForm.tsx +++ b/components/auth/login-form.tsx @@ -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' diff --git a/components/auth/Navbar.tsx b/components/auth/navbar.tsx similarity index 65% rename from components/auth/Navbar.tsx rename to components/auth/navbar.tsx index d913d1d..3cbb156 100644 --- a/components/auth/Navbar.tsx +++ b/components/auth/navbar.tsx @@ -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') diff --git a/components/auth/RegisterForm.tsx b/components/auth/register-form.tsx similarity index 96% rename from components/auth/RegisterForm.tsx rename to components/auth/register-form.tsx index e2fbe1c..a4b3997 100644 --- a/components/auth/RegisterForm.tsx +++ b/components/auth/register-form.tsx @@ -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' diff --git a/components/auth/Social.tsx b/components/auth/social.tsx similarity index 100% rename from components/auth/Social.tsx rename to components/auth/social.tsx diff --git a/components/auth/user-verification-form.tsx b/components/auth/user-verification-form.tsx new file mode 100644 index 0000000..d348777 --- /dev/null +++ b/components/auth/user-verification-form.tsx @@ -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(undefined) + const [success, setSuccess] = useState(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 ( +
+ + + +
+
) +} + +export default UserVerificationForm diff --git a/components/FormError.tsx b/components/form-error.tsx similarity index 100% rename from components/FormError.tsx rename to components/form-error.tsx diff --git a/components/FormSuccess.tsx b/components/form-success.tsx similarity index 100% rename from components/FormSuccess.tsx rename to components/form-success.tsx diff --git a/components/LocaleSwitcher.tsx b/components/locale-switcher.tsx similarity index 92% rename from components/LocaleSwitcher.tsx rename to components/locale-switcher.tsx index 2fabc5a..09fd088 100644 --- a/components/LocaleSwitcher.tsx +++ b/components/locale-switcher.tsx @@ -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() diff --git a/components/translate-client-fragment.tsx b/components/translate-client-fragment.tsx new file mode 100644 index 0000000..58114a7 --- /dev/null +++ b/components/translate-client-fragment.tsx @@ -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 diff --git a/components/ui/form.tsx b/components/ui/form.tsx index 88b670b..5746543 100644 --- a/components/ui/form.tsx +++ b/components/ui/form.tsx @@ -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 - {!process.env.IS_SERVER_FLAG && typeof body === 'string' && - body.includes('schema.message') + {!env('IS_SERVER_FLAG') && typeof body === 'string' && body.match(/^(|\[")schema\./) ? : body}

) diff --git a/config/auth.ts b/config/auth.ts index 89fe762..614a0f1 100644 --- a/config/auth.ts +++ b/config/auth.ts @@ -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' @@ -61,7 +61,7 @@ export const { if (!token.sub) return token const existingUser = await getUserById(token.sub) - + if (!existingUser) return token token.role = existingUser.role diff --git a/config/layout.ts b/config/layout.ts deleted file mode 100644 index 12c53b5..0000000 --- a/config/layout.ts +++ /dev/null @@ -1 +0,0 @@ -export const bg: string = 'bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-sky-400 to-blue-800' \ No newline at end of file diff --git a/config/locales.ts b/config/locales.ts index af08af6..252f45a 100644 --- a/config/locales.ts +++ b/config/locales.ts @@ -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 } \ No newline at end of file +export { locales, defaultLocale, fallbackLocale, LC, importLocales, type loc } \ No newline at end of file diff --git a/config/mailer.ts b/config/mailer.ts index 35995ec..cb3bc17 100644 --- a/config/mailer.ts +++ b/config/mailer.ts @@ -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'), }, } diff --git a/config/routes.ts b/config/routes.ts index 3e61371..0df0958 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -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) } \ No newline at end of file diff --git a/config/validation.ts b/config/validation.ts index 8d41978..2baba01 100644 --- a/config/validation.ts +++ b/config/validation.ts @@ -1,2 +1,8 @@ export const MIN_PASSWORD_LENGTH: number = 6 -export const PASSWORD_SALT_LENGTH: number = 10 \ No newline at end of file +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])$` + diff --git a/data/user.ts b/data/user.ts index c230ad4..33ad80e 100644 --- a/data/user.ts +++ b/data/user.ts @@ -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 => { try { diff --git a/data/verification-token.ts b/data/verification-token.ts index f7ae4ca..5a73e22 100644 --- a/data/verification-token.ts +++ b/data/verification-token.ts @@ -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 => { try { return await db.verificationToken.findFirst({ where: { email } }) } catch { diff --git a/lib/db.ts b/lib/db.ts index d235046..786ce47 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -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 +} + +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 \ No newline at end of file diff --git a/lib/tokens.ts b/lib/tokens.ts index f609a21..01692d5 100644 --- a/lib/tokens.ts +++ b/lib/tokens.ts @@ -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) => { diff --git a/lib/translate.ts b/lib/translate.ts new file mode 100644 index 0000000..3f4ca4e --- /dev/null +++ b/lib/translate.ts @@ -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 +} diff --git a/lib/utils.ts b/lib/utils.ts index fb5a620..c581653 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -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) { + +} \ No newline at end of file diff --git a/locales/custom.en.ts b/locales/custom.en.ts new file mode 100644 index 0000000..630ca56 --- /dev/null +++ b/locales/custom.en.ts @@ -0,0 +1,10 @@ +export default { + single: 'I am the only one', + a: { + b: { + c: { + d: 'I am custom english {man}', + }, + }, + }, +} as const \ No newline at end of file diff --git a/locales/custom.uk.ts b/locales/custom.uk.ts new file mode 100644 index 0000000..805bef6 --- /dev/null +++ b/locales/custom.uk.ts @@ -0,0 +1,10 @@ +export default { + single: 'Я єдиний', + a: { + b: { + c: { + d: 'Я звичайний український {man}', + }, + }, + }, +} as const \ No newline at end of file diff --git a/locales/en.ts b/locales/en.ts index bb381d2..b38cb8b 100644 --- a/locales/en.ts +++ b/locales/en.ts @@ -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: { diff --git a/locales/uk.ts b/locales/uk.ts index bb4e343..e3edaf0 100644 --- a/locales/uk.ts +++ b/locales/uk.ts @@ -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: { diff --git a/middleware.ts b/middleware.ts index 77f5eda..5edced9 100644 --- a/middleware.ts +++ b/middleware.ts @@ -17,15 +17,12 @@ interface AppRouteHandlerFnContext { params?: Record; } -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)(.*)'], } diff --git a/package-lock.json b/package-lock.json index 7723ae7..a51cf8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index fcdd07b..cfbf508 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/schemas/index.ts b/schemas/index.ts index 231938c..fa34b6b 100644 --- a/schemas/index.ts +++ b/schemas/index.ts @@ -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: 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' }), +}) - 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' }), -}) \ No newline at end of file +// Password must contain at least a single lowercase, uppercase, digit and special character. \ No newline at end of file diff --git a/styles/LocaleSwitcher.module.scss b/styles/locale-switcher.module.scss similarity index 100% rename from styles/LocaleSwitcher.module.scss rename to styles/locale-switcher.module.scss