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;
+}