diff --git a/apps/frontend/src/plugins/core/langs/en.json b/apps/frontend/src/plugins/core/langs/en.json index 236ce0182..711e29163 100644 --- a/apps/frontend/src/plugins/core/langs/en.json +++ b/apps/frontend/src/plugins/core/langs/en.json @@ -212,6 +212,10 @@ "error": { "title": "Invalid credentials", "desc": "The email address or password was incorrect. Please try again (make sure your caps lock is off)." + }, + "not_verified": { + "title": "Email is not verified", + "desc": "Please check your inbox and click the link to activate your account." } }, "sign_up": { diff --git a/packages/backend/src/core/sessions/confirm_email/send.confirm_email.service.ts b/packages/backend/src/core/sessions/confirm_email/send.confirm_email.service.ts index 37ef026ee..34baf57c8 100644 --- a/packages/backend/src/core/sessions/confirm_email/send.confirm_email.service.ts +++ b/packages/backend/src/core/sessions/confirm_email/send.confirm_email.service.ts @@ -59,6 +59,11 @@ export class SendConfirmEmailCoreSessionsService { // If user has confirm email, delete it // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (user.confirm_email?.id) { + // Skip if email was sent less than 10 minutes ago + if (user.confirm_email.created > new Date(Date.now() - 1000 * 60 * 10)) { + return; + } + await this.databaseService.db .delete(core_users_confirm_emails) .where(eq(core_users_confirm_emails.id, user.confirm_email.id)); diff --git a/packages/backend/src/core/sessions/sign_in/sign_in.service.ts b/packages/backend/src/core/sessions/sign_in/sign_in.service.ts index 0a25e06e1..03a4fa398 100644 --- a/packages/backend/src/core/sessions/sign_in/sign_in.service.ts +++ b/packages/backend/src/core/sessions/sign_in/sign_in.service.ts @@ -1,3 +1,4 @@ +import { EmailProvider, getConfigFile } from '@/providers'; import { InternalDatabaseService } from '@/utils/database/internal_database.service'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -8,6 +9,7 @@ import { core_admin_sessions } from '../../../database/schema/admins'; import { core_sessions } from '../../../database/schema/sessions'; import { AccessDeniedError, CustomError } from '../../../errors'; import { GqlContext } from '../../../utils'; +import { SendConfirmEmailCoreSessionsService } from '../confirm_email/send.confirm_email.service'; import { verifyPassword } from '../password'; import { DeviceSignInCoreSessionsService } from './device.service'; import { SignInCoreSessionsArgs } from './sign_in.dto'; @@ -27,6 +29,7 @@ export class SignInCoreSessionsService { private readonly jwtService: JwtService, private readonly configService: ConfigService, private readonly deviceService: DeviceSignInCoreSessionsService, + private readonly sendConfirmEmailCoreSessionsService: SendConfirmEmailCoreSessionsService, ) {} protected async createSession({ @@ -205,15 +208,34 @@ export class SignInCoreSessionsService { { admin, email: emailRaw, password, remember }: SignInCoreSessionsArgs, context: GqlContext, ) { + const { settings } = getConfigFile(); const email = emailRaw.toLowerCase(); const user = await this.databaseService.db.query.core_users.findFirst({ where: (table, { eq }) => eq(table.email, email), + with: { + confirm_email: true, + }, }); if (!user) throw new AccessDeniedError(); const validPassword = await verifyPassword(password, user.password); if (!validPassword) throw new AccessDeniedError(); + if ( + !user.email_verified && + settings.authorization.require_confirm_email && + settings.email.provider !== EmailProvider.none + ) { + await this.sendConfirmEmailCoreSessionsService.sendConfirmEmail({ + userId: user.id, + }); + + throw new CustomError({ + code: 'EMAIL_NOT_VERIFIED', + message: 'Email not verified', + }); + } + // If admin mode is enabled, check if user has access to admin cp if (admin) { const accessToAdminCP = diff --git a/packages/frontend/src/hooks/core/sign/up/use-sign-up-view.ts b/packages/frontend/src/hooks/core/sign/up/use-sign-up-view.ts index 097438071..ebd1069d3 100644 --- a/packages/frontend/src/hooks/core/sign/up/use-sign-up-view.ts +++ b/packages/frontend/src/hooks/core/sign/up/use-sign-up-view.ts @@ -85,6 +85,8 @@ export const useSignUpView = () => { shouldFocus: true, }, ); + + return; } else if (mutation.error === 'NAME_ALREADY_EXISTS') { form.setError( 'name', diff --git a/packages/frontend/src/views/theme/views/auth/sign/in/form/form-sign-in.tsx b/packages/frontend/src/views/theme/views/auth/sign/in/form/form-sign-in.tsx index efacf663a..069a00e2b 100644 --- a/packages/frontend/src/views/theme/views/auth/sign/in/form/form-sign-in.tsx +++ b/packages/frontend/src/views/theme/views/auth/sign/in/form/form-sign-in.tsx @@ -9,7 +9,7 @@ import { import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { useSignInView } from '@/hooks/core/sign/in/use-sign-in-view'; -import { AlertCircle, LogIn } from 'lucide-react'; +import { AlertCircle, LogIn, MailQuestion } from 'lucide-react'; import { useTranslations } from 'next-intl'; import React from 'react'; @@ -21,15 +21,21 @@ export const FormSignIn = () => { <> {error && (
- {error === 'ACCESS_DENIED' && ( + {error === 'EMAIL_NOT_VERIFIED' ? ( + + + {t('sign_in.not_verified.title')} + + {t('sign_in.not_verified.desc')} + + + ) : error === 'ACCESS_DENIED' ? ( {t('sign_in.error.title')} {t('sign_in.error.desc')} - )} - - {error !== 'ACCESS_DENIED' && ( + ) : ( {t('errors.title')} diff --git a/packages/frontend/src/views/theme/views/auth/sign/up/confirm-email/confirm-email-sign-up-view.tsx b/packages/frontend/src/views/theme/views/auth/sign/up/confirm-email/confirm-email-sign-up-view.tsx index 43759547f..9e850c398 100644 --- a/packages/frontend/src/views/theme/views/auth/sign/up/confirm-email/confirm-email-sign-up-view.tsx +++ b/packages/frontend/src/views/theme/views/auth/sign/up/confirm-email/confirm-email-sign-up-view.tsx @@ -36,7 +36,7 @@ export const ConfirmEmailSignUpView = async ({ }) => { const t = await getTranslations('core.sign_up.confirm_email'); if (!userId || !token) { - redirect('/'); + redirect('/login'); return; } @@ -47,7 +47,7 @@ export const ConfirmEmailSignUpView = async ({ userId: +userId, }); } catch (_e) { - redirect('/'); + redirect('/login'); } return (