Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eab20aa
Added Gmail messenger
IhorMasechko Apr 7, 2025
a384c93
Merged with main branch
IhorMasechko Apr 7, 2025
f98d561
Merge branch 'main' into 142-if-user-is-verified-after-registration-p…
IhorMasechko Apr 8, 2025
805fe25
Added mutation for the isVerified field
IhorMasechko Apr 8, 2025
2f6e49a
Added a toaster and the option to log in during registration
IhorMasechko Apr 9, 2025
b4b3114
Merge branch 'main' into 142-if-user-is-verified-after-registration-p…
IhorMasechko Apr 9, 2025
00078a2
Fixed jest test errors
IhorMasechko Apr 9, 2025
69dc92b
Added jest test for the new code
IhorMasechko Apr 10, 2025
9a696e5
Fixed test code
IhorMasechko Apr 10, 2025
43cab36
Fixed lint error
IhorMasechko Apr 10, 2025
0ff185b
Fixed Security Hotspots with password in test file
IhorMasechko Apr 10, 2025
e608ea1
Fixed password error in jest tests
IhorMasechko Apr 10, 2025
82784e3
Fixed potentially hard-coded password
IhorMasechko Apr 10, 2025
bdbb985
Avoid duplication code
IhorMasechko Apr 10, 2025
cdedd9c
Fixed issues from Sonar
IhorMasechko Apr 10, 2025
8c2f28d
Fixed Unexpected constant nullishness on the left-hand side of a exp…
IhorMasechko Apr 10, 2025
767034c
Fix cross-site cookie issue for Keystone auth (SameSite/secure config)
IhorMasechko Apr 12, 2025
1372736
Changed placeholder for the email verification component
IhorMasechko Apr 15, 2025
8e10489
Merge branch 'main' into 142-if-user-is-verified-after-registration-p…
IhorMasechko Apr 15, 2025
7fff76a
Fix keystone session
IhorMasechko Apr 15, 2025
b2b3b58
Fix statelessSessions
IhorMasechko Apr 15, 2025
4e3df4a
Set isSecureEnv for the session
IhorMasechko Apr 15, 2025
20a03aa
Merge branch 'main' into 142-if-user-is-verified-after-registration-p…
IhorMasechko Apr 16, 2025
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
67 changes: 27 additions & 40 deletions veterans/app/api/sendEmail/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
jest.mock('nodemailer', () => ({
createTransport: jest.fn().mockReturnValue({
sendMail: jest.fn().mockResolvedValue({ messageId: 'test-id' }),
}),
}));

jest.mock('next/server', () => ({
NextResponse: {
json: jest.fn().mockImplementation((body, { status }) => ({
status,
json: async () => body,
})),
json: jest.fn().mockImplementation((body, { status }) => {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
});
}),
},
}));

Expand All @@ -14,12 +22,6 @@ describe('POST /api/sendEmail', () => {
});

it('should return error if "to" is missing', async () => {
jest.doMock('resend', () => ({
Resend: jest.fn(() => ({
emails: { send: jest.fn() },
})),
}));

const { POST } = await import('./route');
const req = {
json: jest.fn().mockResolvedValue({ to: '', message: 'Hello' }),
Expand All @@ -33,12 +35,6 @@ describe('POST /api/sendEmail', () => {
});

it('should send an email successfully and return a success response', async () => {
jest.doMock('resend', () => ({
Resend: jest.fn(() => ({
emails: { send: jest.fn().mockResolvedValue({ status: 'success' }) },
})),
}));

const { POST } = await import('./route');
const req = {
json: jest
Expand All @@ -51,35 +47,26 @@ describe('POST /api/sendEmail', () => {

expect(response.status).toBe(200);
expect(jsonResponse.message).toBe('Email sent successfully!');
expect(jsonResponse.response).toHaveProperty('messageId', 'test-id');
});

describe('Error Handling', () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
it('should return a 500 error if sending email fails', async () => {
const nodemailer = await import('nodemailer');
(nodemailer.createTransport as jest.Mock).mockReturnValueOnce({
sendMail: jest.fn().mockRejectedValue(new Error('Test error message')),
});

it('should return a 500 error if sending email fails', async () => {
jest.doMock('resend', () => ({
Resend: jest.fn(() => ({
emails: {
send: jest.fn().mockRejectedValue(new Error('Test error message')),
},
})),
}));

const { POST } = await import('./route');
const req = {
json: jest
.fn()
.mockResolvedValue({ to: 'test@example.com', message: 'Hello' }),
} as unknown as Request;
const { POST } = await import('./route');
const req = {
json: jest
.fn()
.mockResolvedValue({ to: 'test@example.com', message: 'Hello' }),
} as unknown as Request;

const response = await POST(req);
const jsonResponse = await response.json();
const response = await POST(req);
const jsonResponse = await response.json();

expect(response.status).toBe(500);
expect(jsonResponse.error).toBe('Test error message');
});
expect(response.status).toBe(500);
expect(jsonResponse.error).toBe('Test error message');
});
});
26 changes: 18 additions & 8 deletions veterans/app/api/sendEmail/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { NextResponse } from 'next/server';
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);
import nodemailer from 'nodemailer';

export async function POST(req: Request) {
const { to, message } = await req.json();
Expand All @@ -14,13 +12,25 @@ export async function POST(req: Request) {
}

try {
const response = await resend.emails.send({
from: 'RAZOM <onboarding@resend.dev>',
to: [to],
subject: 'Your Verification Code',
text: message,
const transporter = nodemailer.createTransport({
service: 'gmail',
port: 465,
secure: true,
auth: {
user: process.env.GMAIL_USER,
pass: process.env.GMAIL_PASSWORD,
},
});

const mailOptions = {
from: process.env.GMAIL_USER,
to,
subject: 'Verify Your Email Address',
text: message,
};

const response = await transporter.sendMail(mailOptions);

return NextResponse.json(
{ message: 'Email sent successfully!', response },
{ status: 200 },
Expand Down
2 changes: 2 additions & 0 deletions veterans/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Toaster } from 'react-hot-toast';
import { Lato, Golos_Text } from 'next/font/google';

import 'styles/global.css';
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function RootLayout({
<Banner name="logotype" height={34} />
</HeaderContent>
</Header>
<Toaster position="top-center" />
<main>{children}</main>
<Footer>
<FooterContent>
Expand Down
7 changes: 7 additions & 0 deletions veterans/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ const { withAuth } = createAuth({

const sessionMaxAge = 60 * 60 * 24 * 30;

const isSecureEnv =
process.env.NODE_ENV === 'production' ||
process.env.VERCEL_ENV === 'preview' ||
process.env.VERCEL_ENV === 'staging';

const session = statelessSessions({
maxAge: sessionMaxAge,
secret: process.env.SESSION_SECRET,
sameSite: isSecureEnv ? 'none' : 'lax',
secure: isSecureEnv,
});

export { withAuth, session };
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useMutation, useQuery } from '@apollo/client';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import EmailVerification from './EmailVerification';

jest.mock('@apollo/client');
jest.mock('next/navigation');
jest.mock('react-hot-toast', () => ({
toast: {
error: jest.fn(),
},
}));

describe('EmailVerification', () => {
const mockRouterPush = jest.fn();
const mockUseQuery = useQuery as jest.Mock;
const mockUseMutation = useMutation as jest.Mock;

beforeEach(() => {
jest.clearAllMocks();

(useRouter as jest.Mock).mockReturnValue({
push: mockRouterPush,
});

mockUseQuery.mockReturnValue({
data: { user: { id: '1' } },
});

mockUseMutation.mockReturnValue([jest.fn()]);
});

it('renders the component correctly', () => {
render(
<EmailVerification verificationCode="1234" email="test@example.com" />,
);

expect(
screen.getByPlaceholderText('Verification code'),
).toBeInTheDocument();
expect(screen.getByText('Verify your email address')).toBeInTheDocument();
});

it('handles invalid verification code', async () => {
render(
<EmailVerification verificationCode="1234" email="test@example.com" />,
);

fireEvent.change(screen.getByPlaceholderText('Verification code'), {
target: { value: 'wrong-code' },
});

fireEvent.click(screen.getByText('Ok'));

await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith('Invalid verification code'),
);
});

it('successfully verifies the email and redirects', async () => {
const mockUpdateUserVerification = jest.fn().mockResolvedValue({
data: {
updateUser: { isVerified: true },
},
});
mockUseMutation.mockReturnValue([mockUpdateUserVerification]);

render(
<EmailVerification verificationCode="1234" email="test@example.com" />,
);

fireEvent.change(screen.getByPlaceholderText('Verification code'), {
target: { value: '1234' },
});

fireEvent.click(screen.getByText('Ok'));

await waitFor(() =>
expect(mockUpdateUserVerification).toHaveBeenCalledWith({
variables: { id: '1', isVerified: true },
}),
);

expect(mockRouterPush).toHaveBeenCalledWith('/');
});

it('handles failed verification', async () => {
const mockUpdateUserVerification = jest.fn().mockResolvedValue({
data: {
updateUser: { isVerified: false },
},
});
mockUseMutation.mockReturnValue([mockUpdateUserVerification]);

render(
<EmailVerification verificationCode="1234" email="test@example.com" />,
);

fireEvent.change(screen.getByPlaceholderText('Verification code'), {
target: { value: '1234' },
});

fireEvent.click(screen.getByText('Ok'));

await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith('Verification failed'),
);
});

it('handles mutation error', async () => {
const mockUpdateUserVerification = jest
.fn()
.mockRejectedValue(new Error('Network error'));
mockUseMutation.mockReturnValue([mockUpdateUserVerification]);

render(
<EmailVerification verificationCode="1234" email="test@example.com" />,
);

fireEvent.change(screen.getByPlaceholderText('Verification code'), {
target: { value: '1234' },
});

fireEvent.click(screen.getByText('Ok'));

await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith('Network error'),
);
});
});
Original file line number Diff line number Diff line change
@@ -1,23 +1,54 @@
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useMutation, useQuery } from '@apollo/client';
import { useRouter } from 'next/navigation';
import st from '@comComps/authForm/AuthForm.module.css';
import { VERIFICATION_MUTATION, CHECK_USER_QUERY } from 'constants/graphql';

interface EmailVerificationProps {
readonly verificationCode: string;
readonly email: string;
}

export default function EmailVerification({
verificationCode,
email,
}: EmailVerificationProps) {
const [confirmedCode, setConfirmedCode] = useState<string>('');
const [updateUserVerification] = useMutation(VERIFICATION_MUTATION);
const router = useRouter();
const { data: userData } = useQuery(CHECK_USER_QUERY, {
variables: { email },
});

const handleVerifyCode = () => {
if (confirmedCode === verificationCode) {
router.push('/login');
} else {
// eslint-disable-next-line no-alert
alert('Invalid verification code');
const handleVerifyCode = async () => {
if (confirmedCode !== verificationCode) {
toast.error('Invalid verification code');
return;
}

const userId = userData.user.id;
Comment thread
IhorMasechko marked this conversation as resolved.

try {
const { data } = await updateUserVerification({
variables: {
id: userId,
isVerified: true,
},
});

if (data?.updateUser?.isVerified) {
router.push('/');
} else {
toast.error('Verification failed');
}
} catch (error: unknown) {
let message = 'Something went wrong during verification';

if (error instanceof Error) {
message = error.message;
}
Comment thread
IhorMasechko marked this conversation as resolved.
toast.error(message);
}
};

Expand All @@ -28,7 +59,7 @@ export default function EmailVerification({
<input
type="text"
id="confirmEmail"
placeholder="Email verification"
placeholder="Verification code"
className={st.input}
style={{ textAlign: 'center' }}
value={confirmedCode}
Expand Down
Loading