From 7c4999830f011fd10d261522baa056c291705e8b Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 29 May 2023 19:48:57 +0200 Subject: [PATCH 01/13] chore: grant privileges dev db --- docker/dev.docker-compose.yml | 2 +- docker/makefile | 15 ++++++++++++++- example.env | 8 ++++---- prisma/migrations/migration_lock.toml | 3 +++ 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 prisma/migrations/migration_lock.toml diff --git a/docker/dev.docker-compose.yml b/docker/dev.docker-compose.yml index 39d7d12..bab30c0 100644 --- a/docker/dev.docker-compose.yml +++ b/docker/dev.docker-compose.yml @@ -1,7 +1,7 @@ version: "3.8" services: - my-links-db: + my-links-dev-db: image: mysql:latest restart: always env_file: diff --git a/docker/makefile b/docker/makefile index 19b85a8..4f4fa83 100644 --- a/docker/makefile +++ b/docker/makefile @@ -1,8 +1,21 @@ +CONTAINER_NAME = "docker-my-links-dev-db-1" +ROOT_PASSWORD = "root_passwd" +USER_NAME = "my-user" + start-dev: + @echo 'Starting DB container' docker compose --env-file ../.env -f ./dev.docker-compose.yml up -d + @echo 'Waiting for a minute (need to set $(USER_NAME) privileges)' + @sleep 1m + + @echo 'Grant privileges for $(USER_NAME)' + docker exec -it $(CONTAINER_NAME) mysql -u root -p$(ROOT_PASSWORD) -e "grant ALL PRIVILEGES ON *.* TO '$(USER_NAME)';flush privileges;" + + @echo 'Dont forget to do migrations before run dev' + start-prod: docker-compose --env-file ../.env -f ./docker-compose.yml up -d build: - docker build -f ./Dockerfile -t sonny/my-links ../ \ No newline at end of file + docker build -f ./Dockerfile -t sonny/my-links ../ diff --git a/example.env b/example.env index 0cdedbe..d811e80 100644 --- a/example.env +++ b/example.env @@ -1,7 +1,7 @@ -MYSQL_USER="my_user" -MYSQL_PASSWORD="root" -MYSQL_DATABASE="my-links" -MYSQL_ROOT_PASSWORD="root" +MYSQL_USER="my-user" +MYSQL_PASSWORD="my-user_passwd" +MYSQL_ROOT_PASSWORD="root_passwd" +MYSQL_DATABASE="MyLinks" # Or if you need external Database # DATABASE_IP="localhost" diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5a788a --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "mysql" \ No newline at end of file From e7e7e0c9501e9f88d04f0709aa7200e17e9e6604 Mon Sep 17 00:00:00 2001 From: Sonny Date: Tue, 30 May 2023 01:17:50 +0200 Subject: [PATCH 02/13] feat(wip): add support for multi user in instance --- .../migration.sql | 17 +++++++ .../migration.sql | 11 +++++ prisma/schema.prisma | 48 ++++++++++++------- src/_middleware.ts | 29 +++++++++++ src/constants/paths.ts | 13 ++--- src/pages/api/auth/[...nextauth].ts | 45 +++++++++++++---- src/pages/api/category/create.ts | 19 +++++++- src/pages/signin.tsx | 8 ++-- 8 files changed, 153 insertions(+), 37 deletions(-) create mode 100644 prisma/migrations/20230529174707_add_category_link_author/migration.sql create mode 100644 prisma/migrations/20230529223602_add_link_author/migration.sql create mode 100644 src/_middleware.ts diff --git a/prisma/migrations/20230529174707_add_category_link_author/migration.sql b/prisma/migrations/20230529174707_add_category_link_author/migration.sql new file mode 100644 index 0000000..776ac93 --- /dev/null +++ b/prisma/migrations/20230529174707_add_category_link_author/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the column `nextCategoryId` on the `category` table. All the data in the column will be lost. + - You are about to drop the column `nextLinkId` on the `link` table. All the data in the column will be lost. + - Added the required column `authorId` to the `category` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `category` DROP COLUMN `nextCategoryId`, + ADD COLUMN `authorId` INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE `link` DROP COLUMN `nextLinkId`; + +-- AddForeignKey +ALTER TABLE `category` ADD CONSTRAINT `category_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230529223602_add_link_author/migration.sql b/prisma/migrations/20230529223602_add_link_author/migration.sql new file mode 100644 index 0000000..a275419 --- /dev/null +++ b/prisma/migrations/20230529223602_add_link_author/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `authorId` to the `link` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `link` ADD COLUMN `authorId` INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE `link` ADD CONSTRAINT `link_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5544f00..da2cd28 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -6,14 +6,19 @@ generator client { } datasource db { - provider = "mysql" - url = env("DATABASE_URL") + provider = "mysql" + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } model User { - id Int @id @default(autoincrement()) - google_id String @unique - email String @unique + id Int @id @default(autoincrement()) + google_id String @unique + email String @unique + + categories Category[] + links Link[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -21,26 +26,33 @@ model User { } model Category { - id Int @id @default(autoincrement()) - name String @unique @db.VarChar(255) - links Link[] - nextCategoryId Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + name String @unique @db.VarChar(255) + links Link[] + + author User @relation(fields: [authorId], references: [id]) + authorId Int + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("category") } model Link { - id Int @id @default(autoincrement()) - name String @db.VarChar(255) - url String @db.Text + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + url String @db.Text + category Category @relation(fields: [categoryId], references: [id]) categoryId Int - nextLinkId Int @default(0) - favorite Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + + author User @relation(fields: [authorId], references: [id]) + authorId Int + + favorite Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("link") } diff --git a/src/_middleware.ts b/src/_middleware.ts new file mode 100644 index 0000000..59120f4 --- /dev/null +++ b/src/_middleware.ts @@ -0,0 +1,29 @@ +import PATHS from "constants/paths"; + +export { default } from "next-auth/middleware"; + +// WAIT: for fix - Next.js@13.4.4 seems to be broken +// cf: https://github.com/nextauthjs/next-auth/issues/7650 +// (this file must renamed "middleware.ts") + +export const config = { + matcher: [ + PATHS.HOME, + + PATHS.LINK.CREATE, + PATHS.LINK.EDIT, + PATHS.LINK.REMOVE, + + PATHS.CATEGORY.CREATE, + PATHS.CATEGORY.EDIT, + PATHS.CATEGORY.REMOVE, + + PATHS.API.CATEGORY.CREATE, + PATHS.API.CATEGORY.EDIT, + PATHS.API.CATEGORY.REMOVE, + + PATHS.API.LINK.CREATE, + PATHS.API.LINK.EDIT, + PATHS.API.LINK.REMOVE, + ], +}; diff --git a/src/constants/paths.ts b/src/constants/paths.ts index 133dcb7..ad99d54 100644 --- a/src/constants/paths.ts +++ b/src/constants/paths.ts @@ -1,5 +1,6 @@ const PATHS = { LOGIN: "/signin", + LOGOUT: "/signout", HOME: "/", CATEGORY: { CREATE: "/category/create", @@ -13,14 +14,14 @@ const PATHS = { }, API: { CATEGORY: { - CREATE: "/category/create", - EDIT: "/category/edit", - REMOVE: "/category/remove", + CREATE: "/api/category/create", + EDIT: "/api/category/edit", + REMOVE: "/api/category/remove", }, LINK: { - CREATE: "/link/create", - EDIT: "/link/edit", - REMOVE: "/link/remove", + CREATE: "/api/link/create", + EDIT: "/api/link/edit", + REMOVE: "/api/link/remove", }, }, NOT_FOUND: "/404", diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 35e4061..f0aab3a 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -1,11 +1,12 @@ import { PrismaClient } from "@prisma/client"; import PATHS from "constants/paths"; -import NextAuth from "next-auth"; +import NextAuth, { NextAuthOptions } from "next-auth"; import GoogleProvider from "next-auth/providers/google"; const prisma = new PrismaClient(); -export default NextAuth({ +// TODO: refactor auth +export const authOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, @@ -20,24 +21,33 @@ export default NextAuth({ }), ], callbacks: { + async session({ session }) { + // check if stored in session still exist in db + await prisma.user.findFirstOrThrow({ + where: { email: session.user.email }, + }); + return session; + }, async signIn({ account: accountParam, profile }) { - // TODO: Auth console.log( - "Connexion via", - accountParam.provider, - accountParam.providerAccountId, - profile.email, - profile.name + "[AUTH]", + "User", + profile.name, + "attempt to log in with", + accountParam.provider ); if (accountParam.provider !== "google") { + console.log("[AUTH]", "User", profile.name, "rejeced : bad provider"); return ( PATHS.LOGIN + "?error=" + encodeURI("Authentitifcation via Google requise") ); } + const email = profile?.email; if (email === "") { + console.log("[AUTH]", "User", profile.name, "rejeced : missing email"); return ( PATHS.LOGIN + "?error=" + @@ -48,6 +58,12 @@ export default NextAuth({ } const googleId = profile?.sub; if (googleId === "") { + console.log( + "[AUTH]", + "User", + profile.name, + "rejeced : missing google id" + ); return ( PATHS.LOGIN + "?error=" + @@ -74,6 +90,13 @@ export default NextAuth({ }); return true; } + + console.log( + "[AUTH]", + "User", + profile.name, + "rejeced : not authorized" + ); return ( PATHS.LOGIN + "?error=" + @@ -82,9 +105,11 @@ export default NextAuth({ ) ); } else { + console.log("[AUTH]", "User", profile.name, "success"); return true; } } catch (error) { + console.log("[AUTH]", "User", profile.name, "unhandled error"); console.error(error); return ( PATHS.LOGIN + @@ -97,8 +122,10 @@ export default NextAuth({ pages: { signIn: PATHS.LOGIN, error: PATHS.LOGIN, + signOut: PATHS.LOGOUT, }, session: { maxAge: 60 * 60 * 6, // Session de 6 heures }, -}); +} as NextAuthOptions; +export default NextAuth(authOptions); diff --git a/src/pages/api/category/create.ts b/src/pages/api/category/create.ts index f96f66d..ccd63f6 100644 --- a/src/pages/api/category/create.ts +++ b/src/pages/api/category/create.ts @@ -1,10 +1,15 @@ import { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../auth/[...nextauth]"; + import prisma from "utils/prisma"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { + const session = await getServerSession(req, res, authOptions); // je sais plus d'où ça vient + console.log("session", session); const name = req.body?.name as string; if (!name) { @@ -30,8 +35,20 @@ export default async function handler( } try { + const { id: authorId } = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + }, + }); + if (!authorId) { + throw new Error("Unable to find user"); + } + const category = await prisma.category.create({ - data: { name }, + data: { name, authorId }, }); return res.status(200).send({ success: "Catégorie créée avec succès", diff --git a/src/pages/signin.tsx b/src/pages/signin.tsx index 574b3b7..54dd4f7 100644 --- a/src/pages/signin.tsx +++ b/src/pages/signin.tsx @@ -1,5 +1,6 @@ +import { getServerSession } from "next-auth/next"; import { Provider } from "next-auth/providers"; -import { getProviders, getSession, signIn } from "next-auth/react"; +import { getProviders, signIn } from "next-auth/react"; import { NextSeo } from "next-seo"; import Link from "next/link"; @@ -7,6 +8,7 @@ import MessageManager from "components/MessageManager/MessageManager"; import PATHS from "constants/paths"; import styles from "styles/login.module.scss"; +import { authOptions } from "./api/auth/[...nextauth]"; interface SignInProps { providers: Provider[]; @@ -36,8 +38,8 @@ export default function SignIn({ providers }: SignInProps) { ); } -export async function getServerSideProps(context) { - const session = await getSession(context); +export async function getServerSideProps({ req, res }) { + const session = await getServerSession(req, res, authOptions); if (session) { return { redirect: { From a89fa471e09ec8a14364ea1c9a8e1f2dc4578f57 Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 31 May 2023 19:27:48 +0200 Subject: [PATCH 03/13] feat(wip): create models to access db data --- package-lock.json | 40 +++++++++++++- package.json | 3 +- src/lib/category/getUserCategories.ts | 17 ++++++ src/lib/link/getUserLink.ts | 14 +++++ src/lib/link/getUserLinks.ts | 10 ++++ src/lib/user/getUserOrThrow.ts | 10 ++++ src/pages/api/auth/[...nextauth].ts | 1 + src/pages/api/category/create.ts | 80 ++++++++++----------------- src/pages/api/link/create.ts | 13 +++++ src/pages/category/edit/[cid].tsx | 2 +- src/pages/category/remove/[cid].tsx | 2 +- src/pages/index.tsx | 22 +++++++- src/pages/link/create.tsx | 4 +- src/pages/link/edit/[lid].tsx | 33 ++++------- src/pages/link/remove/[lid].tsx | 2 +- src/types.d.ts | 8 ++- src/utils/api.ts | 12 ++++ src/utils/front.ts | 11 ++-- src/utils/session.ts | 17 ++++++ 19 files changed, 213 insertions(+), 88 deletions(-) create mode 100644 src/lib/category/getUserCategories.ts create mode 100644 src/lib/link/getUserLink.ts create mode 100644 src/lib/link/getUserLinks.ts create mode 100644 src/lib/user/getUserOrThrow.ts create mode 100644 src/utils/api.ts create mode 100644 src/utils/session.ts diff --git a/package-lock.json b/package-lock.json index 13f9c06..038ca36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,8 @@ "react-select": "^5.7.3", "sass": "^1.62.1", "sharp": "^0.32.1", - "toastr": "^2.1.4" + "toastr": "^2.1.4", + "yup": "^1.2.0" }, "devDependencies": { "@types/node": "^20.2.4", @@ -6005,6 +6006,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6811,6 +6817,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -6848,6 +6859,11 @@ "jquery": ">=1.12.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -7161,6 +7177,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", + "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.21.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", diff --git a/package.json b/package.json index 76eeea0..82cb217 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "react-select": "^5.7.3", "sass": "^1.62.1", "sharp": "^0.32.1", - "toastr": "^2.1.4" + "toastr": "^2.1.4", + "yup": "^1.2.0" }, "devDependencies": { "@types/node": "^20.2.4", diff --git a/src/lib/category/getUserCategories.ts b/src/lib/category/getUserCategories.ts new file mode 100644 index 0000000..81bf447 --- /dev/null +++ b/src/lib/category/getUserCategories.ts @@ -0,0 +1,17 @@ +import { User } from "@prisma/client"; +import prisma from "utils/prisma"; + +export default async function getUserCategories(user: User) { + return await prisma.category.findMany({ + where: { + authorId: user.id, + }, + include: { + links: { + where: { + authorId: user.id, + }, + }, + }, + }); +} diff --git a/src/lib/link/getUserLink.ts b/src/lib/link/getUserLink.ts new file mode 100644 index 0000000..54bfe65 --- /dev/null +++ b/src/lib/link/getUserLink.ts @@ -0,0 +1,14 @@ +import { Link, User } from "@prisma/client"; +import prisma from "utils/prisma"; + +export default async function getUserLink(user: User, id: Link["id"]) { + return await prisma.link.findFirst({ + where: { + id, + authorId: user.id, + }, + include: { + category: true, + }, + }); +} diff --git a/src/lib/link/getUserLinks.ts b/src/lib/link/getUserLinks.ts new file mode 100644 index 0000000..7d8de9a --- /dev/null +++ b/src/lib/link/getUserLinks.ts @@ -0,0 +1,10 @@ +import { User } from "@prisma/client"; +import prisma from "utils/prisma"; + +export default async function getUserLinks(user: User) { + return await prisma.link.findMany({ + where: { + authorId: user.id, + }, + }); +} diff --git a/src/lib/user/getUserOrThrow.ts b/src/lib/user/getUserOrThrow.ts new file mode 100644 index 0000000..5ba5b31 --- /dev/null +++ b/src/lib/user/getUserOrThrow.ts @@ -0,0 +1,10 @@ +import { Session } from "next-auth"; +import prisma from "utils/prisma"; + +export default async function getUserOrThrow(session: Session) { + return await prisma.user.findFirstOrThrow({ + where: { + email: session?.user?.email, + }, + }); +} diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index f0aab3a..80becdd 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -33,6 +33,7 @@ export const authOptions = { "[AUTH]", "User", profile.name, + profile.sub, "attempt to log in with", accountParam.provider ); diff --git a/src/pages/api/category/create.ts b/src/pages/api/category/create.ts index ccd63f6..054e102 100644 --- a/src/pages/api/category/create.ts +++ b/src/pages/api/category/create.ts @@ -1,64 +1,40 @@ import { NextApiRequest, NextApiResponse } from "next"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "../auth/[...nextauth]"; +import { checkMethodAllowedOrThrow } from "utils/api"; import prisma from "utils/prisma"; +import { getSessionOrThrow } from "utils/session"; + +export default async function POST(req: NextApiRequest, res: NextApiResponse) { + await checkMethodAllowedOrThrow(req, ["post"]); + const session = await getSessionOrThrow(req, res); -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const session = await getServerSession(req, res, authOptions); // je sais plus d'où ça vient - console.log("session", session); const name = req.body?.name as string; - if (!name) { - return res.status(400).send({ error: "Nom de catégorie manquant" }); + throw new Error("Categorie name missing"); } - try { - const category = await prisma.category.findFirst({ - where: { name }, - }); - - if (category) { - return res - .status(400) - .send({ error: "Une catégorie avec ce nom existe déjà" }); - } - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la création de la catégorie (category/create->findCategory)", - }); + const category = await prisma.category.findFirst({ + where: { name }, + }); + if (category) { + throw new Error("Category name already used"); } - try { - const { id: authorId } = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true, - }, - }); - if (!authorId) { - throw new Error("Unable to find user"); - } + const { id: authorId } = await prisma.user.findFirstOrThrow({ + where: { + email: session.user.email, + }, + select: { + id: true, + }, + }); - const category = await prisma.category.create({ - data: { name, authorId }, - }); - return res.status(200).send({ - success: "Catégorie créée avec succès", - categoryId: category.id, - }); - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la création de la catégorie (category/create->createCategory)", - }); - } + const categoryCreated = await prisma.category.create({ + data: { name, authorId }, + }); + console.log("là"); + return res.status(200).send({ + success: "Category successfully created", + categoryId: categoryCreated.id, + }); } diff --git a/src/pages/api/link/create.ts b/src/pages/api/link/create.ts index eca3f39..02ed944 100644 --- a/src/pages/api/link/create.ts +++ b/src/pages/api/link/create.ts @@ -1,10 +1,19 @@ import { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; + import prisma from "utils/prisma"; +import { authOptions } from "../auth/[...nextauth]"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { + const session = await getServerSession(req, res, authOptions); // je sais plus d'où ça vient + console.log("session", session); + if (!session?.user) { + return res.status(400).send({ error: "You must be connected" }); + } + const name = req.body?.name as string; const url = req.body?.url as string; const favorite = Boolean(req.body?.favorite) || false; @@ -57,12 +66,16 @@ export default async function handler( } try { + const { id: authorId } = await prisma.user.findFirstOrThrow({ + where: { email: session.user.email }, + }); await prisma.link.create({ data: { name, url, categoryId, favorite, + authorId, }, }); return res diff --git a/src/pages/category/edit/[cid].tsx b/src/pages/category/edit/[cid].tsx index 69f6be9..455bec7 100644 --- a/src/pages/category/edit/[cid].tsx +++ b/src/pages/category/edit/[cid].tsx @@ -79,7 +79,7 @@ export async function getServerSideProps({ query }) { const { cid } = query; const categoryDB = await prisma.category.findFirst({ where: { id: Number(cid) }, - include: { links: true }, + include: { links: true, author: true }, }); if (!categoryDB) { diff --git a/src/pages/category/remove/[cid].tsx b/src/pages/category/remove/[cid].tsx index 0974deb..f4ef4e8 100644 --- a/src/pages/category/remove/[cid].tsx +++ b/src/pages/category/remove/[cid].tsx @@ -84,7 +84,7 @@ export async function getServerSideProps({ query }) { const { cid } = query; const categoryDB = await prisma.category.findFirst({ where: { id: Number(cid) }, - include: { links: true }, + include: { links: true, author: true }, }); if (!categoryDB) { diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 9742e5b..6ec8d81 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -16,6 +16,7 @@ import { Category, Link, SearchItem } from "types"; import { BuildCategory } from "utils/front"; import { pushStateVanilla } from "utils/link"; import prisma from "utils/prisma"; +import { getSessionOrThrow } from "utils/session"; interface HomePageProps { categories: Category[]; @@ -185,10 +186,27 @@ function Home(props: HomePageProps) { ); } -export async function getServerSideProps({ query }) { +export async function getServerSideProps({ req, res, query }) { + const session = await getSessionOrThrow(req, res); const queryCategoryId = (query?.categoryId as string) || ""; + + const user = await prisma.user.findFirstOrThrow({ + where: { + email: session.user.email, + }, + }); const categoriesDB = await prisma.category.findMany({ - include: { links: true }, + include: { + links: { + where: { + authorId: user.id, + }, + }, + author: true, + }, + where: { + authorId: user.id, + }, }); if (categoriesDB.length === 0) { diff --git a/src/pages/link/create.tsx b/src/pages/link/create.tsx index 51a7daf..7694ee2 100644 --- a/src/pages/link/create.tsx +++ b/src/pages/link/create.tsx @@ -111,7 +111,9 @@ CreateLink.authRequired = true; export default CreateLink; export async function getServerSideProps() { - const categoriesDB = await prisma.category.findMany(); + const categoriesDB = await prisma.category.findMany({ + include: { author: true }, + }); const categories = categoriesDB.map((categoryDB) => BuildCategory(categoryDB) ); diff --git a/src/pages/link/edit/[lid].tsx b/src/pages/link/edit/[lid].tsx index 37c5bd4..678dab3 100644 --- a/src/pages/link/edit/[lid].tsx +++ b/src/pages/link/edit/[lid].tsx @@ -11,15 +11,13 @@ import TextBox from "components/TextBox"; import useAutoFocus from "hooks/useAutoFocus"; import { Category, Link } from "types"; -import { - BuildCategory, - BuildLink, - HandleAxiosError, - IsValidURL, -} from "utils/front"; -import prisma from "utils/prisma"; +import { HandleAxiosError, IsValidURL } from "utils/front"; +import getUserCategories from "lib/category/getUserCategories"; +import getUserLink from "lib/link/getUserLink"; +import getUserOrThrow from "lib/user/getUserOrThrow"; import styles from "styles/create.module.scss"; +import { getSessionOrThrow } from "utils/session"; function EditLink({ link, @@ -133,20 +131,15 @@ function EditLink({ EditLink.authRequired = true; export default EditLink; -export async function getServerSideProps({ query }) { +export async function getServerSideProps({ req, res, query }) { const { lid } = query; - const categoriesDB = await prisma.category.findMany(); - const categories = categoriesDB.map((categoryDB) => - BuildCategory(categoryDB) - ); + const session = await getSessionOrThrow(req, res); + const user = await getUserOrThrow(session); + const categories = await getUserCategories(user); - const linkDB = await prisma.link.findFirst({ - where: { id: Number(lid) }, - include: { category: true }, - }); - - if (!linkDB) { + const link = await getUserLink(user, Number(lid)); + if (!link) { return { redirect: { destination: "/", @@ -154,10 +147,6 @@ export async function getServerSideProps({ query }) { }; } - const link = BuildLink(linkDB, { - categoryId: linkDB.categoryId, - categoryName: linkDB.category.name, - }); return { props: { link: JSON.parse(JSON.stringify(link)), diff --git a/src/pages/link/remove/[lid].tsx b/src/pages/link/remove/[lid].tsx index a528d91..481e303 100644 --- a/src/pages/link/remove/[lid].tsx +++ b/src/pages/link/remove/[lid].tsx @@ -98,7 +98,7 @@ export async function getServerSideProps({ query }) { const { lid } = query; const linkDB = await prisma.link.findFirst({ where: { id: Number(lid) }, - include: { category: true }, + include: { category: true, author: true }, }); if (!linkDB) { diff --git a/src/types.d.ts b/src/types.d.ts index d5bfb0d..b8e8a38 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,9 +1,12 @@ +import { User } from "@prisma/client"; + export interface Category { id: number; name: string; links: Link[]; - nextCategoryId: number; + authorId: User["id"]; + author: User; createdAt: Date; updatedAt: Date; @@ -20,7 +23,8 @@ export interface Link { name: string; }; - nextLinkId: number; + authorId: User["id"]; + author: User; favorite: boolean; createdAt: Date; diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 0000000..0e23587 --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,12 @@ +import { NextApiRequest } from "next"; + +export function checkMethodAllowedOrThrow( + req: NextApiRequest, + methods: Array +) { + const isMethodAllowed = methods.includes(req.method.toLowerCase()); + if (!isMethodAllowed) { + throw new Error(`Method ${req.method} not allowed`); + } + return isMethodAllowed; +} diff --git a/src/utils/front.ts b/src/utils/front.ts index b368165..00dc492 100644 --- a/src/utils/front.ts +++ b/src/utils/front.ts @@ -5,7 +5,8 @@ import { Category, Link } from "types"; export function BuildCategory({ id, name, - nextCategoryId, + authorId, + author, links = [], createdAt, updatedAt, @@ -16,14 +17,15 @@ export function BuildCategory({ links: links.map((link) => BuildLink(link, { categoryId: id, categoryName: name }) ), - nextCategoryId, + authorId, + author, createdAt, updatedAt, }; } export function BuildLink( - { id, name, url, nextLinkId, favorite, createdAt, updatedAt }, + { id, name, url, authorId, author, favorite, createdAt, updatedAt }, { categoryId, categoryName } ): Link { return { @@ -34,7 +36,8 @@ export function BuildLink( id: categoryId, name: categoryName, }, - nextLinkId, + authorId, + author, favorite, createdAt, updatedAt, diff --git a/src/utils/session.ts b/src/utils/session.ts new file mode 100644 index 0000000..f1a4905 --- /dev/null +++ b/src/utils/session.ts @@ -0,0 +1,17 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; + +import { authOptions } from "pages/api/auth/[...nextauth]"; + +export async function getSessionOrThrow( + req: NextApiRequest, + res: NextApiResponse +) { + const session = await getServerSession(req, res, authOptions); + + if (!session) { + throw new Error("You must be connected"); + } + + return session; +} From cfcc99fa6b4e0e9c6e0acfdaa8aa8db83190f078 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 3 Jun 2023 20:09:26 +0200 Subject: [PATCH 04/13] feat: api handler --- src/_middleware.ts | 29 ----------- src/lib/api/handler.ts | 93 ++++++++++++++++++++++++++++++++++ src/lib/user/getUserOrThrow.ts | 3 ++ src/utils/session.ts | 6 ++- 4 files changed, 101 insertions(+), 30 deletions(-) delete mode 100644 src/_middleware.ts create mode 100644 src/lib/api/handler.ts diff --git a/src/_middleware.ts b/src/_middleware.ts deleted file mode 100644 index 59120f4..0000000 --- a/src/_middleware.ts +++ /dev/null @@ -1,29 +0,0 @@ -import PATHS from "constants/paths"; - -export { default } from "next-auth/middleware"; - -// WAIT: for fix - Next.js@13.4.4 seems to be broken -// cf: https://github.com/nextauthjs/next-auth/issues/7650 -// (this file must renamed "middleware.ts") - -export const config = { - matcher: [ - PATHS.HOME, - - PATHS.LINK.CREATE, - PATHS.LINK.EDIT, - PATHS.LINK.REMOVE, - - PATHS.CATEGORY.CREATE, - PATHS.CATEGORY.EDIT, - PATHS.CATEGORY.REMOVE, - - PATHS.API.CATEGORY.CREATE, - PATHS.API.CATEGORY.EDIT, - PATHS.API.CATEGORY.REMOVE, - - PATHS.API.LINK.CREATE, - PATHS.API.LINK.EDIT, - PATHS.API.LINK.REMOVE, - ], -}; diff --git a/src/lib/api/handler.ts b/src/lib/api/handler.ts new file mode 100644 index 0000000..18d60ae --- /dev/null +++ b/src/lib/api/handler.ts @@ -0,0 +1,93 @@ +import { User } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { NextApiRequest, NextApiResponse } from "next"; +import { Session } from "next-auth"; + +import getUserOrThrow from "lib/user/getUserOrThrow"; +import { getSessionOrThrow } from "utils/session"; + +type ApiHandlerMethod = ({ + req, + res, + session, + user, +}: { + req: NextApiRequest; + res: NextApiResponse; + session: Session; + user: User; +}) => Promise; + +// This API Handler strongly inspired by +// Source: https://jasonwatmore.com/next-js-13-middleware-for-authentication-and-error-handling-on-api-routes + +export function apiHandler(handler: { + get?: ApiHandlerMethod; + post?: ApiHandlerMethod; + put?: ApiHandlerMethod; + patch?: ApiHandlerMethod; + delete?: ApiHandlerMethod; +}) { + return async (req: NextApiRequest, res: NextApiResponse) => { + const method = req.method.toLowerCase(); + if (!handler[method]) + return res + .status(405) + .json({ error: `Method ${req.method} Not Allowed` }); + + try { + const session = await getSessionOrThrow(req, res); + const user = await getUserOrThrow(session); + + await handler[method]({ req, res, session, user }); + } catch (err) { + errorHandler(err, res); + } + }; +} + +function errorHandler(error: any, response: NextApiResponse) { + if (typeof error === "string") { + const is404 = error.toLowerCase().endsWith("not found"); + const statusCode = is404 ? 404 : 400; + + return response.status(statusCode).json({ message: error }); + } + + // does not fit with current error throwed + // TODO: fix errors returned + // by getSessionOrThrow or getUserOrThrow + if (error.name === "UnauthorizedError") { + // authentication error + return response.status(401).json({ message: "You must be connected" }); + } + + const errorMessage = + error.constructor.name === "PrismaClientKnownRequestError" + ? handlePrismaError(error) // Handle Prisma specific errors + : error.message; + + console.error(error); + return response.status(500).json({ message: errorMessage }); +} + +function handlePrismaError({ + meta, + code, + message, +}: PrismaClientKnownRequestError) { + switch (code) { + case "P2002": + return `Duplicate field value: ${meta.target}`; + case "P2003": + return `Foreign key constraint failed on the field: ${meta.field_name}`; + case "P2014": + return `Invalid ID: ${meta.target}`; + case "P2003": + return `Invalid input data: ${meta.target}`; + + // Details should not leak to client, be carreful with this + default: + return `Something went wrong: ${message}`; + } +} diff --git a/src/lib/user/getUserOrThrow.ts b/src/lib/user/getUserOrThrow.ts index 5ba5b31..0f9626a 100644 --- a/src/lib/user/getUserOrThrow.ts +++ b/src/lib/user/getUserOrThrow.ts @@ -2,6 +2,9 @@ import { Session } from "next-auth"; import prisma from "utils/prisma"; export default async function getUserOrThrow(session: Session) { + if (!session || session === null) { + throw new Error("You must be connected"); + } return await prisma.user.findFirstOrThrow({ where: { email: session?.user?.email, diff --git a/src/utils/session.ts b/src/utils/session.ts index f1a4905..ba428d7 100644 --- a/src/utils/session.ts +++ b/src/utils/session.ts @@ -3,11 +3,15 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "pages/api/auth/[...nextauth]"; +export async function getSession(req: NextApiRequest, res: NextApiResponse) { + return await getServerSession(req, res, authOptions); +} + export async function getSessionOrThrow( req: NextApiRequest, res: NextApiResponse ) { - const session = await getServerSession(req, res, authOptions); + const session = await getSession(req, res); if (!session) { throw new Error("You must be connected"); From a6f32e852cc7335d7244006d1e77f795b33fc233 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 3 Jun 2023 20:12:21 +0200 Subject: [PATCH 05/13] feat: create functions to get user specific data Such as link, category or user related --- src/lib/category/getUserCategories.ts | 4 ++-- src/lib/category/getUserCategory.ts | 18 ++++++++++++++++++ src/lib/link/getLinkFromCategoryByName.ts | 19 +++++++++++++++++++ src/lib/user/getUser.ts | 10 ++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/lib/category/getUserCategory.ts create mode 100644 src/lib/link/getLinkFromCategoryByName.ts create mode 100644 src/lib/user/getUser.ts diff --git a/src/lib/category/getUserCategories.ts b/src/lib/category/getUserCategories.ts index 81bf447..6cbeeb5 100644 --- a/src/lib/category/getUserCategories.ts +++ b/src/lib/category/getUserCategories.ts @@ -4,12 +4,12 @@ import prisma from "utils/prisma"; export default async function getUserCategories(user: User) { return await prisma.category.findMany({ where: { - authorId: user.id, + authorId: user?.id, }, include: { links: { where: { - authorId: user.id, + authorId: user?.id, }, }, }, diff --git a/src/lib/category/getUserCategory.ts b/src/lib/category/getUserCategory.ts new file mode 100644 index 0000000..e7272ad --- /dev/null +++ b/src/lib/category/getUserCategory.ts @@ -0,0 +1,18 @@ +import { Category, User } from "@prisma/client"; +import prisma from "utils/prisma"; + +export default async function getUserCategory(user: User, id: Category["id"]) { + return await prisma.category.findFirst({ + where: { + authorId: user?.id, + id, + }, + include: { + links: { + where: { + authorId: user?.id, + }, + }, + }, + }); +} diff --git a/src/lib/link/getLinkFromCategoryByName.ts b/src/lib/link/getLinkFromCategoryByName.ts new file mode 100644 index 0000000..4bdaa3f --- /dev/null +++ b/src/lib/link/getLinkFromCategoryByName.ts @@ -0,0 +1,19 @@ +import { Category, Link, User } from "@prisma/client"; +import prisma from "utils/prisma"; + +export default async function getLinkFromCategoryByName( + user: User, + name: Link["name"], + categoryId: Category["id"] +) { + return await prisma.link.findFirst({ + where: { + authorId: user.id, + name, + categoryId, + }, + include: { + category: true, + }, + }); +} diff --git a/src/lib/user/getUser.ts b/src/lib/user/getUser.ts new file mode 100644 index 0000000..695a3ea --- /dev/null +++ b/src/lib/user/getUser.ts @@ -0,0 +1,10 @@ +import { Session } from "next-auth"; +import prisma from "utils/prisma"; + +export default async function getUser(session: Session) { + return await prisma.user.findFirst({ + where: { + email: session?.user?.email, + }, + }); +} From 35829522c3012e2c80ce5390fba272e3801efc45 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 3 Jun 2023 20:14:54 +0200 Subject: [PATCH 06/13] refactor: use data retrieval functions --- src/constants/url.ts | 2 ++ src/pages/index.tsx | 33 +++++----------------- src/pages/link/create.tsx | 17 ++++++------ src/pages/link/remove/[lid].tsx | 22 +++++++-------- src/pages/signin.tsx | 5 ++-- src/types.d.ts | 2 ++ src/utils/front.ts | 49 ++------------------------------- 7 files changed, 33 insertions(+), 97 deletions(-) create mode 100644 src/constants/url.ts diff --git a/src/constants/url.ts b/src/constants/url.ts new file mode 100644 index 0000000..7edc2c2 --- /dev/null +++ b/src/constants/url.ts @@ -0,0 +1,2 @@ +export const VALID_URL_REGEX = + /^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6ec8d81..490568d 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -13,10 +13,10 @@ import PATHS from "constants/paths"; import useModal from "hooks/useModal"; import { Category, Link, SearchItem } from "types"; -import { BuildCategory } from "utils/front"; +import getUserCategories from "lib/category/getUserCategories"; +import getUser from "lib/user/getUser"; import { pushStateVanilla } from "utils/link"; -import prisma from "utils/prisma"; -import { getSessionOrThrow } from "utils/session"; +import { getSession } from "utils/session"; interface HomePageProps { categories: Category[]; @@ -187,29 +187,12 @@ function Home(props: HomePageProps) { } export async function getServerSideProps({ req, res, query }) { - const session = await getSessionOrThrow(req, res); + const session = await getSession(req, res); const queryCategoryId = (query?.categoryId as string) || ""; - const user = await prisma.user.findFirstOrThrow({ - where: { - email: session.user.email, - }, - }); - const categoriesDB = await prisma.category.findMany({ - include: { - links: { - where: { - authorId: user.id, - }, - }, - author: true, - }, - where: { - authorId: user.id, - }, - }); - - if (categoriesDB.length === 0) { + const user = await getUser(session); + const categories = await getUserCategories(user); + if (categories.length === 0) { return { redirect: { destination: PATHS.CATEGORY.CREATE, @@ -217,11 +200,9 @@ export async function getServerSideProps({ req, res, query }) { }; } - const categories = categoriesDB.map(BuildCategory); const currentCategory = categories.find( ({ id }) => id === Number(queryCategoryId) ); - return { props: { categories: JSON.parse(JSON.stringify(categories)), diff --git a/src/pages/link/create.tsx b/src/pages/link/create.tsx index 7694ee2..aceda9e 100644 --- a/src/pages/link/create.tsx +++ b/src/pages/link/create.tsx @@ -10,9 +10,11 @@ import Selector from "components/Selector"; import TextBox from "components/TextBox"; import useAutoFocus from "hooks/useAutoFocus"; +import getUserCategories from "lib/category/getUserCategories"; +import getUser from "lib/user/getUser"; import { Category, Link } from "types"; -import { BuildCategory, HandleAxiosError, IsValidURL } from "utils/front"; -import prisma from "utils/prisma"; +import { HandleAxiosError, IsValidURL } from "utils/front"; +import { getSession } from "utils/session"; import styles from "styles/create.module.scss"; @@ -110,14 +112,11 @@ function CreateLink({ categories }: { categories: Category[] }) { CreateLink.authRequired = true; export default CreateLink; -export async function getServerSideProps() { - const categoriesDB = await prisma.category.findMany({ - include: { author: true }, - }); - const categories = categoriesDB.map((categoryDB) => - BuildCategory(categoryDB) - ); +export async function getServerSideProps({ req, res }) { + const session = await getSession(req, res); + const user = await getUser(session); + const categories = await getUserCategories(user); if (categories.length === 0) { return { redirect: { diff --git a/src/pages/link/remove/[lid].tsx b/src/pages/link/remove/[lid].tsx index 481e303..6fe10a5 100644 --- a/src/pages/link/remove/[lid].tsx +++ b/src/pages/link/remove/[lid].tsx @@ -8,9 +8,11 @@ import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; +import getUserLink from "lib/link/getUserLink"; +import getUser from "lib/user/getUser"; import { Link } from "types"; -import { BuildLink, HandleAxiosError } from "utils/front"; -import prisma from "utils/prisma"; +import { HandleAxiosError } from "utils/front"; +import { getSession } from "utils/session"; import styles from "styles/create.module.scss"; @@ -94,14 +96,14 @@ function RemoveLink({ link }: { link: Link }) { RemoveLink.authRequired = true; export default RemoveLink; -export async function getServerSideProps({ query }) { +export async function getServerSideProps({ req, res, query }) { const { lid } = query; - const linkDB = await prisma.link.findFirst({ - where: { id: Number(lid) }, - include: { category: true, author: true }, - }); - if (!linkDB) { + const session = await getSession(req, res); + const user = await getUser(session); + + const link = await getUserLink(user, Number(lid)); + if (!link) { return { redirect: { destination: "/", @@ -109,10 +111,6 @@ export async function getServerSideProps({ query }) { }; } - const link = BuildLink(linkDB, { - categoryId: linkDB.categoryId, - categoryName: linkDB.category.name, - }); return { props: { link: JSON.parse(JSON.stringify(link)), diff --git a/src/pages/signin.tsx b/src/pages/signin.tsx index 54dd4f7..44e3aef 100644 --- a/src/pages/signin.tsx +++ b/src/pages/signin.tsx @@ -1,4 +1,3 @@ -import { getServerSession } from "next-auth/next"; import { Provider } from "next-auth/providers"; import { getProviders, signIn } from "next-auth/react"; import { NextSeo } from "next-seo"; @@ -6,9 +5,9 @@ import Link from "next/link"; import MessageManager from "components/MessageManager/MessageManager"; import PATHS from "constants/paths"; +import { getSession } from "utils/session"; import styles from "styles/login.module.scss"; -import { authOptions } from "./api/auth/[...nextauth]"; interface SignInProps { providers: Provider[]; @@ -39,7 +38,7 @@ export default function SignIn({ providers }: SignInProps) { } export async function getServerSideProps({ req, res }) { - const session = await getServerSession(req, res, authOptions); + const session = await getSession(req, res); if (session) { return { redirect: { diff --git a/src/types.d.ts b/src/types.d.ts index b8e8a38..b561a77 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,5 +1,7 @@ import { User } from "@prisma/client"; +// TODO: extend @prisma/client type with Link[] instead of +// recreate interface (same for Link) export interface Category { id: number; name: string; diff --git a/src/utils/front.ts b/src/utils/front.ts index 00dc492..ff5fd25 100644 --- a/src/utils/front.ts +++ b/src/utils/front.ts @@ -1,53 +1,8 @@ import axios from "axios"; - -import { Category, Link } from "types"; - -export function BuildCategory({ - id, - name, - authorId, - author, - links = [], - createdAt, - updatedAt, -}): Category { - return { - id, - name, - links: links.map((link) => - BuildLink(link, { categoryId: id, categoryName: name }) - ), - authorId, - author, - createdAt, - updatedAt, - }; -} - -export function BuildLink( - { id, name, url, authorId, author, favorite, createdAt, updatedAt }, - { categoryId, categoryName } -): Link { - return { - id, - name, - url, - category: { - id: categoryId, - name: categoryName, - }, - authorId, - author, - favorite, - createdAt, - updatedAt, - }; -} +import { VALID_URL_REGEX } from "constants/url"; export function IsValidURL(url: string): boolean { - const regex = new RegExp( - /^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/ - ); + const regex = new RegExp(VALID_URL_REGEX); return url.match(regex) ? true : false; } From 678da44b802532f1d8e03414d7b7f5dc8756fc86 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 3 Jun 2023 20:17:20 +0200 Subject: [PATCH 07/13] refactor: api link endpoint - edit & delete --- src/pages/api/link/[lid].ts | 82 ++++++++++++++++++++++++++++++ src/pages/api/link/edit/[lid].ts | 65 ----------------------- src/pages/api/link/remove/[lid].ts | 42 --------------- 3 files changed, 82 insertions(+), 107 deletions(-) create mode 100644 src/pages/api/link/[lid].ts delete mode 100644 src/pages/api/link/edit/[lid].ts delete mode 100644 src/pages/api/link/remove/[lid].ts diff --git a/src/pages/api/link/[lid].ts b/src/pages/api/link/[lid].ts new file mode 100644 index 0000000..1d4d621 --- /dev/null +++ b/src/pages/api/link/[lid].ts @@ -0,0 +1,82 @@ +import { boolean, number, object, string } from "yup"; + +import { VALID_URL_REGEX } from "constants/url"; +import { apiHandler } from "lib/api/handler"; +import getUserLink from "lib/link/getUserLink"; +import prisma from "utils/prisma"; + +export default apiHandler({ + put: editLink, + delete: deleteLink, +}); + +const querySchema = object({ + lid: number().required(), +}); + +// FIXME: code duplicated from api/link/create +const bodySchema = object({ + name: string() + .trim() + .required("name is required") + .max(32, "name is too long"), + url: string() + .trim() + .required("url is required") + .matches(VALID_URL_REGEX, "invalid url format"), + categoryId: number().required("categoryId must be a number"), + favorite: boolean().default(() => false), +}).typeError("Missing request Body"); + +async function editLink({ req, res, user }) { + const { lid } = await querySchema.validate(req.query); + const { name, url, favorite, categoryId } = await bodySchema.validate( + req.body + ); + + const link = await getUserLink(user, lid); + if (!link) { + throw new Error("Unable to find link " + lid); + } + + if ( + link.name === name && + link.url === url && + link.favorite === favorite && + link.categoryId === categoryId + ) { + throw new Error("You must update at least one field"); + } + + await prisma.link.update({ + where: { id: Number(lid) }, + data: { + name, + url, + favorite, + categoryId, + }, + }); + + return res + .status(200) + .send({ success: "Link successfully updated", categoryId }); +} + +async function deleteLink({ req, res, user }) { + const { lid } = await querySchema.validate(req.query); + + const link = await getUserLink(user, lid); + if (!link) { + throw new Error("Unable to find link " + lid); + } + + await prisma.link.delete({ + where: { id: Number(lid) }, + }); + + return res.send({ + success: "Link successfully deleted", + categoryId: link.categoryId, + }); +} diff --git a/src/pages/api/link/edit/[lid].ts b/src/pages/api/link/edit/[lid].ts deleted file mode 100644 index b66f192..0000000 --- a/src/pages/api/link/edit/[lid].ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import prisma from "utils/prisma"; - -// TODO: Ajouter vérification -> l'utilisateur doit changer au moins un champ -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const { lid } = req.query; - - try { - const link = await prisma.link.findFirst({ - where: { id: Number(lid) }, - }); - - if (!link) { - return res.status(400).send({ error: "Lien introuvable" }); - } - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de l'édition du lien (link/edit->findLink)", - }); - } - - const name = req.body?.name as string; - const url = req.body?.url as string; - const favorite = Boolean(req.body?.favorite) || false; - const categoryId = Number(req.body?.categoryId); - - if (!name) { - return res.status(400).send({ error: "Nom du lien manquant" }); - } - - if (!url) { - return res.status(400).send({ error: "URL du lien manquant" }); - } - - if (!categoryId) { - return res.status(400).send({ error: "Catégorie du lien manquante" }); - } - - try { - await prisma.link.update({ - where: { id: Number(lid) }, - data: { - name, - url, - favorite, - categoryId, - }, - }); - - return res - .status(200) - .send({ success: "Lien mis à jour avec succès", categoryId }); - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de l'édition du lien (link/remove->updateLink)", - }); - } -} diff --git a/src/pages/api/link/remove/[lid].ts b/src/pages/api/link/remove/[lid].ts deleted file mode 100644 index ccdb086..0000000 --- a/src/pages/api/link/remove/[lid].ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import prisma from "utils/prisma"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const { lid } = req.query; - - try { - const link = await prisma.link.findFirst({ - where: { id: Number(lid) }, - }); - - if (!link) { - return res.status(400).send({ error: "Lien introuvable" }); - } - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la suppression du lien (link/remove->findLink)", - }); - } - - try { - const link = await prisma.link.delete({ - where: { id: Number(lid) }, - }); - - return res.status(200).send({ - success: "Le lien a été supprimé avec succès", - categoryId: link.categoryId, - }); - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la suppression du lien (link/remove->deleteLink)", - }); - } -} From f5986d34ad9332f027506ed78830a93330bb6beb Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 3 Jun 2023 20:17:39 +0200 Subject: [PATCH 08/13] refactor: create api link endpoint --- src/pages/api/link/create.ts | 126 ++++++++++++----------------------- 1 file changed, 44 insertions(+), 82 deletions(-) diff --git a/src/pages/api/link/create.ts b/src/pages/api/link/create.ts index 02ed944..20a9e2c 100644 --- a/src/pages/api/link/create.ts +++ b/src/pages/api/link/create.ts @@ -1,91 +1,53 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { getServerSession } from "next-auth/next"; +import { boolean, number, object, string } from "yup"; +import { apiHandler } from "lib/api/handler"; +import getUserCategory from "lib/category/getUserCategory"; +import getUserLinkByName from "lib/link/getLinkFromCategoryByName"; + +import { VALID_URL_REGEX } from "constants/url"; import prisma from "utils/prisma"; -import { authOptions } from "../auth/[...nextauth]"; -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const session = await getServerSession(req, res, authOptions); // je sais plus d'où ça vient - console.log("session", session); - if (!session?.user) { - return res.status(400).send({ error: "You must be connected" }); +export default apiHandler({ + post: createLink, +}); + +const bodySchema = object({ + name: string() + .trim() + .required("name is required") + .max(32, "name is too long"), + url: string() + .trim() + .required("url is required") + .matches(VALID_URL_REGEX, "invalid url format"), + categoryId: number().required("categoryId must be a number"), + favorite: boolean().default(() => false), +}).typeError("Missing request Body"); + +async function createLink({ req, res, user }) { + const { name, url, favorite, categoryId } = await bodySchema.validate( + req.body + ); + + const link = await getUserLinkByName(user, name, categoryId); + if (link) { + throw new Error("Link name is already used in this category"); } - const name = req.body?.name as string; - const url = req.body?.url as string; - const favorite = Boolean(req.body?.favorite) || false; - const categoryId = Number(req.body?.categoryId); - - if (!name) { - return res.status(400).send({ error: "Nom du lien manquant" }); + const category = await getUserCategory(user, categoryId); + if (!category) { + throw new Error("Unable to find category " + categoryId); } - if (!url) { - return res.status(400).send({ error: "URL du lien manquant" }); - } + await prisma.link.create({ + data: { + name, + url, + categoryId, + favorite, + authorId: user.id, + }, + }); - if (!categoryId) { - return res.status(400).send({ error: "Catégorie du lien manquante" }); - } - - try { - const link = await prisma.link.findFirst({ - where: { name, categoryId }, - }); - - if (link) { - return res.status(400).send({ - error: "Un lien avec ce nom existe déjà dans cette catégorie", - }); - } - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la création du lien (link/create->findLink)", - }); - } - - try { - const category = await prisma.category.findFirst({ - where: { id: categoryId }, - }); - - if (!category) { - return res.status(400).send({ error: "Cette catégorie n'existe pas" }); - } - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la création du lien (link/create->findCategory)", - }); - } - - try { - const { id: authorId } = await prisma.user.findFirstOrThrow({ - where: { email: session.user.email }, - }); - await prisma.link.create({ - data: { - name, - url, - categoryId, - favorite, - authorId, - }, - }); - return res - .status(200) - .send({ success: "Lien créé avec succès", categoryId }); - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la création du lien (link/create->createLink)", - }); - } + return res.send({ success: "Link successfully created", categoryId }); } From e400e688bbc41fba60a397611ba97ac827a63aea Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 3 Jun 2023 23:40:45 +0200 Subject: [PATCH 09/13] refactor: rename api/link/create file --- src/pages/api/link/{create.ts => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/pages/api/link/{create.ts => index.ts} (100%) diff --git a/src/pages/api/link/create.ts b/src/pages/api/link/index.ts similarity index 100% rename from src/pages/api/link/create.ts rename to src/pages/api/link/index.ts From a44fcd3dc9ef70e3c8d5fa5f8a94515b72aa7c49 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 3 Jun 2023 23:56:48 +0200 Subject: [PATCH 10/13] refactor: get/create categories endpoints --- src/lib/category/getUserCategoryByName.ts | 11 +++++ src/pages/api/category/create.ts | 40 ----------------- src/pages/api/category/index.ts | 55 ++++++++++++++++------- src/utils/api.ts | 12 ----- 4 files changed, 49 insertions(+), 69 deletions(-) create mode 100644 src/lib/category/getUserCategoryByName.ts delete mode 100644 src/pages/api/category/create.ts delete mode 100644 src/utils/api.ts diff --git a/src/lib/category/getUserCategoryByName.ts b/src/lib/category/getUserCategoryByName.ts new file mode 100644 index 0000000..07b26de --- /dev/null +++ b/src/lib/category/getUserCategoryByName.ts @@ -0,0 +1,11 @@ +import { Category, User } from "@prisma/client"; +import prisma from "utils/prisma"; + +export default async function getUserCategoryByName( + user: User, + name: Category["name"] +) { + return await prisma.category.findFirst({ + where: { name, authorId: user.id }, + }); +} diff --git a/src/pages/api/category/create.ts b/src/pages/api/category/create.ts deleted file mode 100644 index 054e102..0000000 --- a/src/pages/api/category/create.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -import { checkMethodAllowedOrThrow } from "utils/api"; -import prisma from "utils/prisma"; -import { getSessionOrThrow } from "utils/session"; - -export default async function POST(req: NextApiRequest, res: NextApiResponse) { - await checkMethodAllowedOrThrow(req, ["post"]); - const session = await getSessionOrThrow(req, res); - - const name = req.body?.name as string; - if (!name) { - throw new Error("Categorie name missing"); - } - - const category = await prisma.category.findFirst({ - where: { name }, - }); - if (category) { - throw new Error("Category name already used"); - } - - const { id: authorId } = await prisma.user.findFirstOrThrow({ - where: { - email: session.user.email, - }, - select: { - id: true, - }, - }); - - const categoryCreated = await prisma.category.create({ - data: { name, authorId }, - }); - console.log("là"); - return res.status(200).send({ - success: "Category successfully created", - categoryId: categoryCreated.id, - }); -} diff --git a/src/pages/api/category/index.ts b/src/pages/api/category/index.ts index 8538a81..34ec9c4 100644 --- a/src/pages/api/category/index.ts +++ b/src/pages/api/category/index.ts @@ -1,20 +1,41 @@ -import { NextApiRequest, NextApiResponse } from "next"; +import { apiHandler } from "lib/api/handler"; +import getUserCategories from "lib/category/getUserCategories"; +import getUserCategoryByName from "lib/category/getUserCategoryByName"; import prisma from "utils/prisma"; +import { object, string } from "yup"; -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - try { - const categories = await prisma.category.findMany(); - console.log("request"); - return res.status(200).send({ - categories, - }); - } catch (error) { - console.error(error); - return res.status(400).send({ - error: "Une erreur est survenue lors de la récupération des catégories", - }); - } +export default apiHandler({ + get: getCatgories, + post: createCategory, +}); + +async function getCatgories({ res, user }) { + const categories = await getUserCategories(user); + return res.status(200).send({ + categories, + }); +} + +const bodySchema = object({ + name: string() + .trim() + .required("name is required") + .max(32, "name is too long"), +}).typeError("Missing request Body"); + +async function createCategory({ req, res, user }) { + const { name } = await bodySchema.validate(req.body); + + const category = await getUserCategoryByName(user, name); + if (category) { + throw new Error("Category name already used"); + } + + const categoryCreated = await prisma.category.create({ + data: { name, authorId: user.id }, + }); + return res.status(200).send({ + success: "Category successfully created", + categoryId: categoryCreated.id, + }); } diff --git a/src/utils/api.ts b/src/utils/api.ts deleted file mode 100644 index 0e23587..0000000 --- a/src/utils/api.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextApiRequest } from "next"; - -export function checkMethodAllowedOrThrow( - req: NextApiRequest, - methods: Array -) { - const isMethodAllowed = methods.includes(req.method.toLowerCase()); - if (!isMethodAllowed) { - throw new Error(`Method ${req.method} not allowed`); - } - return isMethodAllowed; -} From 7cd0a92562d6518c2ecac1754bcd3365d5a45585 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 4 Jun 2023 00:12:33 +0200 Subject: [PATCH 11/13] refactor: edit & remove category endpoint --- src/pages/api/category/[cid].ts | 67 ++++++++++++++++++++++++++ src/pages/api/category/edit/[cid].ts | 53 -------------------- src/pages/api/category/remove/[cid].ts | 41 ---------------- 3 files changed, 67 insertions(+), 94 deletions(-) create mode 100644 src/pages/api/category/[cid].ts delete mode 100644 src/pages/api/category/edit/[cid].ts delete mode 100644 src/pages/api/category/remove/[cid].ts diff --git a/src/pages/api/category/[cid].ts b/src/pages/api/category/[cid].ts new file mode 100644 index 0000000..4135959 --- /dev/null +++ b/src/pages/api/category/[cid].ts @@ -0,0 +1,67 @@ +import { number, object, string } from "yup"; + +import { apiHandler } from "lib/api/handler"; +import getUserCategory from "lib/category/getUserCategory"; +import prisma from "utils/prisma"; +import getUserCategoryByName from "lib/category/getUserCategoryByName"; + +export default apiHandler({ + put: editCategory, + delete: deleteCategory, +}); + +const querySchema = object({ + cid: number().required(), +}); + +const bodySchema = object({ + name: string() + .trim() + .required("name is required") + .max(32, "name is too long"), +}).typeError("Missing request Body"); + +async function editCategory({ req, res, user }) { + const { cid } = await querySchema.validate(req.query); + const { name } = await bodySchema.validate(req.body); + + const category = await getUserCategory(user, cid); + if (!category) { + throw new Error("Unable to find category " + cid); + } + + const isCategoryNameAlreadyused = await getUserCategoryByName(user, name); + if (isCategoryNameAlreadyused) { + throw new Error("Category name already used"); + } + + if (category.name === name) { + throw new Error("New category name must be different"); + } + + await prisma.category.update({ + where: { id: cid }, + data: { name }, + }); + return res.send({ + success: "Category successfully updated", + categoryId: category.id, + }); +} + +async function deleteCategory({ req, res, user }) { + const { cid } = await querySchema.validate(req.query); + + const category = await getUserCategory(user, cid); + if (!category) { + throw new Error("Unable to find category " + cid); + } + + await prisma.category.delete({ + where: { id: cid }, + }); + return res.send({ + success: "Category successfully deleted", + categoryId: category.id, + }); +} diff --git a/src/pages/api/category/edit/[cid].ts b/src/pages/api/category/edit/[cid].ts deleted file mode 100644 index 8c611fa..0000000 --- a/src/pages/api/category/edit/[cid].ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import prisma from "utils/prisma"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const { cid } = req.query; - - let category; - try { - category = await prisma.category.findFirst({ - where: { id: Number(cid) }, - }); - - if (!category) { - return res.status(400).send({ error: "Catégorie introuvable" }); - } - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de l'édition de la catégorie (category/edit->findCategory)", - }); - } - - const name = req.body?.name as string; - if (!name) { - return res.status(400).send({ error: "Nom de la catégorie manquante" }); - } else if (name === category.name) { - return res.status(400).send({ - error: "Le nom de la catégorie doit être différent du nom actuel", - }); - } - - try { - const category = await prisma.category.update({ - where: { id: Number(cid) }, - data: { name }, - }); - - return res.status(200).send({ - success: "Catégorie mise à jour avec succès", - categoryId: category.id, - }); - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de l'édition de la catégorie (category/edit->updateCategory)", - }); - } -} diff --git a/src/pages/api/category/remove/[cid].ts b/src/pages/api/category/remove/[cid].ts deleted file mode 100644 index 9813535..0000000 --- a/src/pages/api/category/remove/[cid].ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import prisma from "utils/prisma"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const { cid } = req.query; - - try { - const category = await prisma.category.findFirst({ - where: { id: Number(cid) }, - }); - - if (!category) { - return res.status(400).send({ error: "Categorie introuvable" }); - } - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la suppression de la catégorie (category/remove->findCategory)", - }); - } - - try { - await prisma.category.delete({ - where: { id: Number(cid) }, - }); - - return res - .status(200) - .send({ success: "La catégorie a été supprimée avec succès" }); - } catch (error) { - console.error(error); - return res.status(400).send({ - error: - "Une erreur est survenue lors de la suppression de la catégorie (category/remove->deleteCategory)", - }); - } -} From 5c2c2de1bd1524da6e701cdd1035b4acafe9040c Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 4 Jun 2023 00:43:02 +0200 Subject: [PATCH 12/13] refactor: update front api endpoints --- src/pages/category/create.tsx | 3 +++ src/pages/category/edit/[cid].tsx | 32 ++++++++++++++--------------- src/pages/category/remove/[cid].tsx | 26 ++++++++++++----------- src/pages/link/create.tsx | 7 ++++--- src/pages/link/edit/[lid].tsx | 10 +++++---- src/pages/link/remove/[lid].tsx | 7 ++++--- 6 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/pages/category/create.tsx b/src/pages/category/create.tsx index c609806..29eaad3 100644 --- a/src/pages/category/create.tsx +++ b/src/pages/category/create.tsx @@ -7,6 +7,7 @@ import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; +import PATHS from "constants/paths"; import useAutoFocus from "hooks/useAutoFocus"; import { redirectWithoutClientCache } from "utils/client"; import { HandleAxiosError } from "utils/front"; @@ -36,8 +37,10 @@ function CreateCategory() { try { const { data } = await axios.post("/api/category/create", { name }); + const { data } = await axios.post(PATHS.API.CATEGORY, { name }); redirectWithoutClientCache(router, ""); router.push(`/?categoryId=${data?.categoryId}`); + router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`); setSubmitted(true); } catch (error) { setError(HandleAxiosError(error)); diff --git a/src/pages/category/edit/[cid].tsx b/src/pages/category/edit/[cid].tsx index 455bec7..d6357ad 100644 --- a/src/pages/category/edit/[cid].tsx +++ b/src/pages/category/edit/[cid].tsx @@ -7,10 +7,13 @@ import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; +import PATHS from "constants/paths"; import useAutoFocus from "hooks/useAutoFocus"; +import getUserCategory from "lib/category/getUserCategory"; +import getUser from "lib/user/getUser"; import { Category } from "types"; -import { BuildCategory, HandleAxiosError } from "utils/front"; -import prisma from "utils/prisma"; +import { HandleAxiosError } from "utils/front"; +import { getSession } from "utils/session"; import styles from "styles/create.module.scss"; @@ -35,12 +38,10 @@ function EditCategory({ category }: { category: Category }) { nProgress.start(); try { - const payload = { name }; - const { data } = await axios.put( - `/api/category/edit/${category.id}`, - payload - ); - router.push(`/?categoryId=${data?.categoryId}`); + const { data } = await axios.put(`${PATHS.API.CATEGORY}/${category.id}`, { + name, + }); + router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`); setSubmitted(true); } catch (error) { setError(HandleAxiosError(error)); @@ -75,22 +76,21 @@ function EditCategory({ category }: { category: Category }) { EditCategory.authRequired = true; export default EditCategory; -export async function getServerSideProps({ query }) { +export async function getServerSideProps({ req, res, query }) { const { cid } = query; - const categoryDB = await prisma.category.findFirst({ - where: { id: Number(cid) }, - include: { links: true, author: true }, - }); - if (!categoryDB) { + const session = await getSession(req, res); + const user = await getUser(session); + + const category = await getUserCategory(user, Number(cid)); + if (!category) { return { redirect: { - destination: "/", + destination: PATHS.HOME, }, }; } - const category = BuildCategory(categoryDB); return { props: { category: JSON.parse(JSON.stringify(category)), diff --git a/src/pages/category/remove/[cid].tsx b/src/pages/category/remove/[cid].tsx index f4ef4e8..8edd906 100644 --- a/src/pages/category/remove/[cid].tsx +++ b/src/pages/category/remove/[cid].tsx @@ -8,9 +8,12 @@ import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; +import PATHS from "constants/paths"; +import getUserCategory from "lib/category/getUserCategory"; +import getUser from "lib/user/getUser"; import { Category } from "types"; -import { BuildCategory, HandleAxiosError } from "utils/front"; -import prisma from "utils/prisma"; +import { HandleAxiosError } from "utils/front"; +import { getSession } from "utils/session"; import styles from "styles/create.module.scss"; @@ -36,8 +39,8 @@ function RemoveCategory({ category }: { category: Category }) { nProgress.start(); try { - await axios.delete(`/api/category/remove/${category.id}`); - router.push("/"); + await axios.delete(`${PATHS.API.CATEGORY}/${category.id}`); + router.push(PATHS.HOME); setSubmitted(true); } catch (error) { setError(HandleAxiosError(error)); @@ -80,22 +83,21 @@ function RemoveCategory({ category }: { category: Category }) { RemoveCategory.authRequired = true; export default RemoveCategory; -export async function getServerSideProps({ query }) { +export async function getServerSideProps({ req, res, query }) { const { cid } = query; - const categoryDB = await prisma.category.findFirst({ - where: { id: Number(cid) }, - include: { links: true, author: true }, - }); - if (!categoryDB) { + const session = await getSession(req, res); + const user = await getUser(session); + + const category = await getUserCategory(user, Number(cid)); + if (!category) { return { redirect: { - destination: "/", + destination: PATHS.HOME, }, }; } - const category = BuildCategory(categoryDB); return { props: { category: JSON.parse(JSON.stringify(category)), diff --git a/src/pages/link/create.tsx b/src/pages/link/create.tsx index aceda9e..71e73ca 100644 --- a/src/pages/link/create.tsx +++ b/src/pages/link/create.tsx @@ -9,6 +9,7 @@ import PageTransition from "components/PageTransition"; import Selector from "components/Selector"; import TextBox from "components/TextBox"; +import PATHS from "constants/paths"; import useAutoFocus from "hooks/useAutoFocus"; import getUserCategories from "lib/category/getUserCategories"; import getUser from "lib/user/getUser"; @@ -51,8 +52,8 @@ function CreateLink({ categories }: { categories: Category[] }) { try { const payload = { name, url, favorite, categoryId }; - const { data } = await axios.post("/api/link/create", payload); - router.push(`/?categoryId=${data?.categoryId}`); + const { data } = await axios.post(PATHS.API.LINK, payload); + router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`); setSubmitted(true); } catch (error) { setError(HandleAxiosError(error)); @@ -120,7 +121,7 @@ export async function getServerSideProps({ req, res }) { if (categories.length === 0) { return { redirect: { - destination: "/", + destination: PATHS.HOME, }, }; } diff --git a/src/pages/link/edit/[lid].tsx b/src/pages/link/edit/[lid].tsx index 678dab3..f7bd793 100644 --- a/src/pages/link/edit/[lid].tsx +++ b/src/pages/link/edit/[lid].tsx @@ -12,12 +12,14 @@ import TextBox from "components/TextBox"; import useAutoFocus from "hooks/useAutoFocus"; import { Category, Link } from "types"; import { HandleAxiosError, IsValidURL } from "utils/front"; +import { getSessionOrThrow } from "utils/session"; import getUserCategories from "lib/category/getUserCategories"; import getUserLink from "lib/link/getUserLink"; import getUserOrThrow from "lib/user/getUserOrThrow"; + +import PATHS from "constants/paths"; import styles from "styles/create.module.scss"; -import { getSessionOrThrow } from "utils/session"; function EditLink({ link, @@ -71,8 +73,8 @@ function EditLink({ try { const payload = { name, url, favorite, categoryId }; - const { data } = await axios.put(`/api/link/edit/${link.id}`, payload); - router.push(`/?categoryId=${data?.categoryId}`); + const { data } = await axios.put(`${PATHS.API.LINK}/${link.id}`, payload); + router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`); setSubmitted(true); } catch (error) { setError(HandleAxiosError(error)); @@ -142,7 +144,7 @@ export async function getServerSideProps({ req, res, query }) { if (!link) { return { redirect: { - destination: "/", + destination: PATHS.HOME, }, }; } diff --git a/src/pages/link/remove/[lid].tsx b/src/pages/link/remove/[lid].tsx index 6fe10a5..e4d2495 100644 --- a/src/pages/link/remove/[lid].tsx +++ b/src/pages/link/remove/[lid].tsx @@ -8,6 +8,7 @@ import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; +import PATHS from "constants/paths"; import getUserLink from "lib/link/getUserLink"; import getUser from "lib/user/getUser"; import { Link } from "types"; @@ -34,8 +35,8 @@ function RemoveLink({ link }: { link: Link }) { nProgress.start(); try { - const { data } = await axios.delete(`/api/link/remove/${link.id}`); - router.push(`/?categoryId=${data?.categoryId}`); + const { data } = await axios.delete(`${PATHS.API.LINK}/${link.id}`); + router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`); setSubmitted(true); } catch (error) { setError(HandleAxiosError(error)); @@ -106,7 +107,7 @@ export async function getServerSideProps({ req, res, query }) { if (!link) { return { redirect: { - destination: "/", + destination: PATHS.HOME, }, }; } From d9757bdc18848631c54108468a697006e21c2d57 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 4 Jun 2023 00:44:30 +0200 Subject: [PATCH 13/13] feat: disable link to home page when user has 0 category --- src/components/FormLayout.tsx | 10 +++++++--- src/components/Links/LinkItem.tsx | 2 +- src/constants/paths.ts | 12 ++---------- src/lib/category/getUserCategoriesCount.ts | 10 ++++++++++ src/pages/_app.tsx | 5 +++-- src/pages/api/category/[cid].ts | 4 ++++ src/pages/category/create.tsx | 20 +++++++++++++++++--- 7 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 src/lib/category/getUserCategoriesCount.ts diff --git a/src/components/FormLayout.tsx b/src/components/FormLayout.tsx index be036fd..b552150 100644 --- a/src/components/FormLayout.tsx +++ b/src/components/FormLayout.tsx @@ -21,6 +21,7 @@ interface FormProps { classBtnConfirm?: string; children: any; + disableHomeLink?: boolean; } export default function Form({ title, @@ -33,6 +34,7 @@ export default function Form({ textBtnConfirm = "Valider", classBtnConfirm = "", children, + disableHomeLink = false, }: FormProps) { return ( <> @@ -49,9 +51,11 @@ export default function Form({ {textBtnConfirm} - - ← Revenir à l'accueil - + {!disableHomeLink && ( + + ← Revenir à l'accueil + + )} toggleFavorite(link.id)) .catch(console.error); }; diff --git a/src/constants/paths.ts b/src/constants/paths.ts index ad99d54..73aeb00 100644 --- a/src/constants/paths.ts +++ b/src/constants/paths.ts @@ -13,16 +13,8 @@ const PATHS = { REMOVE: "/link/remove", }, API: { - CATEGORY: { - CREATE: "/api/category/create", - EDIT: "/api/category/edit", - REMOVE: "/api/category/remove", - }, - LINK: { - CREATE: "/api/link/create", - EDIT: "/api/link/edit", - REMOVE: "/api/link/remove", - }, + CATEGORY: "/api/category", + LINK: "/api/link", }, NOT_FOUND: "/404", SERVER_ERROR: "/505", diff --git a/src/lib/category/getUserCategoriesCount.ts b/src/lib/category/getUserCategoriesCount.ts new file mode 100644 index 0000000..88c7418 --- /dev/null +++ b/src/lib/category/getUserCategoriesCount.ts @@ -0,0 +1,10 @@ +import { User } from "@prisma/client"; +import prisma from "utils/prisma"; + +export default async function getUserCategoriesCount(user: User) { + return await prisma.category.count({ + where: { + authorId: user.id, + }, + }); +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 2a810e2..26fca70 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -7,6 +7,7 @@ import { useHotkeys } from "react-hotkeys-hook"; import AuthRequired from "components/AuthRequired"; import * as Keys from "constants/keys"; +import PATHS from "constants/paths"; import "nprogress/nprogress.css"; import "styles/globals.scss"; @@ -14,8 +15,8 @@ import "styles/globals.scss"; function MyApp({ Component, pageProps: { session, ...pageProps } }) { const router = useRouter(); - useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push("/"), { - enabled: router.pathname !== "/", + useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push(PATHS.HOME), { + enabled: router.pathname !== PATHS.HOME, enableOnFormTags: ["INPUT"], }); diff --git a/src/pages/api/category/[cid].ts b/src/pages/api/category/[cid].ts index 4135959..322f1e3 100644 --- a/src/pages/api/category/[cid].ts +++ b/src/pages/api/category/[cid].ts @@ -57,6 +57,10 @@ async function deleteCategory({ req, res, user }) { throw new Error("Unable to find category " + cid); } + if (category.links.length !== 0) { + throw new Error("You cannot remove category with links"); + } + await prisma.category.delete({ where: { id: cid }, }); diff --git a/src/pages/category/create.tsx b/src/pages/category/create.tsx index 29eaad3..01cab7d 100644 --- a/src/pages/category/create.tsx +++ b/src/pages/category/create.tsx @@ -12,9 +12,12 @@ import useAutoFocus from "hooks/useAutoFocus"; import { redirectWithoutClientCache } from "utils/client"; import { HandleAxiosError } from "utils/front"; +import getUserCategoriesCount from "lib/category/getUserCategoriesCount"; +import getUser from "lib/user/getUser"; import styles from "styles/create.module.scss"; +import { getSession } from "utils/session"; -function CreateCategory() { +function CreateCategory({ categoriesCount }: { categoriesCount: number }) { const autoFocusRef = useAutoFocus(); const router = useRouter(); const info = useRouter().query?.info as string; @@ -36,10 +39,8 @@ function CreateCategory() { nProgress.start(); try { - const { data } = await axios.post("/api/category/create", { name }); const { data } = await axios.post(PATHS.API.CATEGORY, { name }); redirectWithoutClientCache(router, ""); - router.push(`/?categoryId=${data?.categoryId}`); router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`); setSubmitted(true); } catch (error) { @@ -58,6 +59,7 @@ function CreateCategory() { infoMessage={info} canSubmit={canSubmit} handleSubmit={handleSubmit} + disableHomeLink={categoriesCount === 0} >