From bbf262a7510777ea91b87781275ceb546d4bed11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?= Date: Mon, 14 Oct 2024 18:33:11 +0200 Subject: [PATCH] WIP --- .../app/auth/confirm-email/[token]/page.tsx | 5 + .../auth/confirm-email/form/index.tsx | 139 ++++++++++++++++++ .../containers/auth/confirm-email/index.tsx | 0 .../containers/profile/update-email/index.tsx | 8 +- client/src/lib/query-keys.ts | 1 + e2e/tests/auth/update-password.spec.ts | 56 +++++++ shared/contracts/users.contract.ts | 6 +- 7 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 client/src/app/auth/confirm-email/[token]/page.tsx create mode 100644 client/src/containers/auth/confirm-email/form/index.tsx create mode 100644 client/src/containers/auth/confirm-email/index.tsx create mode 100644 e2e/tests/auth/update-password.spec.ts diff --git a/client/src/app/auth/confirm-email/[token]/page.tsx b/client/src/app/auth/confirm-email/[token]/page.tsx new file mode 100644 index 00000000..fbffb70b --- /dev/null +++ b/client/src/app/auth/confirm-email/[token]/page.tsx @@ -0,0 +1,5 @@ +import ConfirmEmailForm from "@/containers/auth/confirm-email/form"; + +export default function ConfirmEmailPage() { + return ; +} diff --git a/client/src/containers/auth/confirm-email/form/index.tsx b/client/src/containers/auth/confirm-email/form/index.tsx new file mode 100644 index 00000000..62a578ab --- /dev/null +++ b/client/src/containers/auth/confirm-email/form/index.tsx @@ -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(null); + const form = useForm>({ + resolver: zodResolver(RequestEmailUpdateSchema), + defaultValues: { + newEmail: newEmail as NonNullable, + }, + }); + 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) => { + 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 ( +
+
+

Confirm email

+ {!isValidToken && ( +

+ The token is invalid or has expired. +

+ )} +
+
+ + ( + + + + + + + )} + /> +
+ +
+ + +
+ ); +}; + +export default NewPasswordForm; diff --git a/client/src/containers/auth/confirm-email/index.tsx b/client/src/containers/auth/confirm-email/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/client/src/containers/profile/update-email/index.tsx b/client/src/containers/profile/update-email/index.tsx index cc5f0eca..8aca3c00 100644 --- a/client/src/containers/profile/update-email/index.tsx +++ b/client/src/containers/profile/update-email/index.tsx @@ -58,13 +58,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}`, diff --git a/client/src/lib/query-keys.ts b/client/src/lib/query-keys.ts index 429a7981..96daeb3f 100644 --- a/client/src/lib/query-keys.ts +++ b/client/src/lib/query-keys.ts @@ -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", { diff --git a/e2e/tests/auth/update-password.spec.ts b/e2e/tests/auth/update-password.spec.ts new file mode 100644 index 00000000..7f502bbb --- /dev/null +++ b/e2e/tests/auth/update-password.spec.ts @@ -0,0 +1,56 @@ +import { expect, Page, test } from "@playwright/test"; +import { E2eTestManager } from "@shared/lib/e2e-test-manager"; +import { User } from "@shared/entities/users/user.entity"; + +let testManager: E2eTestManager; +let page: Page; + +test.describe.configure({ mode: "serial" }); + +test.describe("Auth - Update Password process", () => { + 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 changes their password successfully", async () => { + const user: Pick = { + email: "jhondoe@test.com", + password: "12345678", + partnerName: "admin", + }; + const newPassword = "987654321987654321"; + + await testManager.mocks().createUser(user); + await testManager.login(user as User); + + await page.waitForURL('/profile'); + + await page.getByPlaceholder('Type your current password').fill(user.password); + await page.getByPlaceholder('Create new password').fill(newPassword); + await page.getByPlaceholder('Repeat new password').fill(newPassword); + + await page.getByRole("button", { name: /update password/i }).click(); + + // expect to see toast message + await page.waitForSelector("text=Your password has been updated successfully."); + + await page.getByRole("button", { name: /sign out/i }).click(); + + await testManager.login({ email: user.email, password: newPassword } as User); + + await expect(page).toHaveURL("/profile"); + }); +}); diff --git a/shared/contracts/users.contract.ts b/shared/contracts/users.contract.ts index fe2db709..f63d00f8 100644 --- a/shared/contracts/users.contract.ts +++ b/shared/contracts/users.contract.ts @@ -4,6 +4,7 @@ import { User } from "@shared/entities/users/user.entity"; import { UserDto } from "@shared/dtos/users/user.dto"; import { z } from "zod"; import { UpdateUserDto } from "@shared/dtos/users/update-user.dto"; +import { JSONAPIError } from '@shared/dtos/json-api.error'; import { ApiResponse } from "@shared/dtos/global/api-response.dto"; import { UpdateUserPasswordSchema } from "@shared/schemas/users/update-password.schema"; @@ -12,10 +13,11 @@ import { RequestEmailUpdateSchema } from "@shared/schemas/users/request-email-up const contract = initContract(); export const usersContract = contract.router({ findMe: { - method: "GET", - path: "/users/me", + method: 'GET', + path: '/users/me', responses: { 200: contract.type>(), + 401: contract.type(), }, query: generateEntityQuerySchema(User), },