Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Send verification email when user trying to sign in without confirm_email #537

Merged
merged 1 commit into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/frontend/src/plugins/core/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
22 changes: 22 additions & 0 deletions packages/backend/src/core/sessions/sign_in/sign_in.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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({
Expand Down Expand Up @@ -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 =
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/hooks/core/sign/up/use-sign-up-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export const useSignUpView = () => {
shouldFocus: true,
},
);

return;
} else if (mutation.error === 'NAME_ALREADY_EXISTS') {
form.setError(
'name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -21,15 +21,21 @@ export const FormSignIn = () => {
<>
{error && (
<div className="mb-6 space-y-4">
{error === 'ACCESS_DENIED' && (
{error === 'EMAIL_NOT_VERIFIED' ? (
<Alert variant="warn">
<MailQuestion className="size-4" />
<AlertTitle>{t('sign_in.not_verified.title')}</AlertTitle>
<AlertDescription>
{t('sign_in.not_verified.desc')}
</AlertDescription>
</Alert>
) : error === 'ACCESS_DENIED' ? (
<Alert variant="error">
<AlertCircle className="size-4" />
<AlertTitle>{t('sign_in.error.title')}</AlertTitle>
<AlertDescription>{t('sign_in.error.desc')}</AlertDescription>
</Alert>
)}

{error !== 'ACCESS_DENIED' && (
) : (
<Alert variant="error">
<AlertCircle className="size-4" />
<AlertTitle>{t('errors.title')}</AlertTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const ConfirmEmailSignUpView = async ({
}) => {
const t = await getTranslations('core.sign_up.confirm_email');
if (!userId || !token) {
redirect('/');
redirect('/login');

return;
}
Expand All @@ -47,7 +47,7 @@ export const ConfirmEmailSignUpView = async ({
userId: +userId,
});
} catch (_e) {
redirect('/');
redirect('/login');
}

return (
Expand Down
Loading