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/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/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/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 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/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 133dcb7..73aeb00 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", @@ -12,16 +13,8 @@ const PATHS = { REMOVE: "/link/remove", }, API: { - CATEGORY: { - CREATE: "/category/create", - EDIT: "/category/edit", - REMOVE: "/category/remove", - }, - LINK: { - CREATE: "/link/create", - EDIT: "/link/edit", - REMOVE: "/link/remove", - }, + CATEGORY: "/api/category", + LINK: "/api/link", }, NOT_FOUND: "/404", SERVER_ERROR: "/505", 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/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/category/getUserCategories.ts b/src/lib/category/getUserCategories.ts new file mode 100644 index 0000000..6cbeeb5 --- /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/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/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/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/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/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/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, + }, + }); +} diff --git a/src/lib/user/getUserOrThrow.ts b/src/lib/user/getUserOrThrow.ts new file mode 100644 index 0000000..0f9626a --- /dev/null +++ b/src/lib/user/getUserOrThrow.ts @@ -0,0 +1,13 @@ +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/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/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 35e4061..80becdd 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,34 @@ 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, + profile.sub, + "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 +59,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 +91,13 @@ export default NextAuth({ }); return true; } + + console.log( + "[AUTH]", + "User", + profile.name, + "rejeced : not authorized" + ); return ( PATHS.LOGIN + "?error=" + @@ -82,9 +106,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 +123,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/[cid].ts b/src/pages/api/category/[cid].ts new file mode 100644 index 0000000..322f1e3 --- /dev/null +++ b/src/pages/api/category/[cid].ts @@ -0,0 +1,71 @@ +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); + } + + if (category.links.length !== 0) { + throw new Error("You cannot remove category with links"); + } + + await prisma.category.delete({ + where: { id: cid }, + }); + return res.send({ + success: "Category successfully deleted", + categoryId: category.id, + }); +} diff --git a/src/pages/api/category/create.ts b/src/pages/api/category/create.ts deleted file mode 100644 index f96f66d..0000000 --- a/src/pages/api/category/create.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import prisma from "utils/prisma"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const name = req.body?.name as string; - - if (!name) { - return res.status(400).send({ error: "Nom de catégorie manquant" }); - } - - 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)", - }); - } - - try { - const category = await prisma.category.create({ - data: { name }, - }); - 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)", - }); - } -} 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/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/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)", - }); - } -} 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/create.ts b/src/pages/api/link/create.ts deleted file mode 100644 index eca3f39..0000000 --- a/src/pages/api/link/create.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import prisma from "utils/prisma"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - 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 { - 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 { - await prisma.link.create({ - data: { - name, - url, - categoryId, - favorite, - }, - }); - 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)", - }); - } -} 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/index.ts b/src/pages/api/link/index.ts new file mode 100644 index 0000000..20a9e2c --- /dev/null +++ b/src/pages/api/link/index.ts @@ -0,0 +1,53 @@ +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"; + +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 category = await getUserCategory(user, categoryId); + if (!category) { + throw new Error("Unable to find category " + categoryId); + } + + await prisma.link.create({ + data: { + name, + url, + categoryId, + favorite, + authorId: user.id, + }, + }); + + return res.send({ success: "Link successfully created", categoryId }); +} 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)", - }); - } -} diff --git a/src/pages/category/create.tsx b/src/pages/category/create.tsx index c609806..01cab7d 100644 --- a/src/pages/category/create.tsx +++ b/src/pages/category/create.tsx @@ -7,13 +7,17 @@ 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"; +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; @@ -35,9 +39,9 @@ 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) { setError(HandleAxiosError(error)); @@ -55,6 +59,7 @@ function CreateCategory() { infoMessage={info} canSubmit={canSubmit} handleSubmit={handleSubmit} + disableHomeLink={categoriesCount === 0} > 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 51a7daf..71e73ca 100644 --- a/src/pages/link/create.tsx +++ b/src/pages/link/create.tsx @@ -9,10 +9,13 @@ 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"; 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"; @@ -49,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)); @@ -110,16 +113,15 @@ function CreateLink({ categories }: { categories: Category[] }) { CreateLink.authRequired = true; export default CreateLink; -export async function getServerSideProps() { - const categoriesDB = await prisma.category.findMany(); - 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: { - destination: "/", + destination: PATHS.HOME, }, }; } diff --git a/src/pages/link/edit/[lid].tsx b/src/pages/link/edit/[lid].tsx index 37c5bd4..f7bd793 100644 --- a/src/pages/link/edit/[lid].tsx +++ b/src/pages/link/edit/[lid].tsx @@ -11,14 +11,14 @@ 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 { 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"; function EditLink({ @@ -73,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)); @@ -133,31 +133,22 @@ 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: "/", + destination: PATHS.HOME, }, }; } - 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..e4d2495 100644 --- a/src/pages/link/remove/[lid].tsx +++ b/src/pages/link/remove/[lid].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 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"; @@ -32,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)); @@ -94,25 +97,21 @@ 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 }, - }); - 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: "/", + destination: PATHS.HOME, }, }; } - 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 574b3b7..44e3aef 100644 --- a/src/pages/signin.tsx +++ b/src/pages/signin.tsx @@ -1,10 +1,11 @@ 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"; import MessageManager from "components/MessageManager/MessageManager"; import PATHS from "constants/paths"; +import { getSession } from "utils/session"; import styles from "styles/login.module.scss"; @@ -36,8 +37,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 getSession(req, res); if (session) { return { redirect: { diff --git a/src/types.d.ts b/src/types.d.ts index d5bfb0d..b561a77 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,9 +1,14 @@ +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; links: Link[]; - nextCategoryId: number; + authorId: User["id"]; + author: User; createdAt: Date; updatedAt: Date; @@ -20,7 +25,8 @@ export interface Link { name: string; }; - nextLinkId: number; + authorId: User["id"]; + author: User; favorite: boolean; createdAt: Date; diff --git a/src/utils/front.ts b/src/utils/front.ts index b368165..ff5fd25 100644 --- a/src/utils/front.ts +++ b/src/utils/front.ts @@ -1,50 +1,8 @@ import axios from "axios"; - -import { Category, Link } from "types"; - -export function BuildCategory({ - id, - name, - nextCategoryId, - links = [], - createdAt, - updatedAt, -}): Category { - return { - id, - name, - links: links.map((link) => - BuildLink(link, { categoryId: id, categoryName: name }) - ), - nextCategoryId, - createdAt, - updatedAt, - }; -} - -export function BuildLink( - { id, name, url, nextLinkId, favorite, createdAt, updatedAt }, - { categoryId, categoryName } -): Link { - return { - id, - name, - url, - category: { - id: categoryId, - name: categoryName, - }, - nextLinkId, - 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; } diff --git a/src/utils/session.ts b/src/utils/session.ts new file mode 100644 index 0000000..ba428d7 --- /dev/null +++ b/src/utils/session.ts @@ -0,0 +1,21 @@ +import { NextApiRequest, NextApiResponse } from "next"; +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 getSession(req, res); + + if (!session) { + throw new Error("You must be connected"); + } + + return session; +}