Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
andresgnlez committed Oct 14, 2024
1 parent 8f884ee commit bbf262a
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 8 deletions.
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.
8 changes: 2 additions & 6 deletions client/src/containers/profile/update-email/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
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
56 changes: 56 additions & 0 deletions e2e/tests/auth/update-password.spec.ts
Original file line number Diff line number Diff line change
@@ -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<User, "email" | "password" | "partnerName"> = {
email: "[email protected]",
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");
});
});
6 changes: 4 additions & 2 deletions shared/contracts/users.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<ApiResponse<UserDto>>(),
401: contract.type<JSONAPIError>(),
},
query: generateEntityQuerySchema(User),
},
Expand Down

0 comments on commit bbf262a

Please sign in to comment.