Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
andresgnlez authored and Andrés González committed Oct 16, 2024
1 parent 169efe9 commit 7be0fbd
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 37 deletions.
4 changes: 2 additions & 2 deletions api/src/modules/notifications/email/email.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { forwardRef, Module } from '@nestjs/common';
import { IEmailServiceToken } from '@api/modules/notifications/email/email-service.interface';
import { NodemailerEmailService } from '@api/modules/notifications/email/nodemailer.email.service';
import { AuthModule } from '@api/modules/auth/auth.module';
import { EmailFailedEventHandler } from '@api/modules/notifications/email/events/handlers/emai-failed-event.handler';
import { SendWelcomeEmailHandler } from '@api/modules/notifications/email/commands/handlers/send-welcome-email.handler';
import { SendEmailConfirmationHandler } from '@api/modules/notifications/email/commands/handlers/send-email-confirmation.handler';
import { EmailProviderFactory } from '@api/modules/notifications/email/email.provider';

@Module({
imports: [forwardRef(() => AuthModule)],
providers: [
{ provide: IEmailServiceToken, useClass: NodemailerEmailService },
EmailProviderFactory,
SendEmailConfirmationHandler,
SendWelcomeEmailHandler,
EmailFailedEventHandler,
Expand Down
17 changes: 17 additions & 0 deletions api/src/modules/notifications/email/email.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FactoryProvider } from '@nestjs/common';
import { IEmailServiceToken } from '@api/modules/notifications/email/email-service.interface';
import { MockEmailService } from '../../../../test/utils/mocks/mock-email.service';
import { NodemailerEmailService } from '@api/modules/notifications/email/nodemailer.email.service';
import { ApiConfigService } from '@api/modules/config/app-config.service';
import { EventBus } from '@nestjs/cqrs';

export const EmailProviderFactory: FactoryProvider = {
provide: IEmailServiceToken,
useFactory: (configService: ApiConfigService, eventBus: EventBus) => {
const env = configService.get('NODE_ENV');
return env === 'test'
? new MockEmailService()
: new NodemailerEmailService(eventBus, configService);
},
inject: [ApiConfigService],
};
19 changes: 14 additions & 5 deletions api/test/utils/mocks/mock-email.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { IEmailServiceInterface } from '@api/modules/notifications/email/email-service.interface';
import {
IEmailServiceInterface,
SendMailDTO,
} from '@api/modules/notifications/email/email-service.interface';
import { Logger } from '@nestjs/common';

export class MockEmailService implements IEmailServiceInterface {
logger: Logger = new Logger(MockEmailService.name);

sendMail = jest.fn(async (): Promise<void> => {
this.logger.log('Mock Email sent');
return Promise.resolve();
});
sendMail =
typeof jest !== 'undefined'
? jest.fn(async (sendMailDTO: SendMailDTO): Promise<void> => {
this.logger.log('Mock Email sent', this.constructor.name);
return Promise.resolve();
})
: async (sendMailDTO: SendMailDTO): Promise<void> => {
this.logger.log('Mock Email sent', this.constructor.name);
return Promise.resolve();
};
}
5 changes: 5 additions & 0 deletions client/src/app/auth/confirm-email/[token]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ConfirmEmailForm from "@/containers/auth/confirm-email/form";

export default function ConfirmEmailPage() {
return <ConfirmEmailForm />;
}
139 changes: 139 additions & 0 deletions client/src/containers/auth/confirm-email/form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client";

import { FC, FormEvent, useCallback, useRef } from "react";

import { useForm } from "react-hook-form";

import { useParams, useRouter, useSearchParams } from "next/navigation";

import { zodResolver } from "@hookform/resolvers/zod";
import { TOKEN_TYPE_ENUM } from "@shared/schemas/auth/token-type.schema";
import { RequestEmailUpdateSchema } from "@shared/schemas/users/request-email-update.schema";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";

import { client } from "@/lib/query-client";
import { queryKeys } from "@/lib/query-keys";

import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useApiResponseToast } from "@/components/ui/toast/use-api-response-toast";

const NewPasswordForm: FC = () => {
const router = useRouter();
const params = useParams<{ token: string }>();
const searchParams = useSearchParams();
const newEmail = searchParams.get("newEmail");

const formRef = useRef<HTMLFormElement>(null);
const form = useForm<z.infer<typeof RequestEmailUpdateSchema>>({
resolver: zodResolver(RequestEmailUpdateSchema),
defaultValues: {
newEmail: newEmail as NonNullable<typeof newEmail>,
},
});
const { apiResponseToast, toast } = useApiResponseToast();

const {
data: isValidToken,
isFetching,
isError,
} = useQuery({
queryKey: queryKeys.auth.confirmEmailToken(params.token).queryKey,
queryFn: () => {
return client.auth.validateToken.query({
headers: {
authorization: `Bearer ${params.token}`,
},
query: {
tokenType: TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION,
},
});
},
select: (data) => data.status === 200,
});

const handleEmailConfirmation = useCallback(
(evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();

form.handleSubmit(async (formValues) => {
try {
const { status, body } = await client.auth.confirmEmail.mutation({
body: formValues,
extraHeaders: {
authorization: `Bearer ${params.token}`,
},
});
apiResponseToast(
{ status, body },
{
successMessage: "Email updated successfully.",
},
);
router.push("/auth/signin");
} catch (err) {
toast({
variant: "destructive",
description: "Something went wrong",
});
}
})(evt);
},
[form, apiResponseToast, toast, params.token, router],
);

const isDisabled = isFetching || isError || !isValidToken;

return (
<div className="space-y-8 rounded-2xl py-6">
<div className="space-y-4 px-6">
<h2 className="text-xl font-semibold">Confirm email</h2>
{!isValidToken && (
<p className="text-sm text-destructive">
The token is invalid or has expired.
</p>
)}
</div>
<Form {...form}>
<form
ref={formRef}
className="w-full space-y-8"
onSubmit={handleEmailConfirmation}
>
<FormField
control={form.control}
name="newEmail"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="hidden" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-2 px-6">
<Button
variant="secondary"
type="submit"
className="w-full"
disabled={isDisabled}
>
Confirm email
</Button>
</div>
</form>
</Form>
</div>
);
};

export default NewPasswordForm;
Empty file.
18 changes: 10 additions & 8 deletions client/src/containers/profile/account-details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ const UpdateEmailForm: FC = () => {
},
},
{
// @ts-expect-error todo
select: (data) => data.body.data,
},
);

const form = useForm<z.infer<typeof accountDetailsSchema>>({
resolver: zodResolver(accountDetailsSchema),
defaultValues: {
// @ts-expect-error todo
name: user?.name,
// @ts-expect-error todo
role: user?.role,
},
mode: "onSubmit",
Expand All @@ -59,7 +62,6 @@ const UpdateEmailForm: FC = () => {
const parsed = accountDetailsSchema.safeParse(formData);

if (parsed.success) {
// todo: update method
const response = await client.user.updateMe.mutation({
params: {
id: session?.user?.id as string,
Expand All @@ -73,9 +75,9 @@ const UpdateEmailForm: FC = () => {
});

if (response.status === 200) {
updateSession(response.body);
await updateSession(response.body);

queryClient.invalidateQueries({
await queryClient.invalidateQueries({
queryKey: queryKeys.user.me(session?.user?.id as string).queryKey,
});

Expand All @@ -91,8 +93,8 @@ const UpdateEmailForm: FC = () => {
const handleEnterKey = useCallback(
(evt: KeyboardEvent) => {
if (evt.code === "Enter" && form.formState.isValid) {
form.handleSubmit(() => {
onSubmit(new FormData(formRef.current!));
form.handleSubmit(async () => {
await onSubmit(new FormData(formRef.current!));
})();
}
},
Expand All @@ -105,8 +107,8 @@ const UpdateEmailForm: FC = () => {
ref={formRef}
className="w-full space-y-4"
onSubmit={(evt) => {
form.handleSubmit(() => {
onSubmit(new FormData(formRef.current!));
form.handleSubmit(async () => {
await onSubmit(new FormData(formRef.current!));
})(evt);
}}
>
Expand Down Expand Up @@ -143,8 +145,8 @@ const UpdateEmailForm: FC = () => {
<div className="relative flex items-center">
<Input
type="text"
// @ts-expect-error todo
placeholder={user?.role}
value={user?.role}
{...field}
disabled
/>
Expand Down
11 changes: 5 additions & 6 deletions client/src/containers/profile/update-email/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ const UpdateEmailForm: FC = () => {
},
},
{
// @ts-expect-error todo
select: (data) => data.body.data,
},
);

const form = useForm<z.infer<typeof accountDetailsSchema>>({
resolver: zodResolver(accountDetailsSchema),
defaultValues: {
// @ts-expect-error todo
email: user?.email,
},
mode: "onSubmit",
Expand All @@ -58,13 +60,9 @@ const UpdateEmailForm: FC = () => {
const parsed = accountDetailsSchema.safeParse(formData);

if (parsed.success) {
// todo: update method
const response = await client.user.updateUser.mutation({
params: {
id: session?.user?.id as string,
},
const response = await client.user.requestEmailUpdate.mutation({
body: {
email: parsed.data.email,
newEmail: parsed.data.email,
},
extraHeaders: {
authorization: `Bearer ${session?.accessToken as string}`,
Expand Down Expand Up @@ -121,6 +119,7 @@ const UpdateEmailForm: FC = () => {
type="email"
autoComplete={field.name}
onKeyDown={handleEnterKey}
// @ts-expect-error todo
placeholder={user?.email}
className="w-full"
{...field}
Expand Down
1 change: 1 addition & 0 deletions client/src/lib/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {

export const authKeys = createQueryKeys("auth", {
resetPasswordToken: (token: string) => ["reset-password-token", token],
confirmEmailToken: (token: string) => ["confirm-email-token", token],
});

export const userKeys = createQueryKeys("user", {
Expand Down
54 changes: 54 additions & 0 deletions e2e/tests/auth/delete-account.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect, Page, test } from "@playwright/test";
import { E2eTestManager } from "@shared/lib/e2e-test-manager";
import { User } from "@shared/entities/users/user.entity";
import { ROLES } from "@shared/entities/users/roles.enum";

let testManager: E2eTestManager;
let page: Page;

test.describe.configure({ mode: "serial" });

test.describe("Auth - Delete Account", () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
testManager = await E2eTestManager.load(page);
});

test.beforeEach(async () => {
await testManager.clearDatabase();
});

test.afterEach(async () => {
// await testManager.clearDatabase();
});

test.afterAll(async () => {
await testManager.close();
});

test("an user deletes their account successfully", async () => {
const user: Pick<User, "email" | "password" | "partnerName" | "role"> = {
email: "[email protected]",
password: "12345678",
partnerName: "partner-test",
role: ROLES.ADMIN,
};

await testManager.mocks().createUser(user);
await testManager.login(user as User);

await page.waitForURL('/profile');

await page.getByRole('button', { name: 'Delete account' }).click();
await page.getByRole('button', { name: 'Delete account' }).click();

await page.waitForURL('/auth/signin');

await page.getByLabel("Email").fill(user.email);
await page.locator('input[type="password"]').fill(user.password);
await page.getByRole("button", { name: /log in/i }).click();


await expect(page.getByText('Invalid credentials')).toBeVisible();
});
});
Loading

0 comments on commit 7be0fbd

Please sign in to comment.