diff --git a/components/BeerIndex/BeerCard.tsx b/components/BeerIndex/BeerCard.tsx
index 9a68635..0741ce7 100644
--- a/components/BeerIndex/BeerCard.tsx
+++ b/components/BeerIndex/BeerCard.tsx
@@ -8,7 +8,12 @@ const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => {
{post.beerImages.length > 0 && (
-
+
)}
diff --git a/config/nextConnect/NextConnectOptions.ts b/config/nextConnect/NextConnectOptions.ts
index 36ac5fc..a72c300 100644
--- a/config/nextConnect/NextConnectOptions.ts
+++ b/config/nextConnect/NextConnectOptions.ts
@@ -1,15 +1,32 @@
-import { NextApiRequest, NextApiResponse } from 'next';
+import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
+import type { NextApiRequest, NextApiResponse } from 'next';
+import type { RequestHandler } from 'next-connect/dist/types/node';
+import type { HandlerOptions } from 'next-connect/dist/types/types';
+import { z } from 'zod';
+import logger from '../pino/logger';
+
import ServerError from '../util/ServerError';
-const NextConnectOptions = {
- onNoMatch(req: NextApiRequest, res: NextApiResponse) {
+type NextConnectOptionsT = HandlerOptions<
+ RequestHandler<
+ NextApiRequest,
+ NextApiResponse>
+ >
+>;
+
+const NextConnectOptions: NextConnectOptionsT = {
+ onNoMatch(req, res) {
res.status(405).json({
message: 'Method not allowed.',
statusCode: 405,
success: false,
});
},
- onError(error: unknown, req: NextApiRequest, res: NextApiResponse) {
+ onError(error, req, res) {
+ if (process.env.NODE_ENV !== 'production') {
+ logger.error(error);
+ }
+
const message = error instanceof Error ? error.message : 'Internal server error.';
const statusCode = error instanceof ServerError ? error.statusCode : 500;
res.status(statusCode).json({
diff --git a/config/nextConnect/middleware/checkIfBeerPostOwner.ts b/config/nextConnect/middleware/checkIfBeerPostOwner.ts
new file mode 100644
index 0000000..f5e1169
--- /dev/null
+++ b/config/nextConnect/middleware/checkIfBeerPostOwner.ts
@@ -0,0 +1,31 @@
+import { UserExtendedNextApiRequest } from '@/config/auth/types';
+import ServerError from '@/config/util/ServerError';
+import getBeerPostById from '@/services/BeerPost/getBeerPostById';
+import { NextApiResponse } from 'next';
+import { NextHandler } from 'next-connect';
+
+interface CheckIfBeerPostOwnerRequest extends UserExtendedNextApiRequest {
+ query: { id: string };
+}
+
+const checkIfBeerPostOwner = async (
+ req: RequestType,
+ res: NextApiResponse,
+ next: NextHandler,
+) => {
+ const { id } = req.query;
+ const user = req.user!;
+ const beerPost = await getBeerPostById(id);
+
+ if (!beerPost) {
+ throw new ServerError('Beer post not found', 404);
+ }
+
+ if (beerPost.postedBy.id !== user.id) {
+ throw new ServerError('You are not authorized to edit this beer post', 403);
+ }
+
+ return next();
+};
+
+export default checkIfBeerPostOwner;
diff --git a/config/nextConnect/middleware/validateRequest.ts b/config/nextConnect/middleware/validateRequest.ts
index 6ae494d..37f5bfd 100644
--- a/config/nextConnect/middleware/validateRequest.ts
+++ b/config/nextConnect/middleware/validateRequest.ts
@@ -28,10 +28,11 @@ const validateRequest =
}) =>
async (req: NextApiRequest, res: NextApiResponse, next: NextHandler) => {
if (bodySchema) {
- const parsed = bodySchema.safeParse(req.body);
+ const parsed = bodySchema.safeParse(JSON.parse(JSON.stringify(req.body)));
if (!parsed.success) {
throw new ServerError('Invalid request body.', 400);
}
+ req.body = parsed.data;
}
if (querySchema) {
diff --git a/pages/api/beers/[id]/comments.ts b/pages/api/beers/[id]/comments/index.ts
similarity index 100%
rename from pages/api/beers/[id]/comments.ts
rename to pages/api/beers/[id]/comments/index.ts
diff --git a/pages/api/beers/[id]/images/index.ts b/pages/api/beers/[id]/images/index.ts
index fec38be..5e5b29c 100644
--- a/pages/api/beers/[id]/images/index.ts
+++ b/pages/api/beers/[id]/images/index.ts
@@ -1,10 +1,11 @@
+import DBClient from '@/prisma/DBClient';
+import { BeerImage } from '@prisma/client';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
-import { NextHandler, createRouter, expressWrapper } from 'next-connect';
+import { createRouter, expressWrapper } from 'next-connect';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
-import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import multer from 'multer';
@@ -12,55 +13,70 @@ import cloudinaryConfig from '@/config/cloudinary';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import ServerError from '@/config/util/ServerError';
+import validateRequest from '@/config/nextConnect/middleware/validateRequest';
const { storage } = cloudinaryConfig;
const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => {
- if (
- file.mimetype === 'image/png' ||
- file.mimetype === 'image/jpg' ||
- file.mimetype === 'image/jpeg'
- ) {
- cb(null, true);
- } else {
+ const { mimetype } = file;
+
+ const isImage = mimetype.startsWith('image/');
+
+ if (!isImage) {
cb(null, false);
}
+ cb(null, true);
};
-const uploadMiddleware = multer({ storage, fileFilter }).array('images');
+const uploadMiddleware = expressWrapper(
+ multer({ storage, fileFilter, limits: { files: 3 } }).array('images'),
+);
+
+const BeerPostImageValidationSchema = z.object({
+ caption: z.string(),
+ alt: z.string(),
+});
interface UploadBeerPostImagesRequest extends UserExtendedNextApiRequest {
- files?:
- | Express.Multer.File[]
- | {
- [fieldname: string]: Express.Multer.File[];
- };
-
- query: {
- id: string;
- };
-
- // beerPost?: BeerPostQueryResult;
+ files?: Express.Multer.File[];
+ query: { id: string };
+ body: z.infer;
}
-const checkIfBeerPostOwner = async (
+const processImageData = async (
req: UploadBeerPostImagesRequest,
- res: NextApiResponse,
- next: NextHandler,
+ res: NextApiResponse>,
) => {
- const { id } = req.query;
- const user = req.user!;
- const beerPost = await getBeerPostById(id);
+ const { files, user, body } = req;
- if (!beerPost) {
- throw new ServerError('Beer post not found', 404);
+ if (!files || !files.length) {
+ throw new ServerError('No images uploaded', 400);
}
+ const beerImagePromises: Promise[] = [];
- if (beerPost.postedBy.id !== user.id) {
- throw new ServerError('You are not authorized to edit this beer post', 403);
- }
+ files.forEach((file) => {
+ beerImagePromises.push(
+ DBClient.instance.beerImage.create({
+ data: {
+ alt: body.alt,
+ postedBy: { connect: { id: user!.id } },
+ beerPost: { connect: { id: req.query.id } },
+ path: file.path,
+ caption: body.caption,
+ },
+ }),
+ );
+ });
- return next();
+ const beerImages = await Promise.all(beerImagePromises);
+
+ res.status(200).json({
+ success: true,
+ message: `Successfully uploaded ${beerImages.length} image${
+ beerImages.length > 1 ? 's' : ''
+ }`,
+ statusCode: 200,
+ });
};
const router = createRouter<
@@ -68,8 +84,13 @@ const router = createRouter<
NextApiResponse>
>();
-// @ts-expect-error
-router.post(getCurrentUser, expressWrapper(uploadMiddleware), checkIfBeerPostOwner);
+router.post(
+ getCurrentUser,
+ // @ts-expect-error
+ uploadMiddleware,
+ validateRequest({ bodySchema: BeerPostImageValidationSchema }),
+ processImageData,
+);
const handler = router.handler(NextConnectOptions);
export default handler;
diff --git a/pages/beers/[id].tsx b/pages/beers/[id].tsx
index cb0e65f..08690c9 100644
--- a/pages/beers/[id].tsx
+++ b/pages/beers/[id].tsx
@@ -53,7 +53,7 @@ const BeerByIdPage: NextPage = ({
{beerPost.beerImages[0] && (
{
await prisma.$executeRaw`TRUNCATE TABLE "BreweryPost" CASCADE`;
await prisma.$executeRaw`TRUNCATE TABLE "BeerComment" CASCADE`;
await prisma.$executeRaw`TRUNCATE TABLE "BreweryComment" CASCADE`;
+ await prisma.$executeRaw`TRUNCATE TABLE "BeerPostLike" CASCADE`;
+ await prisma.$executeRaw`TRUNCATE TABLE "BeerImage" CASCADE`;
+ await prisma.$executeRaw`TRUNCATE TABLE "BreweryImage" CASCADE`;
+
+ await prisma.$disconnect();
};
export default cleanDatabase;
diff --git a/prisma/seed/create/createNewBeerImages.ts b/prisma/seed/create/createNewBeerImages.ts
index f71b872..8dbd0c5 100644
--- a/prisma/seed/create/createNewBeerImages.ts
+++ b/prisma/seed/create/createNewBeerImages.ts
@@ -1,15 +1,19 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
-import { BeerPost, BeerImage } from '@prisma/client';
+import { BeerPost, BeerImage, User } from '@prisma/client';
import DBClient from '../../DBClient';
interface CreateNewBeerImagesArgs {
numberOfImages: number;
- beerPosts: BeerPost[];
+
+ joinData: {
+ beerPosts: BeerPost[];
+ users: User[];
+ };
}
const createNewBeerImages = async ({
numberOfImages,
- beerPosts,
+ joinData: { beerPosts, users },
}: CreateNewBeerImagesArgs) => {
const prisma = DBClient.instance;
const createdAt = faker.date.past(1);
@@ -18,12 +22,15 @@ const createNewBeerImages = async ({
// eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfImages; i++) {
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
+ const user = users[Math.floor(Math.random() * users.length)];
beerImagesPromises.push(
prisma.beerImage.create({
data: {
- url: 'https://picsum.photos/900/1600',
+ path: 'https://picsum.photos/900/1600',
alt: 'Placeholder beer image.',
+ caption: 'Placeholder beer image caption.',
beerPost: { connect: { id: beerPost.id } },
+ postedBy: { connect: { id: user.id } },
createdAt,
},
}),
diff --git a/prisma/seed/create/createNewBreweryImages.ts b/prisma/seed/create/createNewBreweryImages.ts
index 67c2909..897b664 100644
--- a/prisma/seed/create/createNewBreweryImages.ts
+++ b/prisma/seed/create/createNewBreweryImages.ts
@@ -1,15 +1,19 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
-import { BreweryPost, BreweryImage } from '@prisma/client';
+import { BreweryPost, BreweryImage, User } from '@prisma/client';
import DBClient from '../../DBClient';
interface CreateBreweryImagesArgs {
numberOfImages: number;
- breweryPosts: BreweryPost[];
+
+ joinData: {
+ breweryPosts: BreweryPost[];
+ users: User[];
+ };
}
const createNewBreweryImages = async ({
numberOfImages,
- breweryPosts,
+ joinData: { breweryPosts, users },
}: CreateBreweryImagesArgs) => {
const prisma = DBClient.instance;
const createdAt = faker.date.past(1);
@@ -18,13 +22,16 @@ const createNewBreweryImages = async ({
// eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfImages; i++) {
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
+ const user = users[Math.floor(Math.random() * users.length)];
breweryImagesPromises.push(
prisma.breweryImage.create({
data: {
- url: 'https://picsum.photos/900/1600',
+ path: 'https://picsum.photos/900/1600',
alt: 'Placeholder brewery image.',
+ caption: 'Placeholder brewery image caption.',
breweryPost: { connect: { id: breweryPost.id } },
+ postedBy: { connect: { id: user.id } },
createdAt,
},
}),
diff --git a/prisma/seed/index.ts b/prisma/seed/index.ts
index 1012ffd..d6b52a1 100644
--- a/prisma/seed/index.ts
+++ b/prisma/seed/index.ts
@@ -54,11 +54,11 @@ import createNewUsers from './create/createNewUsers';
}),
createNewBeerImages({
numberOfImages: 1000,
- beerPosts,
+ joinData: { beerPosts, users },
}),
createNewBreweryImages({
numberOfImages: 1000,
- breweryPosts,
+ joinData: { breweryPosts, users },
}),
]);
diff --git a/services/BeerPost/getAllBeerPosts.ts b/services/BeerPost/getAllBeerPosts.ts
index 6e20cc6..25d7d6c 100644
--- a/services/BeerPost/getAllBeerPosts.ts
+++ b/services/BeerPost/getAllBeerPosts.ts
@@ -34,7 +34,8 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
},
beerImages: {
select: {
- url: true,
+ path: true,
+ caption: true,
id: true,
alt: true,
},
diff --git a/services/BeerPost/getBeerPostById.ts b/services/BeerPost/getBeerPostById.ts
index bc67ca3..1410d29 100644
--- a/services/BeerPost/getBeerPostById.ts
+++ b/services/BeerPost/getBeerPostById.ts
@@ -39,7 +39,8 @@ const getBeerPostById = async (id: string) => {
beerImages: {
select: {
alt: true,
- url: true,
+ path: true,
+ caption: true,
id: true,
},
},
diff --git a/services/BeerPost/getBeerRecommendations.ts b/services/BeerPost/getBeerRecommendations.ts
index 4c7dae1..a64b3ba 100644
--- a/services/BeerPost/getBeerRecommendations.ts
+++ b/services/BeerPost/getBeerRecommendations.ts
@@ -10,7 +10,7 @@ const getBeerRecommendations = async (
NOT: { id: beerPost.id },
},
include: {
- beerImages: { select: { id: true, url: true, alt: true } },
+ beerImages: { select: { id: true, path: true, caption: true, alt: true } },
brewery: { select: { id: true, name: true } },
},
});
diff --git a/services/BeerPost/schema/BeerPostQueryResult.ts b/services/BeerPost/schema/BeerPostQueryResult.ts
index fd08595..7585da3 100644
--- a/services/BeerPost/schema/BeerPostQueryResult.ts
+++ b/services/BeerPost/schema/BeerPostQueryResult.ts
@@ -7,7 +7,8 @@ export default interface BeerPostQueryResult {
};
description: string;
beerImages: {
- url: string;
+ path: string;
+ caption: string;
id: string;
alt: string;
}[];