From f4e6a307f2663a3029883f4566e1c11778f17134 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 28 May 2023 20:05:00 -0400 Subject: [PATCH 1/4] feat: implement change password --- next.config.js | 1 + package-lock.json | 77 +++++++++++++++++++ package.json | 2 + src/components/Account/Security.tsx | 77 +++++++++++++++++++ src/pages/account/index.tsx | 6 +- src/pages/api/users/edit-password.ts | 60 +++++++++++++++ .../User/sendUpdatePasswordRequest.ts | 33 ++++++++ src/services/User/createNewUser.ts | 1 + .../schema/CreateUserValidationSchemas.ts | 8 ++ src/services/User/schema/GetUserSchema.ts | 2 +- 10 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 src/components/Account/Security.tsx create mode 100644 src/pages/api/users/edit-password.ts create mode 100644 src/requests/User/sendUpdatePasswordRequest.ts diff --git a/next.config.js b/next.config.js index 39d2b47..35d5fb2 100644 --- a/next.config.js +++ b/next.config.js @@ -4,6 +4,7 @@ const nextConfig = { images: { domains: ['picsum.photos', 'res.cloudinary.com'], }, + swcMinify: true, }; const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', diff --git a/package-lock.json b/package-lock.json index 1cfce31..567b219 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "multer-storage-cloudinary": "^4.0.0", "next": "^13.3.4", "next-connect": "^1.0.0-next.3", + "onchange": "^7.1.0", "passport": "^0.6.0", "passport-local": "^1.0.0", "pino": "^8.12.0", @@ -202,6 +203,16 @@ "node": ">=6.9.0" } }, + "node_modules/@blakeembrey/deque": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@blakeembrey/deque/-/deque-1.0.5.tgz", + "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==" + }, + "node_modules/@blakeembrey/template": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@blakeembrey/template/-/template-1.1.0.tgz", + "integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -8288,6 +8299,28 @@ "wrappy": "1" } }, + "node_modules/onchange": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz", + "integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==", + "dependencies": { + "@blakeembrey/deque": "^1.0.5", + "@blakeembrey/template": "^1.0.0", + "arg": "^4.1.3", + "chokidar": "^3.3.1", + "cross-spawn": "^7.0.1", + "ignore": "^5.1.4", + "tree-kill": "^1.2.2" + }, + "bin": { + "onchange": "dist/bin.js" + } + }, + "node_modules/onchange/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -10741,6 +10774,14 @@ "node": "*" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tree-node-cli": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz", @@ -11669,6 +11710,16 @@ "regenerator-runtime": "^0.13.11" } }, + "@blakeembrey/deque": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@blakeembrey/deque/-/deque-1.0.5.tgz", + "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==" + }, + "@blakeembrey/template": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@blakeembrey/template/-/template-1.1.0.tgz", + "integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==" + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -17455,6 +17506,27 @@ "wrappy": "1" } }, + "onchange": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz", + "integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==", + "requires": { + "@blakeembrey/deque": "^1.0.5", + "@blakeembrey/template": "^1.0.0", + "arg": "^4.1.3", + "chokidar": "^3.3.1", + "cross-spawn": "^7.0.1", + "ignore": "^5.1.4", + "tree-kill": "^1.2.2" + }, + "dependencies": { + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + } + } + }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -19181,6 +19253,11 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + }, "tree-node-cli": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz", diff --git a/package.json b/package.json index ae633c8..a5bade0 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "next start", "lint": "next lint", "format": "npx prettier . --write", + "format-watch": "npx onchange \"**/*\" -- prettier --write --ignore-unknown {{changed}}", "seed": "npx --max-old-space-size=4096 ts-node ./src/prisma/seed/index.ts" }, "dependencies": { @@ -80,6 +81,7 @@ "prettier": "^2.8.8", "prettier-plugin-jsdoc": "^0.4.2", "prettier-plugin-tailwindcss": "^0.2.8", + "onchange": "^7.1.0", "prisma": "^4.13.0", "tailwindcss": "^3.3.2", "tailwindcss-animate": "^1.0.5", diff --git a/src/components/Account/Security.tsx b/src/components/Account/Security.tsx new file mode 100644 index 0000000..283b95a --- /dev/null +++ b/src/components/Account/Security.tsx @@ -0,0 +1,77 @@ +import { Switch } from '@headlessui/react'; +import { FunctionComponent, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; +import sendUpdatePasswordRequest from '@/requests/User/sendUpdatePasswordRequest'; +import FormError from '../ui/forms/FormError'; +import FormInfo from '../ui/forms/FormInfo'; +import FormLabel from '../ui/forms/FormLabel'; +import FormTextInput from '../ui/forms/FormTextInput'; + +const Security: FunctionComponent = () => { + const [editToggled, setEditToggled] = useState(false); + const { register, handleSubmit, formState, reset } = useForm< + z.infer + >({ + resolver: zodResolver(UpdatePasswordSchema), + }); + + const onSubmit: SubmitHandler> = async (data) => { + await sendUpdatePasswordRequest(data) + + reset(); + }; + + return ( +
+
+
+

Change Your Password

+

Update your password to maintain the safety of your account.

+
+
+ setEditToggled((val) => !val)} + /> +
+
+ {editToggled && ( +
+ + New Password + {formState.errors.password?.message} + + + + Confirm Password + {formState.errors.confirmPassword?.message} + + + + + + )} +
+ ); +}; + +export default Security; diff --git a/src/pages/account/index.tsx b/src/pages/account/index.tsx index eb45097..05dacfd 100644 --- a/src/pages/account/index.tsx +++ b/src/pages/account/index.tsx @@ -6,6 +6,7 @@ import Head from 'next/head'; import AccountInfo from '@/components/Account/AccountInfo'; import { useContext } from 'react'; import UserContext from '@/contexts/UserContext'; +import Security from '@/components/Account/Security'; const AccountPage: NextPage = () => { const { user } = useContext(UserContext); @@ -50,7 +51,10 @@ const AccountPage: NextPage = () => { - Content 3 + + + + Your posts! diff --git a/src/pages/api/users/edit-password.ts b/src/pages/api/users/edit-password.ts new file mode 100644 index 0000000..5f29441 --- /dev/null +++ b/src/pages/api/users/edit-password.ts @@ -0,0 +1,60 @@ +import { hashPassword } from '@/config/auth/passwordFns'; +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import DBClient from '@/prisma/DBClient'; +import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; +import GetUserSchema from '@/services/User/schema/GetUserSchema'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +interface UpdatePasswordRequest extends UserExtendedNextApiRequest { + body: z.infer; +} + +const updatePassword = async ( + req: UpdatePasswordRequest, + res: NextApiResponse>, +) => { + const { password } = req.body; + const hash = await hashPassword(password); + + const user = req.user!; + const updatedUser: z.infer = await DBClient.instance.user.update({ + data: { hash }, + where: { id: user.id }, + select: { + id: true, + username: true, + createdAt: true, + updatedAt: true, + email: true, + firstName: true, + lastName: true, + dateOfBirth: true, + accountIsVerified: true, + }, + }); + res.json({ + message: 'Updated user password.', + statusCode: 200, + success: true, + payload: updatedUser, + }); +}; +const router = createRouter< + UpdatePasswordRequest, + NextApiResponse> +>(); + +router.put( + validateRequest({ bodySchema: UpdatePasswordSchema }), + getCurrentUser, + updatePassword, +); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/requests/User/sendUpdatePasswordRequest.ts b/src/requests/User/sendUpdatePasswordRequest.ts new file mode 100644 index 0000000..1c17ef6 --- /dev/null +++ b/src/requests/User/sendUpdatePasswordRequest.ts @@ -0,0 +1,33 @@ +import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; +import GetUserSchema from '@/services/User/schema/GetUserSchema'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; + +const sendUpdatePasswordRequest = async (data: z.infer) => { + const response = await fetch('/api/users/edit-password', { + body: JSON.stringify(data), + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('API response validation failed.'); + } + + const parsedPayload = GetUserSchema.safeParse(parsed.data.payload); + + if (!parsedPayload.success) { + throw new Error('API payload validation failed.'); + } + + return parsedPayload.data; +}; + +export default sendUpdatePasswordRequest; diff --git a/src/services/User/createNewUser.ts b/src/services/User/createNewUser.ts index 62e18e5..b07124c 100644 --- a/src/services/User/createNewUser.ts +++ b/src/services/User/createNewUser.ts @@ -31,6 +31,7 @@ const createNewUser = async ({ dateOfBirth: true, createdAt: true, accountIsVerified: true, + updatedAt: true, }, }); diff --git a/src/services/User/schema/CreateUserValidationSchemas.ts b/src/services/User/schema/CreateUserValidationSchemas.ts index f9b421d..485edcc 100644 --- a/src/services/User/schema/CreateUserValidationSchemas.ts +++ b/src/services/User/schema/CreateUserValidationSchemas.ts @@ -69,3 +69,11 @@ export const CreateUserValidationSchemaWithUsernameAndEmailCheck = message: 'Passwords do not match.', path: ['confirmPassword'], }); + +export const UpdatePasswordSchema = BaseCreateUserSchema.pick({ + password: true, + confirmPassword: true, +}).refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match.', + path: ['confirmPassword'], +}); diff --git a/src/services/User/schema/GetUserSchema.ts b/src/services/User/schema/GetUserSchema.ts index 5e1df4e..aa625c6 100644 --- a/src/services/User/schema/GetUserSchema.ts +++ b/src/services/User/schema/GetUserSchema.ts @@ -4,7 +4,7 @@ const GetUserSchema = z.object({ id: z.string().uuid(), username: z.string(), createdAt: z.coerce.date(), - updatedAt: z.coerce.date().optional(), + updatedAt: z.coerce.date().nullable(), email: z.string().email(), firstName: z.string(), lastName: z.string(), From 2b026b7e5f24b571b11d8947b61d5aa12ecb0ff7 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 28 May 2023 20:05:49 -0400 Subject: [PATCH 2/4] format: format codebase to prettier schema --- package-lock.json | 26 +++++++++++++------ package.json | 1 - src/components/Account/Security.tsx | 2 +- .../BeerById/BeerPostCommentsSection.tsx | 4 +-- .../BeerById/BeerRecommendations.tsx | 13 +++++----- .../BreweryById/BreweryBeerSection.tsx | 13 +++++----- src/components/ui/CommentsComponent.tsx | 14 +++++----- src/hooks/utilities/useMediaQuery.ts | 9 +++---- 8 files changed, 43 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 567b219..c5e2f87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "multer-storage-cloudinary": "^4.0.0", "next": "^13.3.4", "next-connect": "^1.0.0-next.3", - "onchange": "^7.1.0", "passport": "^0.6.0", "passport-local": "^1.0.0", "pino": "^8.12.0", @@ -74,6 +73,7 @@ "eslint-config-next": "^13.3.4", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.32.2", + "onchange": "^7.1.0", "postcss": "^8.4.23", "prettier": "^2.8.8", "prettier-plugin-jsdoc": "^0.4.2", @@ -206,12 +206,14 @@ "node_modules/@blakeembrey/deque": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@blakeembrey/deque/-/deque-1.0.5.tgz", - "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==" + "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==", + "dev": true }, "node_modules/@blakeembrey/template": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@blakeembrey/template/-/template-1.1.0.tgz", - "integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==" + "integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==", + "dev": true }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -8303,6 +8305,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz", "integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==", + "dev": true, "dependencies": { "@blakeembrey/deque": "^1.0.5", "@blakeembrey/template": "^1.0.0", @@ -8319,7 +8322,8 @@ "node_modules/onchange/node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true }, "node_modules/onetime": { "version": "5.1.2", @@ -10778,6 +10782,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, "bin": { "tree-kill": "cli.js" } @@ -11713,12 +11718,14 @@ "@blakeembrey/deque": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@blakeembrey/deque/-/deque-1.0.5.tgz", - "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==" + "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==", + "dev": true }, "@blakeembrey/template": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@blakeembrey/template/-/template-1.1.0.tgz", - "integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==" + "integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==", + "dev": true }, "@cspotcode/source-map-support": { "version": "0.8.1", @@ -17510,6 +17517,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz", "integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==", + "dev": true, "requires": { "@blakeembrey/deque": "^1.0.5", "@blakeembrey/template": "^1.0.0", @@ -17523,7 +17531,8 @@ "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true } } }, @@ -19256,7 +19265,8 @@ "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true }, "tree-node-cli": { "version": "1.6.0", diff --git a/package.json b/package.json index a5bade0..91d1918 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "devDependencies": { "@faker-js/faker": "^7.6.0", "@types/cookie": "^0.5.1", - "@types/ejs": "^3.1.2", "@types/jsonwebtoken": "^9.0.2", "@types/lodash": "^4.14.194", "@types/mapbox__mapbox-sdk": "^0.13.4", diff --git a/src/components/Account/Security.tsx b/src/components/Account/Security.tsx index 283b95a..1fc4991 100644 --- a/src/components/Account/Security.tsx +++ b/src/components/Account/Security.tsx @@ -19,7 +19,7 @@ const Security: FunctionComponent = () => { }); const onSubmit: SubmitHandler> = async (data) => { - await sendUpdatePasswordRequest(data) + await sendUpdatePasswordRequest(data); reset(); }; diff --git a/src/components/BeerById/BeerPostCommentsSection.tsx b/src/components/BeerById/BeerPostCommentsSection.tsx index f0831e8..1329d5b 100644 --- a/src/components/BeerById/BeerPostCommentsSection.tsx +++ b/src/components/BeerById/BeerPostCommentsSection.tsx @@ -67,8 +67,8 @@ const BeerPostCommentsSection: FC = ({ beerPost }) { /** - * If the comments are loading, show a loading component. Otherwise, show - * the comments. + * If the comments are loading, show a loading component. Otherwise, show the + * comments. */ isLoading ? (
diff --git a/src/components/BeerById/BeerRecommendations.tsx b/src/components/BeerById/BeerRecommendations.tsx index 3f053b9..5be84b4 100644 --- a/src/components/BeerById/BeerRecommendations.tsx +++ b/src/components/BeerById/BeerRecommendations.tsx @@ -19,8 +19,8 @@ const BeerRecommendationsSection: FC<{ const { ref: penultimateBeerPostRef } = useInView({ /** - * When the last beer post comes into view, call setSize from - * useBeerPostsByBrewery to load more beer posts. + * When the last beer post comes into view, call setSize from useBeerPostsByBrewery to + * load more beer posts. */ onChange: (visible) => { if (!visible || isAtEnd) return; @@ -46,9 +46,8 @@ const BeerRecommendationsSection: FC<{ const isPenultimateBeerPost = index === beerPosts.length - 2; /** - * Attach a ref to the second last beer post in the list. - * When it comes into view, the component will call - * setSize to load more beer posts. + * Attach a ref to the second last beer post in the list. When it comes + * into view, the component will call setSize to load more beer posts. */ return ( @@ -86,8 +85,8 @@ const BeerRecommendationsSection: FC<{ { /** - * If there are more beer posts to load, show a loading component - * with a skeleton loader and a loading spinner. + * If there are more beer posts to load, show a loading component with a + * skeleton loader and a loading spinner. */ !!isLoadingMore && !isAtEnd && ( diff --git a/src/components/BreweryById/BreweryBeerSection.tsx b/src/components/BreweryById/BreweryBeerSection.tsx index 9416383..5400277 100644 --- a/src/components/BreweryById/BreweryBeerSection.tsx +++ b/src/components/BreweryById/BreweryBeerSection.tsx @@ -22,8 +22,8 @@ const BreweryBeersSection: FC = ({ breweryPost }) = }); const { ref: penultimateBeerPostRef } = useInView({ /** - * When the last beer post comes into view, call setSize from - * useBeerPostsByBrewery to load more beer posts. + * When the last beer post comes into view, call setSize from useBeerPostsByBrewery to + * load more beer posts. */ onChange: (visible) => { if (!visible || isAtEnd) return; @@ -60,9 +60,8 @@ const BreweryBeersSection: FC = ({ breweryPost }) = const isPenultimateBeerPost = index === beerPosts.length - 2; /** - * Attach a ref to the second last beer post in the list. - * When it comes into view, the component will call - * setSize to load more beer posts. + * Attach a ref to the second last beer post in the list. When it comes + * into view, the component will call setSize to load more beer posts. */ return ( @@ -91,8 +90,8 @@ const BreweryBeersSection: FC = ({ breweryPost }) = { /** - * If there are more beer posts to load, show a loading component - * with a skeleton loader and a loading spinner. + * If there are more beer posts to load, show a loading component with a + * skeleton loader and a loading spinner. */ !!isLoadingMore && !isAtEnd && ( diff --git a/src/components/ui/CommentsComponent.tsx b/src/components/ui/CommentsComponent.tsx index 9a1d3be..fb08e56 100644 --- a/src/components/ui/CommentsComponent.tsx +++ b/src/components/ui/CommentsComponent.tsx @@ -68,9 +68,8 @@ const CommentsComponent: FC = ({ const isLastComment = index === comments.length - 1; /** - * Attach a ref to the last comment in the list. When it comes - * into view, the component will call setSize to load more - * comments. + * Attach a ref to the last comment in the list. When it comes into view, the + * component will call setSize to load more comments. */ return (
= ({ { /** - * If there are more comments to load, show a loading component - * with a skeleton loader and a loading spinner. + * If there are more comments to load, show a loading component with a + * skeleton loader and a loading spinner. */ !!isLoadingMore && } { /** - * If the user has scrolled to the end of the comments, show a - * button that will scroll them back to the top of the comments - * section. + * If the user has scrolled to the end of the comments, show a button that + * will scroll them back to the top of the comments section. */ !!isAtEnd && (
diff --git a/src/hooks/utilities/useMediaQuery.ts b/src/hooks/utilities/useMediaQuery.ts index f27af5a..60e8413 100644 --- a/src/hooks/utilities/useMediaQuery.ts +++ b/src/hooks/utilities/useMediaQuery.ts @@ -18,9 +18,8 @@ import { useState, useEffect } from 'react'; */ const useMediaQuery = (query: `(${string})`) => { /** - * Initialize the matches state variable to false. This is updated whenever the - * viewport size changes (i.e. when the component is mounted and when the window is - * resized) + * Initialize the matches state variable to false. This is updated whenever the viewport + * size changes (i.e. when the component is mounted and when the window is resized) */ const [matches, setMatches] = useState(false); @@ -35,8 +34,8 @@ const useMediaQuery = (query: `(${string})`) => { } /** - * Add a resize event listener to the window object, and update the `matches` - * state variable whenever the viewport size changes. + * Add a resize event listener to the window object, and update the `matches` state + * variable whenever the viewport size changes. */ const listener = () => setMatches(media.matches); window.addEventListener('resize', listener); From bc298bce0e17e315dfcc2a0930d9343b83760b41 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 28 May 2023 21:24:59 -0400 Subject: [PATCH 3/4] Update: change identifiers to cuid --- src/components/Login/LoginForm.tsx | 2 +- src/config/auth/types.ts | 2 +- src/hooks/auth/useUser.ts | 2 ++ src/pages/api/beer-comments/[id].ts | 4 ++-- src/pages/api/beers/[id]/comments/index.ts | 4 ++-- src/pages/api/beers/[id]/like/index.ts | 4 ++-- src/pages/api/beers/[id]/like/is-liked.ts | 2 +- src/pages/api/beers/[id]/recommendations.ts | 2 +- .../api/breweries/[id]/comments/index.ts | 4 ++-- src/pages/api/breweries/[id]/like/index.ts | 4 ++-- src/pages/api/breweries/[id]/like/is-liked.ts | 2 +- src/pages/api/brewery-comments/[id].ts | 4 ++-- src/pages/api/users/[id]/edit.ts | 2 +- src/pages/users/[id].tsx | 6 +++++ src/pages/{user => users}/current.tsx | 0 src/prisma/schema.prisma | 22 +++++++++---------- .../sendCreateBeerCommentRequest.ts | 2 +- .../BeerComment/createNewBeerComment.ts | 4 ++-- src/services/BeerPost/createNewBeerPost.ts | 2 +- .../schema/CreateBeerPostValidationSchema.ts | 4 ++-- .../schema/EditBeerPostValidationSchema.ts | 2 +- .../BreweryComment/createNewBreweryComment.ts | 4 ++-- src/services/User/findUserById.ts | 1 + src/services/User/schema/GetUserSchema.ts | 2 +- .../User/updateUserToBeConfirmedById.ts | 1 + .../types/CommentSchema/CommentQueryResult.ts | 4 ++-- 26 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 src/pages/users/[id].tsx rename src/pages/{user => users}/current.tsx (100%) diff --git a/src/components/Login/LoginForm.tsx b/src/components/Login/LoginForm.tsx index 7b88170..a9d5443 100644 --- a/src/components/Login/LoginForm.tsx +++ b/src/components/Login/LoginForm.tsx @@ -38,7 +38,7 @@ const LoginForm = () => { await mutate!(); toast.remove(loadingToast); toast.success('Logged in!'); - await router.push(`/user/current`); + await router.push(`/users/current`); } catch (error) { toast.remove(loadingToast); createErrorToast(error); diff --git a/src/config/auth/types.ts b/src/config/auth/types.ts index 8dce7ce..c02fb94 100644 --- a/src/config/auth/types.ts +++ b/src/config/auth/types.ts @@ -4,7 +4,7 @@ import { NextApiRequest } from 'next'; import { z } from 'zod'; export const BasicUserInfoSchema = z.object({ - id: z.string().uuid(), + id: z.string().cuid(), username: z.string(), }); diff --git a/src/hooks/auth/useUser.ts b/src/hooks/auth/useUser.ts index 86c4a9f..f1340df 100644 --- a/src/hooks/auth/useUser.ts +++ b/src/hooks/auth/useUser.ts @@ -39,6 +39,7 @@ const useUser = () => { const parsedPayload = GetUserSchema.safeParse(parsed.data.payload); + console.log(parsedPayload) if (!parsedPayload.success) { throw new Error(parsedPayload.error.message); } @@ -46,6 +47,7 @@ const useUser = () => { return parsedPayload.data; }); + return { user, isLoading, error: error as unknown, mutate }; }; diff --git a/src/pages/api/beer-comments/[id].ts b/src/pages/api/beer-comments/[id].ts index bf9129d..f5130b8 100644 --- a/src/pages/api/beer-comments/[id].ts +++ b/src/pages/api/beer-comments/[id].ts @@ -85,14 +85,14 @@ const router = createRouter< router .delete( - validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), getCurrentUser, checkIfCommentOwner, deleteComment, ) .put( validateRequest({ - querySchema: z.object({ id: z.string().uuid() }), + querySchema: z.object({ id: z.string().cuid() }), bodySchema: CreateCommentValidationSchema, }), getCurrentUser, diff --git a/src/pages/api/beers/[id]/comments/index.ts b/src/pages/api/beers/[id]/comments/index.ts index a385c40..bd44ae4 100644 --- a/src/pages/api/beers/[id]/comments/index.ts +++ b/src/pages/api/beers/[id]/comments/index.ts @@ -79,7 +79,7 @@ const router = createRouter< router.post( validateRequest({ bodySchema: CreateCommentValidationSchema, - querySchema: z.object({ id: z.string().uuid() }), + querySchema: z.object({ id: z.string().cuid() }), }), getCurrentUser, createComment, @@ -88,7 +88,7 @@ router.post( router.get( validateRequest({ querySchema: z.object({ - id: z.string().uuid(), + id: z.string().cuid(), page_size: z.coerce.number().int().positive(), page_num: z.coerce.number().int().positive(), }), diff --git a/src/pages/api/beers/[id]/like/index.ts b/src/pages/api/beers/[id]/like/index.ts index d422617..ed58b52 100644 --- a/src/pages/api/beers/[id]/like/index.ts +++ b/src/pages/api/beers/[id]/like/index.ts @@ -70,12 +70,12 @@ const router = createRouter< router.post( getCurrentUser, - validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), sendLikeRequest, ); router.get( - validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), getLikeCount, ); diff --git a/src/pages/api/beers/[id]/like/is-liked.ts b/src/pages/api/beers/[id]/like/is-liked.ts index 2b325b2..811cd9e 100644 --- a/src/pages/api/beers/[id]/like/is-liked.ts +++ b/src/pages/api/beers/[id]/like/is-liked.ts @@ -32,7 +32,7 @@ const router = createRouter< router.get( getCurrentUser, - validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), checkIfLiked, ); diff --git a/src/pages/api/beers/[id]/recommendations.ts b/src/pages/api/beers/[id]/recommendations.ts index 28c7c23..bdcef88 100644 --- a/src/pages/api/beers/[id]/recommendations.ts +++ b/src/pages/api/beers/[id]/recommendations.ts @@ -50,7 +50,7 @@ const getBeerRecommendationsRequest = async ( router.get( validateRequest({ querySchema: z.object({ - id: z.string().uuid(), + id: z.string().cuid(), page_num: z.string().regex(/^[0-9]+$/), page_size: z.string().regex(/^[0-9]+$/), }), diff --git a/src/pages/api/breweries/[id]/comments/index.ts b/src/pages/api/breweries/[id]/comments/index.ts index 3daa44c..b8ea448 100644 --- a/src/pages/api/breweries/[id]/comments/index.ts +++ b/src/pages/api/breweries/[id]/comments/index.ts @@ -89,7 +89,7 @@ const router = createRouter< router.post( validateRequest({ bodySchema: CreateCommentValidationSchema, - querySchema: z.object({ id: z.string().uuid() }), + querySchema: z.object({ id: z.string().cuid() }), }), getCurrentUser, createComment, @@ -98,7 +98,7 @@ router.post( router.get( validateRequest({ querySchema: z.object({ - id: z.string().uuid(), + id: z.string().cuid(), page_size: z.coerce.number().int().positive(), page_num: z.coerce.number().int().positive(), }), diff --git a/src/pages/api/breweries/[id]/like/index.ts b/src/pages/api/breweries/[id]/like/index.ts index 4c75729..d567dd6 100644 --- a/src/pages/api/breweries/[id]/like/index.ts +++ b/src/pages/api/breweries/[id]/like/index.ts @@ -83,12 +83,12 @@ const router = createRouter< router.post( getCurrentUser, - validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), sendLikeRequest, ); router.get( - validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), getLikeCount, ); diff --git a/src/pages/api/breweries/[id]/like/is-liked.ts b/src/pages/api/breweries/[id]/like/is-liked.ts index 7f7a0f6..00dc7eb 100644 --- a/src/pages/api/breweries/[id]/like/is-liked.ts +++ b/src/pages/api/breweries/[id]/like/is-liked.ts @@ -39,7 +39,7 @@ router.get( getCurrentUser, validateRequest({ querySchema: z.object({ - id: z.string().uuid(), + id: z.string().cuid(), }), }), checkIfLiked, diff --git a/src/pages/api/brewery-comments/[id].ts b/src/pages/api/brewery-comments/[id].ts index 8b81abf..b72479e 100644 --- a/src/pages/api/brewery-comments/[id].ts +++ b/src/pages/api/brewery-comments/[id].ts @@ -87,14 +87,14 @@ const router = createRouter< router .delete( - validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), getCurrentUser, checkIfCommentOwner, deleteComment, ) .put( validateRequest({ - querySchema: z.object({ id: z.string().uuid() }), + querySchema: z.object({ id: z.string().cuid() }), bodySchema: CreateCommentValidationSchema, }), getCurrentUser, diff --git a/src/pages/api/users/[id]/edit.ts b/src/pages/api/users/[id]/edit.ts index 4ef5b14..e644878 100644 --- a/src/pages/api/users/[id]/edit.ts +++ b/src/pages/api/users/[id]/edit.ts @@ -97,7 +97,7 @@ router.put( getCurrentUser, validateRequest({ bodySchema: EditUserSchema, - querySchema: z.object({ id: z.string().uuid() }), + querySchema: z.object({ id: z.string().cuid() }), }), checkIfUserCanEditUser, editUser, diff --git a/src/pages/users/[id].tsx b/src/pages/users/[id].tsx new file mode 100644 index 0000000..570a3c8 --- /dev/null +++ b/src/pages/users/[id].tsx @@ -0,0 +1,6 @@ +import { FC } from "react" + +const UserInfoPage: FC = () => null + + +export default UserInfoPage \ No newline at end of file diff --git a/src/pages/user/current.tsx b/src/pages/users/current.tsx similarity index 100% rename from src/pages/user/current.tsx rename to src/pages/users/current.tsx diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index d9e0c98..7fdb15e 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -13,7 +13,7 @@ datasource db { } model User { - id String @id @default(uuid()) + id String @id @default(cuid()) username String @unique firstName String lastName String @@ -36,7 +36,7 @@ model User { } model BeerPost { - id String @id @default(uuid()) + id String @id @default(cuid()) name String ibu Float abv Float @@ -55,7 +55,7 @@ model BeerPost { } model BeerPostLike { - id String @id @default(uuid()) + id String @id @default(cuid()) beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade) beerPostId String likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade) @@ -65,7 +65,7 @@ model BeerPostLike { } model BreweryPostLike { - id String @id @default(uuid()) + id String @id @default(cuid()) breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade) breweryPostId String likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade) @@ -75,7 +75,7 @@ model BreweryPostLike { } model BeerComment { - id String @id @default(uuid()) + id String @id @default(cuid()) rating Int beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade) beerPostId String @@ -87,7 +87,7 @@ model BeerComment { } model BeerType { - id String @id @default(uuid()) + id String @id @default(cuid()) name String createdAt DateTime @default(now()) @db.Timestamptz(3) updatedAt DateTime? @updatedAt @db.Timestamptz(3) @@ -97,7 +97,7 @@ model BeerType { } model Location { - id String @id @default(uuid()) + id String @id @default(cuid()) city String stateOrProvince String? country String? @@ -109,7 +109,7 @@ model Location { } model BreweryPost { - id String @id @default(uuid()) + id String @id @default(cuid()) name String location Location @relation(fields: [locationId], references: [id]) locationId String @unique @@ -126,7 +126,7 @@ model BreweryPost { } model BreweryComment { - id String @id @default(uuid()) + id String @id @default(cuid()) rating Int breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade) breweryPostId String @@ -138,7 +138,7 @@ model BreweryComment { } model BeerImage { - id String @id @default(uuid()) + id String @id @default(cuid()) beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade) beerPostId String path String @@ -151,7 +151,7 @@ model BeerImage { } model BreweryImage { - id String @id @default(uuid()) + id String @id @default(cuid()) breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade) breweryPostId String path String diff --git a/src/requests/BeerComment/sendCreateBeerCommentRequest.ts b/src/requests/BeerComment/sendCreateBeerCommentRequest.ts index afa8a2c..0874ec9 100644 --- a/src/requests/BeerComment/sendCreateBeerCommentRequest.ts +++ b/src/requests/BeerComment/sendCreateBeerCommentRequest.ts @@ -5,7 +5,7 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem import { z } from 'zod'; const BeerCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({ - beerPostId: z.string().uuid(), + beerPostId: z.string().cuid(), }); const sendCreateBeerCommentRequest = async ({ diff --git a/src/services/BeerComment/createNewBeerComment.ts b/src/services/BeerComment/createNewBeerComment.ts index c869b68..edd4825 100644 --- a/src/services/BeerComment/createNewBeerComment.ts +++ b/src/services/BeerComment/createNewBeerComment.ts @@ -3,8 +3,8 @@ import { z } from 'zod'; import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema'; const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({ - userId: z.string().uuid(), - beerPostId: z.string().uuid(), + userId: z.string().cuid(), + beerPostId: z.string().cuid(), }); const createNewBeerComment = async ({ diff --git a/src/services/BeerPost/createNewBeerPost.ts b/src/services/BeerPost/createNewBeerPost.ts index 7391ff7..1b8d3f1 100644 --- a/src/services/BeerPost/createNewBeerPost.ts +++ b/src/services/BeerPost/createNewBeerPost.ts @@ -4,7 +4,7 @@ import beerPostQueryResult from './schema/BeerPostQueryResult'; import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema'; const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({ - userId: z.string().uuid(), + userId: z.string().cuid(), }); const createNewBeerPost = async ({ diff --git a/src/services/BeerPost/schema/CreateBeerPostValidationSchema.ts b/src/services/BeerPost/schema/CreateBeerPostValidationSchema.ts index 5325920..e9aa330 100644 --- a/src/services/BeerPost/schema/CreateBeerPostValidationSchema.ts +++ b/src/services/BeerPost/schema/CreateBeerPostValidationSchema.ts @@ -31,13 +31,13 @@ const CreateBeerPostValidationSchema = z.object({ required_error: 'Type id is required.', invalid_type_error: 'Type id must be a string.', }) - .uuid({ message: 'Invalid type id.' }), + .cuid({ message: 'Invalid type id.' }), breweryId: z .string({ required_error: 'Brewery id is required.', invalid_type_error: 'Brewery id must be a string.', }) - .uuid({ message: 'Invalid brewery id.' }), + .cuid({ message: 'Invalid brewery id.' }), }); export default CreateBeerPostValidationSchema; diff --git a/src/services/BeerPost/schema/EditBeerPostValidationSchema.ts b/src/services/BeerPost/schema/EditBeerPostValidationSchema.ts index ab11b77..f9f7233 100644 --- a/src/services/BeerPost/schema/EditBeerPostValidationSchema.ts +++ b/src/services/BeerPost/schema/EditBeerPostValidationSchema.ts @@ -4,6 +4,6 @@ import CreateBeerPostValidationSchema from './CreateBeerPostValidationSchema'; const EditBeerPostValidationSchema = CreateBeerPostValidationSchema.omit({ breweryId: true, typeId: true, -}).extend({ id: z.string().uuid() }); +}).extend({ id: z.string().cuid() }); export default EditBeerPostValidationSchema; diff --git a/src/services/BreweryComment/createNewBreweryComment.ts b/src/services/BreweryComment/createNewBreweryComment.ts index 9dc1ed6..c89fa07 100644 --- a/src/services/BreweryComment/createNewBreweryComment.ts +++ b/src/services/BreweryComment/createNewBreweryComment.ts @@ -3,8 +3,8 @@ import { z } from 'zod'; import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema'; const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({ - userId: z.string().uuid(), - breweryPostId: z.string().uuid(), + userId: z.string().cuid(), + breweryPostId: z.string().cuid(), }); const createNewBreweryComment = async ({ diff --git a/src/services/User/findUserById.ts b/src/services/User/findUserById.ts index 211317f..f120404 100644 --- a/src/services/User/findUserById.ts +++ b/src/services/User/findUserById.ts @@ -15,6 +15,7 @@ const findUserById = async (id: string) => { dateOfBirth: true, createdAt: true, accountIsVerified: true, + updatedAt: true }, }); diff --git a/src/services/User/schema/GetUserSchema.ts b/src/services/User/schema/GetUserSchema.ts index aa625c6..b14f8fd 100644 --- a/src/services/User/schema/GetUserSchema.ts +++ b/src/services/User/schema/GetUserSchema.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; const GetUserSchema = z.object({ - id: z.string().uuid(), + id: z.string().cuid(), username: z.string(), createdAt: z.coerce.date(), updatedAt: z.coerce.date().nullable(), diff --git a/src/services/User/updateUserToBeConfirmedById.ts b/src/services/User/updateUserToBeConfirmedById.ts index 34478f7..0cf8e7f 100644 --- a/src/services/User/updateUserToBeConfirmedById.ts +++ b/src/services/User/updateUserToBeConfirmedById.ts @@ -14,6 +14,7 @@ const updateUserToBeConfirmedById = async (id: string) => { createdAt: true, firstName: true, lastName: true, + updatedAt: true, dateOfBirth: true, }, }); diff --git a/src/services/types/CommentSchema/CommentQueryResult.ts b/src/services/types/CommentSchema/CommentQueryResult.ts index f88ba5b..bb52111 100644 --- a/src/services/types/CommentSchema/CommentQueryResult.ts +++ b/src/services/types/CommentSchema/CommentQueryResult.ts @@ -1,12 +1,12 @@ import { z } from 'zod'; const CommentQueryResult = z.object({ - id: z.string().uuid(), + id: z.string().cuid(), content: z.string().min(1).max(500), rating: z.number().int().min(1).max(5), createdAt: z.coerce.date(), postedBy: z.object({ - id: z.string().uuid(), + id: z.string().cuid(), username: z.string().min(1).max(50), }), }); From 06ae380b8f24b89f340d57d4be49d30058a39f15 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Mon, 29 May 2023 15:51:59 -0400 Subject: [PATCH 4/4] feat: create confirm user page, option to resend email if link expires --- src/config/jwt/index.ts | 33 ++++-- src/hooks/auth/useUser.ts | 2 - src/pages/api/users/confirm.ts | 10 +- src/pages/api/users/resend-confirmation.ts | 32 ++++++ src/pages/users/[id].tsx | 7 +- src/pages/users/confirm.tsx | 125 +++++++++++++++++++++ src/pages/users/current.tsx | 38 ++++--- src/services/User/findUserById.ts | 2 +- src/services/User/sendConfirmationEmail.ts | 2 +- 9 files changed, 210 insertions(+), 41 deletions(-) create mode 100644 src/pages/api/users/resend-confirmation.ts create mode 100644 src/pages/users/confirm.tsx diff --git a/src/config/jwt/index.ts b/src/config/jwt/index.ts index 54a659f..add709b 100644 --- a/src/config/jwt/index.ts +++ b/src/config/jwt/index.ts @@ -2,22 +2,31 @@ import { BasicUserInfoSchema } from '@/config/auth/types'; import jwt from 'jsonwebtoken'; import { z } from 'zod'; import { CONFIRMATION_TOKEN_SECRET } from '../env'; +import ServerError from '../util/ServerError'; -type User = z.infer; - -export const generateConfirmationToken = (user: User) => { - const token = jwt.sign(user, CONFIRMATION_TOKEN_SECRET, { expiresIn: '30m' }); - return token; +export const generateConfirmationToken = (user: z.infer) => { + return jwt.sign(user, CONFIRMATION_TOKEN_SECRET, { expiresIn: '3m' }); }; -export const verifyConfirmationToken = (token: string) => { - const decoded = jwt.verify(token, CONFIRMATION_TOKEN_SECRET); +export const verifyConfirmationToken = async (token: string) => { + try { + const decoded = jwt.verify(token, CONFIRMATION_TOKEN_SECRET); - const parsed = BasicUserInfoSchema.safeParse(decoded); + const parsed = BasicUserInfoSchema.safeParse(decoded); - if (!parsed.success) { - throw new Error('Invalid token'); + if (!parsed.success) { + throw new Error('Invalid token'); + } + + return parsed.data; + } catch (error) { + if (error instanceof Error && error.message === 'jwt expired') { + throw new ServerError( + 'Your confirmation token is expired. Please generate a new one.', + 401, + ); + } + + throw new ServerError('Something went wrong', 500); } - - return parsed.data; }; diff --git a/src/hooks/auth/useUser.ts b/src/hooks/auth/useUser.ts index f1340df..86c4a9f 100644 --- a/src/hooks/auth/useUser.ts +++ b/src/hooks/auth/useUser.ts @@ -39,7 +39,6 @@ const useUser = () => { const parsedPayload = GetUserSchema.safeParse(parsed.data.payload); - console.log(parsedPayload) if (!parsedPayload.success) { throw new Error(parsedPayload.error.message); } @@ -47,7 +46,6 @@ const useUser = () => { return parsedPayload.data; }); - return { user, isLoading, error: error as unknown, mutate }; }; diff --git a/src/pages/api/users/confirm.ts b/src/pages/api/users/confirm.ts index af3f7fa..9186631 100644 --- a/src/pages/api/users/confirm.ts +++ b/src/pages/api/users/confirm.ts @@ -21,16 +21,16 @@ const confirmUser = async (req: ConfirmUserRequest, res: NextApiResponse) => { const { token } = req.query; const user = req.user!; - const { id } = verifyConfirmationToken(token); + const { id } = await verifyConfirmationToken(token); + + if (user.accountIsVerified) { + throw new ServerError('Your account is already verified.', 400); + } if (user.id !== id) { throw new ServerError('Could not confirm user.', 401); } - if (user.accountIsVerified) { - throw new ServerError('User is already verified.', 400); - } - await updateUserToBeConfirmedById(id); res.status(200).json({ diff --git a/src/pages/api/users/resend-confirmation.ts b/src/pages/api/users/resend-confirmation.ts new file mode 100644 index 0000000..d422c29 --- /dev/null +++ b/src/pages/api/users/resend-confirmation.ts @@ -0,0 +1,32 @@ +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; +import sendConfirmationEmail from '@/services/User/sendConfirmationEmail'; + +const resendConfirmation = async ( + req: UserExtendedNextApiRequest, + res: NextApiResponse, +) => { + const user = req.user!; + + await sendConfirmationEmail(user); + res.status(200).json({ + message: `Resent the confirmation email for ${user.username}.`, + statusCode: 200, + success: true, + }); +}; + +const router = createRouter< + UserExtendedNextApiRequest, + NextApiResponse> +>(); + +router.post(getCurrentUser, resendConfirmation); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/pages/users/[id].tsx b/src/pages/users/[id].tsx index 570a3c8..f7550d1 100644 --- a/src/pages/users/[id].tsx +++ b/src/pages/users/[id].tsx @@ -1,6 +1,5 @@ -import { FC } from "react" +import { FC } from 'react'; -const UserInfoPage: FC = () => null +const UserInfoPage: FC = () => null; - -export default UserInfoPage \ No newline at end of file +export default UserInfoPage; diff --git a/src/pages/users/confirm.tsx b/src/pages/users/confirm.tsx new file mode 100644 index 0000000..3ab094a --- /dev/null +++ b/src/pages/users/confirm.tsx @@ -0,0 +1,125 @@ +import UserContext from '@/contexts/UserContext'; +import createErrorToast from '@/util/createErrorToast'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { FC, useContext, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import useSWR from 'swr'; + +const useSendConfirmUserRequest = () => { + const router = useRouter(); + const token = router.query.token as string | undefined; + + const { data, error } = useSWR(`/api/users/confirm?token=${token}`, async (url) => { + if (!token) { + throw new Error('Token must be provided.'); + } + + const response = await fetch(url); + if (!response.ok) { + throw new Error(response.statusText); + } + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('API response validation failed.'); + } + return parsed.data; + }); + + return { data, error: error as unknown }; +}; + +const ConfirmUserPage: FC = () => { + const router = useRouter(); + const { error, data } = useSendConfirmUserRequest(); + const { user } = useContext(UserContext); + + const needsToLogin = + error instanceof Error && error.message === 'Unauthorized' && !user; + const tokenExpired = error instanceof Error && error.message === 'Unauthorized' && user; + const [confirmationResent, setConfirmationResent] = useState(false); + + if (user?.accountIsVerified) { + router.push('/users/current'); + return null; + } + + if (data) { + router.push('/users/current'); + return null; + } + + if (needsToLogin) { + return ( + <> + + Confirm User | The Biergarten App + +
+

+ Please login to confirm your account. +

+
+ + ); + } + if (tokenExpired) { + const onClick = async () => { + const loadingToast = toast.loading('Resending your confirmation email.'); + try { + const response = await fetch('/api/users/resend-confirmation', { + method: 'POST', + }); + if (!response.ok) { + throw new Error('Something went wrong.'); + } + + toast.remove(loadingToast); + toast.success('Sent a new confirmation email.'); + + setConfirmationResent(true); + } catch (err) { + createErrorToast(err); + } + }; + + return ( + <> + + Confirm User | The Biergarten App + +
+ {!confirmationResent ? ( + <> +

+ Your confirmation token is expired. +

+ + + ) : ( + <> +

+ Resent your confirmation link. +

+

Please check your email.

+ + )} +
+ + ); + } + + return null; +}; + +export default ConfirmUserPage; diff --git a/src/pages/users/current.tsx b/src/pages/users/current.tsx index 0d0f5c4..58980d2 100644 --- a/src/pages/users/current.tsx +++ b/src/pages/users/current.tsx @@ -5,6 +5,7 @@ import UserContext from '@/contexts/UserContext'; import { GetServerSideProps, NextPage } from 'next'; import { useContext } from 'react'; import useMediaQuery from '@/hooks/utilities/useMediaQuery'; +import Head from 'next/head'; const ProtectedPage: NextPage = () => { const { user, isLoading } = useContext(UserContext); @@ -17,22 +18,27 @@ const ProtectedPage: NextPage = () => { const isDesktop = useMediaQuery('(min-width: 768px)'); return ( -
- {isLoading && } - {user && !isLoading && ( - <> -

- Good {isMorning && 'morning'} - {isAfternoon && 'afternoon'} - {isEvening && 'evening'} - {`, ${user?.firstName}!`} -

-

- Welcome to the Biergarten App! -

- - )} -
+ <> + + Hello, {user?.firstName}! | The Biergarten App + +
+ {isLoading && } + {user && !isLoading && ( + <> +

+ Good {isMorning && 'morning'} + {isAfternoon && 'afternoon'} + {isEvening && 'evening'} + {`, ${user?.firstName}!`} +

+

+ Welcome to the Biergarten App! +

+ + )} +
+ ); }; diff --git a/src/services/User/findUserById.ts b/src/services/User/findUserById.ts index f120404..fb962b2 100644 --- a/src/services/User/findUserById.ts +++ b/src/services/User/findUserById.ts @@ -15,7 +15,7 @@ const findUserById = async (id: string) => { dateOfBirth: true, createdAt: true, accountIsVerified: true, - updatedAt: true + updatedAt: true, }, }); diff --git a/src/services/User/sendConfirmationEmail.ts b/src/services/User/sendConfirmationEmail.ts index c45bc1a..8abcfed 100644 --- a/src/services/User/sendConfirmationEmail.ts +++ b/src/services/User/sendConfirmationEmail.ts @@ -14,7 +14,7 @@ const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => { const subject = 'Confirm your email'; const name = username; - const url = `${BASE_URL}/api/users/confirm?token=${confirmationToken}`; + const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`; const address = email; const html = render(Welcome({ name, url, subject })!);