diff --git a/components/Login/LoginForm.tsx b/components/Login/LoginForm.tsx new file mode 100644 index 0000000..12fb75b --- /dev/null +++ b/components/Login/LoginForm.tsx @@ -0,0 +1,77 @@ +import sendLoginUserRequest from '@/requests/sendLoginUserRequest'; +import LoginValidationSchema from '@/services/user/schema/LoginValidationSchema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useRouter } from 'next/router'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { z } from 'zod'; +import FormError from '../ui/forms/FormError'; +import FormInfo from '../ui/forms/FormInfo'; +import FormLabel from '../ui/forms/FormLabel'; +import FormSegment from '../ui/forms/FormSegment'; +import FormTextInput from '../ui/forms/FormTextInput'; + +type LoginT = z.infer; +const LoginForm = () => { + const router = useRouter(); + const { register, handleSubmit, formState } = useForm({ + resolver: zodResolver(LoginValidationSchema), + defaultValues: { + username: '', + password: '', + }, + }); + + const { errors } = formState; + + const onSubmit: SubmitHandler = async (data) => { + try { + const response = await sendLoginUserRequest(data); + + router.push(`/users/${response.id}`); + } catch (error) { + console.error(error); + } + }; + + return ( +
+
+ + username + {errors.username?.message} + + + + + + + password + {errors.password?.message} + + + + +
+ +
+ +
+
+ ); +}; + +export default LoginForm; diff --git a/config/auth/middleware/getCurrentUser.ts b/config/auth/middleware/getCurrentUser.ts index b827bac..d9142eb 100644 --- a/config/auth/middleware/getCurrentUser.ts +++ b/config/auth/middleware/getCurrentUser.ts @@ -3,11 +3,11 @@ import { NextHandler } from 'next-connect'; import findUserById from '@/services/user/findUserById'; import ServerError from '@/config/util/ServerError'; import { getLoginSession } from '../session'; -import { ExtendedNextApiRequest } from '../types'; +import { UserExtendedNextApiRequest } from '../types'; /** Get the current user from the session. Adds the user to the request object. */ const getCurrentUser = async ( - req: ExtendedNextApiRequest, + req: UserExtendedNextApiRequest, res: NextApiResponse, next: NextHandler, ) => { diff --git a/config/auth/types.ts b/config/auth/types.ts index 2c63ab8..ba8ecf5 100644 --- a/config/auth/types.ts +++ b/config/auth/types.ts @@ -15,7 +15,7 @@ export const UserSessionSchema = BasicUserInfoSchema.merge( }), ); -export interface ExtendedNextApiRequest extends NextApiRequest { +export interface UserExtendedNextApiRequest extends NextApiRequest { user?: z.infer; } diff --git a/config/zod/middleware/validateRequest.ts b/config/zod/middleware/validateRequest.ts new file mode 100644 index 0000000..a44531c --- /dev/null +++ b/config/zod/middleware/validateRequest.ts @@ -0,0 +1,48 @@ +import ServerError from '@/config/util/ServerError'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { NextHandler } from 'next-connect'; +import { z } from 'zod'; + +/** + * Middleware to validate the request body and/or query against a zod schema. + * + * @example + * const handler = nextConnect(NextConnectConfig).post( + * validateRequest({ bodySchema: BeerPostValidationSchema }), + * getCurrentUser, + * createBeerPost, + * ); + * + * @param args + * @param args.bodySchema The body schema to validate against. + * @param args.querySchema The query schema to validate against. + * @throws ServerError with status code 400 if the request body or query is invalid. + */ +const validateRequest = + ({ + bodySchema, + querySchema, + }: { + bodySchema?: z.ZodSchema; + querySchema?: z.ZodSchema; + }) => + async (req: NextApiRequest, res: NextApiResponse, next: NextHandler) => { + if (bodySchema) { + const parsed = bodySchema.safeParse(req.body); + if (!parsed.success) { + throw new ServerError('Invalid request body.', 400); + } + } + + if (querySchema) { + const parsed = querySchema.safeParse(req.query); + if (!parsed.success) { + throw new ServerError(parsed.error.message, 400); + } + req.query = parsed.data; + } + + next(); + }; + +export default validateRequest; diff --git a/pages/api/beers/[id]/comments.ts b/pages/api/beers/[id]/comments.ts index c140513..cba0521 100644 --- a/pages/api/beers/[id]/comments.ts +++ b/pages/api/beers/[id]/comments.ts @@ -1,27 +1,31 @@ +import validateRequest from '@/config/zod/middleware/validateRequest'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectConfig from '@/config/nextConnect/NextConnectConfig'; -import ServerError from '@/config/util/ServerError'; import createNewBeerComment from '@/services/BeerComment/createNewBeerComment'; import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; -import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -import { NextApiHandler } from 'next'; + import nextConnect from 'next-connect'; import { z } from 'zod'; +import getCurrentUser from '@/config/auth/middleware/getCurrentUser'; +import { NextApiResponse } from 'next'; -const createComment: NextApiHandler> = async ( - req, - res, +interface CreateCommentRequest extends UserExtendedNextApiRequest { + body: z.infer; +} + +const createComment = async ( + req: CreateCommentRequest, + res: NextApiResponse>, ) => { - const cleanedReqBody = BeerCommentValidationSchema.safeParse(req.body); - if (!cleanedReqBody.success) { - throw new ServerError('Invalid request body', 400); - } - const { content, rating, beerPostId } = cleanedReqBody.data; + const { content, rating, beerPostId } = req.body; const newBeerComment: BeerCommentQueryResultT = await createNewBeerComment({ content, rating, beerPostId, + userId: req.user!.id, }); res.status(201).json({ @@ -32,6 +36,10 @@ const createComment: NextApiHandler> }); }; -const handler = nextConnect(NextConnectConfig).post(createComment); +const handler = nextConnect(NextConnectConfig).post( + validateRequest({ bodySchema: BeerCommentValidationSchema }), + getCurrentUser, + createComment, +); export default handler; diff --git a/pages/api/beers/create.ts b/pages/api/beers/create.ts index 2e50ad8..203ec85 100644 --- a/pages/api/beers/create.ts +++ b/pages/api/beers/create.ts @@ -1,21 +1,24 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import validateRequest from '@/config/zod/middleware/validateRequest'; import nextConnect from 'next-connect'; -import ServerError from '@/config/util/ServerError'; import createNewBeerPost from '@/services/BeerPost/createNewBeerPost'; import BeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -import { NextApiHandler } from 'next'; +import { NextApiResponse } from 'next'; import { z } from 'zod'; import NextConnectConfig from '@/config/nextConnect/NextConnectConfig'; +import getCurrentUser from '@/config/auth/middleware/getCurrentUser'; -const createBeerPost: NextApiHandler< - z.infer -> = async (req, res) => { - const cleanedReqBody = BeerPostValidationSchema.safeParse(req.body); - if (!cleanedReqBody.success) { - throw new ServerError('Invalid request body', 400); - } +interface CreateBeerPostRequest extends UserExtendedNextApiRequest { + body: z.infer; +} + +const createBeerPost = async ( + req: CreateBeerPostRequest, + res: NextApiResponse>, +) => { + const { name, description, typeId, abv, ibu, breweryId } = req.body; - const { name, description, typeId, abv, ibu, breweryId } = cleanedReqBody.data; const newBeerPost = await createNewBeerPost({ name, description, @@ -23,6 +26,7 @@ const createBeerPost: NextApiHandler< ibu, typeId, breweryId, + userId: req.user!.id, }); res.status(201).json({ @@ -33,6 +37,10 @@ const createBeerPost: NextApiHandler< }); }; -const handler = nextConnect(NextConnectConfig).post(createBeerPost); +const handler = nextConnect(NextConnectConfig).post( + validateRequest({ bodySchema: BeerPostValidationSchema }), + getCurrentUser, + createBeerPost, +); export default handler; diff --git a/pages/api/users/current.ts b/pages/api/users/current.ts index 4c2422d..3ad8558 100644 --- a/pages/api/users/current.ts +++ b/pages/api/users/current.ts @@ -1,12 +1,12 @@ import NextConnectConfig from '@/config/nextConnect/NextConnectConfig'; -import { ExtendedNextApiRequest } from '@/config/auth/types'; +import { UserExtendedNextApiRequest } from '@/config/auth/types'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import getCurrentUser from '@/config/auth/middleware/getCurrentUser'; import nextConnect from 'next-connect'; import { z } from 'zod'; -const sendCurrentUser = async (req: ExtendedNextApiRequest, res: NextApiResponse) => { +const sendCurrentUser = async (req: UserExtendedNextApiRequest, res: NextApiResponse) => { const { user } = req; res.status(200).json({ message: `Currently logged in as ${user!.username}`, @@ -17,7 +17,7 @@ const sendCurrentUser = async (req: ExtendedNextApiRequest, res: NextApiResponse }; const handler = nextConnect< - ExtendedNextApiRequest, + UserExtendedNextApiRequest, NextApiResponse> >(NextConnectConfig).get(getCurrentUser, sendCurrentUser); diff --git a/pages/api/users/login.ts b/pages/api/users/login.ts index 4a60dff..cb62ddf 100644 --- a/pages/api/users/login.ts +++ b/pages/api/users/login.ts @@ -8,20 +8,19 @@ import { NextApiResponse } from 'next'; import { z } from 'zod'; import ServerError from '@/config/util/ServerError'; import LoginValidationSchema from '@/services/user/schema/LoginValidationSchema'; -import { ExtendedNextApiRequest } from '../../../config/auth/types'; +import { UserExtendedNextApiRequest } from '../../../config/auth/types'; export default nextConnect< - ExtendedNextApiRequest, + UserExtendedNextApiRequest, NextApiResponse> >(NextConnectConfig) .use(passport.initialize()) .use(async (req, res, next) => { - passport.use(localStrat); const parsed = LoginValidationSchema.safeParse(req.body); if (!parsed.success) { throw new ServerError('Username and password are required.', 400); } - + passport.use(localStrat); passport.authenticate('local', { session: false }, (error, token) => { if (error) { next(error); diff --git a/pages/api/users/register.ts b/pages/api/users/register.ts index 3088792..4607d85 100644 --- a/pages/api/users/register.ts +++ b/pages/api/users/register.ts @@ -1,44 +1,18 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { z } from 'zod'; import ServerError from '@/config/util/ServerError'; -import nc, { NextHandler } from 'next-connect'; +import nc from 'next-connect'; import createNewUser from '@/services/user/createNewUser'; import CreateUserValidationSchema from '@/services/user/schema/CreateUserValidationSchema'; import NextConnectConfig from '@/config/nextConnect/NextConnectConfig'; import findUserByUsername from '@/services/user/findUserByUsername'; import findUserByEmail from '@/services/user/findUserByEmail'; +import validateRequest from '@/config/zod/middleware/validateRequest'; interface RegisterUserRequest extends NextApiRequest { body: z.infer; } -const validateRequest = - ({ - bodySchema, - querySchema, - }: { - bodySchema?: z.ZodSchema; - querySchema?: z.ZodSchema; - }) => - async (req: NextApiRequest, res: NextApiResponse, next: NextHandler) => { - if (bodySchema) { - const parsed = bodySchema.safeParse(req.body); - if (!parsed.success) { - throw new ServerError('Invalid request body.', 400); - } - } - - if (querySchema) { - const parsed = querySchema.safeParse(req.query); - if (!parsed.success) { - throw new ServerError(parsed.error.message, 400); - } - req.query = parsed.data; - } - - next(); - }; - const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => { const [usernameTaken, emailTaken] = await Promise.all([ findUserByUsername(req.body.username), diff --git a/pages/login/index.tsx b/pages/login/index.tsx index 73117a4..b564eea 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -1,82 +1,9 @@ import { NextPage } from 'next'; -import { SubmitHandler, useForm } from 'react-hook-form'; import { useEffect } from 'react'; import { useRouter } from 'next/router'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import FormError from '@/components/ui/forms/FormError'; -import FormInfo from '@/components/ui/forms/FormInfo'; -import FormLabel from '@/components/ui/forms/FormLabel'; -import FormSegment from '@/components/ui/forms/FormSegment'; -import FormTextInput from '@/components/ui/forms/FormTextInput'; import Layout from '@/components/ui/Layout'; -import LoginValidationSchema from '@/services/user/schema/LoginValidationSchema'; -import sendLoginUserRequest from '@/requests/sendLoginUserRequest'; import useUser from '@/hooks/useUser'; - -type LoginT = z.infer; -const LoginForm = () => { - const router = useRouter(); - const { register, handleSubmit, formState } = useForm({ - resolver: zodResolver(LoginValidationSchema), - defaultValues: { - username: '', - password: '', - }, - }); - - const { errors } = formState; - - const onSubmit: SubmitHandler = async (data) => { - try { - const response = await sendLoginUserRequest(data); - - router.push(`/users/${response.id}`); - } catch (error) { - console.error(error); - } - }; - - return ( -
-
- - username - {errors.username?.message} - - - - - - - password - {errors.password?.message} - - - - -
- -
- -
-
- ); -}; +import LoginForm from '@/components/Login/LoginForm'; const LoginPage: NextPage = () => { const { user } = useUser(); @@ -88,7 +15,7 @@ const LoginPage: NextPage = () => { } router.push(`/user/current`); - }, [user]); + }, [user, router]); return ( diff --git a/services/BeerComment/createNewBeerComment.ts b/services/BeerComment/createNewBeerComment.ts index ec6b55e..169daa5 100644 --- a/services/BeerComment/createNewBeerComment.ts +++ b/services/BeerComment/createNewBeerComment.ts @@ -2,18 +2,21 @@ import DBClient from '@/prisma/DBClient'; import { z } from 'zod'; import BeerCommentValidationSchema from './schema/CreateBeerCommentValidationSchema'; +const CreateBeerCommentWithUserSchema = BeerCommentValidationSchema.extend({ + userId: z.string().uuid(), +}); const createNewBeerComment = async ({ content, rating, beerPostId, -}: z.infer) => { - const user = await DBClient.instance.user.findFirstOrThrow(); + userId, +}: z.infer) => { return DBClient.instance.beerComment.create({ data: { content, rating, beerPost: { connect: { id: beerPostId } }, - postedBy: { connect: { id: user.id } }, + postedBy: { connect: { id: userId } }, }, select: { id: true, diff --git a/services/BeerPost/createNewBeerPost.ts b/services/BeerPost/createNewBeerPost.ts index 64f9a54..afc2188 100644 --- a/services/BeerPost/createNewBeerPost.ts +++ b/services/BeerPost/createNewBeerPost.ts @@ -2,6 +2,10 @@ import DBClient from '@/prisma/DBClient'; import { z } from 'zod'; import BeerPostValidationSchema from './schema/CreateBeerPostValidationSchema'; +const CreateBeerPostWithUserSchema = BeerPostValidationSchema.extend({ + userId: z.string().uuid(), +}); + const createNewBeerPost = async ({ name, description, @@ -9,9 +13,8 @@ const createNewBeerPost = async ({ ibu, typeId, breweryId, -}: z.infer) => { - const user = await DBClient.instance.user.findFirstOrThrow(); - + userId, +}: z.infer) => { const newBeerPost = await DBClient.instance.beerPost.create({ data: { name, @@ -19,7 +22,7 @@ const createNewBeerPost = async ({ abv, ibu, type: { connect: { id: typeId } }, - postedBy: { connect: { id: user.id } }, + postedBy: { connect: { id: userId } }, brewery: { connect: { id: breweryId } }, }, });