Merge pull request #2 from Sonny93/cat_link_author

Assign categories and links to their author
This commit is contained in:
Sonny
2023-06-04 01:05:27 +02:00
committed by GitHub
46 changed files with 768 additions and 533 deletions

View File

@@ -1,7 +1,7 @@
version: "3.8" version: "3.8"
services: services:
my-links-db: my-links-dev-db:
image: mysql:latest image: mysql:latest
restart: always restart: always
env_file: env_file:

View File

@@ -1,6 +1,19 @@
CONTAINER_NAME = "docker-my-links-dev-db-1"
ROOT_PASSWORD = "root_passwd"
USER_NAME = "my-user"
start-dev: start-dev:
@echo 'Starting DB container'
docker compose --env-file ../.env -f ./dev.docker-compose.yml up -d 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: start-prod:
docker-compose --env-file ../.env -f ./docker-compose.yml up -d docker-compose --env-file ../.env -f ./docker-compose.yml up -d

View File

@@ -1,7 +1,7 @@
MYSQL_USER="my_user" MYSQL_USER="my-user"
MYSQL_PASSWORD="root" MYSQL_PASSWORD="my-user_passwd"
MYSQL_DATABASE="my-links" MYSQL_ROOT_PASSWORD="root_passwd"
MYSQL_ROOT_PASSWORD="root" MYSQL_DATABASE="MyLinks"
# Or if you need external Database # Or if you need external Database
# DATABASE_IP="localhost" # DATABASE_IP="localhost"

40
package-lock.json generated
View File

@@ -22,7 +22,8 @@
"react-select": "^5.7.3", "react-select": "^5.7.3",
"sass": "^1.62.1", "sass": "^1.62.1",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"toastr": "^2.1.4" "toastr": "^2.1.4",
"yup": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.2.4", "@types/node": "^20.2.4",
@@ -6005,6 +6006,11 @@
"react-is": "^16.13.1" "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": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "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==", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true "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": { "node_modules/tiny-glob": {
"version": "0.2.9", "version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
@@ -6848,6 +6859,11 @@
"jquery": ">=1.12.0" "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": { "node_modules/tsconfig-paths": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
@@ -7161,6 +7177,28 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/zod": {
"version": "3.21.4", "version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",

View File

@@ -24,7 +24,8 @@
"react-select": "^5.7.3", "react-select": "^5.7.3",
"sass": "^1.62.1", "sass": "^1.62.1",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"toastr": "^2.1.4" "toastr": "^2.1.4",
"yup": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.2.4", "@types/node": "^20.2.4",

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"

View File

@@ -6,14 +6,19 @@ generator client {
} }
datasource db { datasource db {
provider = "mysql" provider = "mysql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
} }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
google_id String @unique google_id String @unique
email String @unique email String @unique
categories Category[]
links Link[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -21,26 +26,33 @@ model User {
} }
model Category { model Category {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique @db.VarChar(255) name String @unique @db.VarChar(255)
links Link[] links Link[]
nextCategoryId Int @default(0)
createdAt DateTime @default(now()) author User @relation(fields: [authorId], references: [id])
updatedAt DateTime @updatedAt authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("category") @@map("category")
} }
model Link { model Link {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @db.VarChar(255) name String @db.VarChar(255)
url String @db.Text url String @db.Text
category Category @relation(fields: [categoryId], references: [id]) category Category @relation(fields: [categoryId], references: [id])
categoryId Int categoryId Int
nextLinkId Int @default(0)
favorite Boolean @default(false) author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now()) authorId Int
updatedAt DateTime @updatedAt
favorite Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("link") @@map("link")
} }

View File

@@ -21,6 +21,7 @@ interface FormProps {
classBtnConfirm?: string; classBtnConfirm?: string;
children: any; children: any;
disableHomeLink?: boolean;
} }
export default function Form({ export default function Form({
title, title,
@@ -33,6 +34,7 @@ export default function Form({
textBtnConfirm = "Valider", textBtnConfirm = "Valider",
classBtnConfirm = "", classBtnConfirm = "",
children, children,
disableHomeLink = false,
}: FormProps) { }: FormProps) {
return ( return (
<> <>
@@ -49,9 +51,11 @@ export default function Form({
{textBtnConfirm} {textBtnConfirm}
</button> </button>
</form> </form>
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}> {!disableHomeLink && (
Revenir à l'accueil <Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}>
</Link> Revenir à l'accueil
</Link>
)}
<MessageManager <MessageManager
info={infoMessage} info={infoMessage}
error={errorMessage} error={errorMessage}

View File

@@ -31,7 +31,7 @@ export default function LinkItem({
categoryId: link.category.id, categoryId: link.category.id,
}; };
axios axios
.put(`${PATHS.API.LINK.EDIT}/${link.id}`, payload) .put(`${PATHS.API.LINK}/${link.id}`, payload)
.then(() => toggleFavorite(link.id)) .then(() => toggleFavorite(link.id))
.catch(console.error); .catch(console.error);
}; };

View File

@@ -1,5 +1,6 @@
const PATHS = { const PATHS = {
LOGIN: "/signin", LOGIN: "/signin",
LOGOUT: "/signout",
HOME: "/", HOME: "/",
CATEGORY: { CATEGORY: {
CREATE: "/category/create", CREATE: "/category/create",
@@ -12,16 +13,8 @@ const PATHS = {
REMOVE: "/link/remove", REMOVE: "/link/remove",
}, },
API: { API: {
CATEGORY: { CATEGORY: "/api/category",
CREATE: "/category/create", LINK: "/api/link",
EDIT: "/category/edit",
REMOVE: "/category/remove",
},
LINK: {
CREATE: "/link/create",
EDIT: "/link/edit",
REMOVE: "/link/remove",
},
}, },
NOT_FOUND: "/404", NOT_FOUND: "/404",
SERVER_ERROR: "/505", SERVER_ERROR: "/505",

2
src/constants/url.ts Normal file
View File

@@ -0,0 +1,2 @@
export const VALID_URL_REGEX =
/^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/;

93
src/lib/api/handler.ts Normal file
View File

@@ -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<void>;
// 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}`;
}
}

View File

@@ -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,
},
},
},
});
}

View File

@@ -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,
},
});
}

View File

@@ -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,
},
},
},
});
}

View File

@@ -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 },
});
}

View File

@@ -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,
},
});
}

View File

@@ -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,
},
});
}

View File

@@ -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,
},
});
}

10
src/lib/user/getUser.ts Normal file
View File

@@ -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,
},
});
}

View File

@@ -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,
},
});
}

View File

@@ -7,6 +7,7 @@ import { useHotkeys } from "react-hotkeys-hook";
import AuthRequired from "components/AuthRequired"; import AuthRequired from "components/AuthRequired";
import * as Keys from "constants/keys"; import * as Keys from "constants/keys";
import PATHS from "constants/paths";
import "nprogress/nprogress.css"; import "nprogress/nprogress.css";
import "styles/globals.scss"; import "styles/globals.scss";
@@ -14,8 +15,8 @@ import "styles/globals.scss";
function MyApp({ Component, pageProps: { session, ...pageProps } }) { function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const router = useRouter(); const router = useRouter();
useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push("/"), { useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push(PATHS.HOME), {
enabled: router.pathname !== "/", enabled: router.pathname !== PATHS.HOME,
enableOnFormTags: ["INPUT"], enableOnFormTags: ["INPUT"],
}); });

View File

@@ -1,11 +1,12 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import PATHS from "constants/paths"; import PATHS from "constants/paths";
import NextAuth from "next-auth"; import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google"; import GoogleProvider from "next-auth/providers/google";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
export default NextAuth({ // TODO: refactor auth
export const authOptions = {
providers: [ providers: [
GoogleProvider({ GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID, clientId: process.env.GOOGLE_CLIENT_ID,
@@ -20,24 +21,34 @@ export default NextAuth({
}), }),
], ],
callbacks: { 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 }) { async signIn({ account: accountParam, profile }) {
// TODO: Auth
console.log( console.log(
"Connexion via", "[AUTH]",
accountParam.provider, "User",
accountParam.providerAccountId, profile.name,
profile.email, profile.sub,
profile.name "attempt to log in with",
accountParam.provider
); );
if (accountParam.provider !== "google") { if (accountParam.provider !== "google") {
console.log("[AUTH]", "User", profile.name, "rejeced : bad provider");
return ( return (
PATHS.LOGIN + PATHS.LOGIN +
"?error=" + "?error=" +
encodeURI("Authentitifcation via Google requise") encodeURI("Authentitifcation via Google requise")
); );
} }
const email = profile?.email; const email = profile?.email;
if (email === "") { if (email === "") {
console.log("[AUTH]", "User", profile.name, "rejeced : missing email");
return ( return (
PATHS.LOGIN + PATHS.LOGIN +
"?error=" + "?error=" +
@@ -48,6 +59,12 @@ export default NextAuth({
} }
const googleId = profile?.sub; const googleId = profile?.sub;
if (googleId === "") { if (googleId === "") {
console.log(
"[AUTH]",
"User",
profile.name,
"rejeced : missing google id"
);
return ( return (
PATHS.LOGIN + PATHS.LOGIN +
"?error=" + "?error=" +
@@ -74,6 +91,13 @@ export default NextAuth({
}); });
return true; return true;
} }
console.log(
"[AUTH]",
"User",
profile.name,
"rejeced : not authorized"
);
return ( return (
PATHS.LOGIN + PATHS.LOGIN +
"?error=" + "?error=" +
@@ -82,9 +106,11 @@ export default NextAuth({
) )
); );
} else { } else {
console.log("[AUTH]", "User", profile.name, "success");
return true; return true;
} }
} catch (error) { } catch (error) {
console.log("[AUTH]", "User", profile.name, "unhandled error");
console.error(error); console.error(error);
return ( return (
PATHS.LOGIN + PATHS.LOGIN +
@@ -97,8 +123,10 @@ export default NextAuth({
pages: { pages: {
signIn: PATHS.LOGIN, signIn: PATHS.LOGIN,
error: PATHS.LOGIN, error: PATHS.LOGIN,
signOut: PATHS.LOGOUT,
}, },
session: { session: {
maxAge: 60 * 60 * 6, // Session de 6 heures maxAge: 60 * 60 * 6, // Session de 6 heures
}, },
}); } as NextAuthOptions;
export default NextAuth(authOptions);

View File

@@ -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,
});
}

View File

@@ -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)",
});
}
}

View File

@@ -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)",
});
}
}

View File

@@ -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 prisma from "utils/prisma";
import { object, string } from "yup";
export default async function handler( export default apiHandler({
req: NextApiRequest, get: getCatgories,
res: NextApiResponse post: createCategory,
) { });
try {
const categories = await prisma.category.findMany(); async function getCatgories({ res, user }) {
console.log("request"); const categories = await getUserCategories(user);
return res.status(200).send({ return res.status(200).send({
categories, categories,
}); });
} catch (error) { }
console.error(error);
return res.status(400).send({ const bodySchema = object({
error: "Une erreur est survenue lors de la récupération des catégories", 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,
});
} }

View File

@@ -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)",
});
}
}

View File

@@ -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,
});
}

View File

@@ -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)",
});
}
}

View File

@@ -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)",
});
}
}

View File

@@ -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 });
}

View File

@@ -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)",
});
}
}

View File

@@ -7,13 +7,17 @@ import FormLayout from "components/FormLayout";
import PageTransition from "components/PageTransition"; import PageTransition from "components/PageTransition";
import TextBox from "components/TextBox"; import TextBox from "components/TextBox";
import PATHS from "constants/paths";
import useAutoFocus from "hooks/useAutoFocus"; import useAutoFocus from "hooks/useAutoFocus";
import { redirectWithoutClientCache } from "utils/client"; import { redirectWithoutClientCache } from "utils/client";
import { HandleAxiosError } from "utils/front"; 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 styles from "styles/create.module.scss";
import { getSession } from "utils/session";
function CreateCategory() { function CreateCategory({ categoriesCount }: { categoriesCount: number }) {
const autoFocusRef = useAutoFocus(); const autoFocusRef = useAutoFocus();
const router = useRouter(); const router = useRouter();
const info = useRouter().query?.info as string; const info = useRouter().query?.info as string;
@@ -35,9 +39,9 @@ function CreateCategory() {
nProgress.start(); nProgress.start();
try { try {
const { data } = await axios.post("/api/category/create", { name }); const { data } = await axios.post(PATHS.API.CATEGORY, { name });
redirectWithoutClientCache(router, ""); redirectWithoutClientCache(router, "");
router.push(`/?categoryId=${data?.categoryId}`); router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`);
setSubmitted(true); setSubmitted(true);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
@@ -55,6 +59,7 @@ function CreateCategory() {
infoMessage={info} infoMessage={info}
canSubmit={canSubmit} canSubmit={canSubmit}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
disableHomeLink={categoriesCount === 0}
> >
<TextBox <TextBox
name="name" name="name"
@@ -72,3 +77,15 @@ function CreateCategory() {
CreateCategory.authRequired = true; CreateCategory.authRequired = true;
export default CreateCategory; export default CreateCategory;
export async function getServerSideProps({ req, res }) {
const session = await getSession(req, res);
const user = await getUser(session);
const categoriesCount = await getUserCategoriesCount(user);
return {
props: {
categoriesCount,
},
};
}

View File

@@ -7,10 +7,13 @@ import FormLayout from "components/FormLayout";
import PageTransition from "components/PageTransition"; import PageTransition from "components/PageTransition";
import TextBox from "components/TextBox"; import TextBox from "components/TextBox";
import PATHS from "constants/paths";
import useAutoFocus from "hooks/useAutoFocus"; import useAutoFocus from "hooks/useAutoFocus";
import getUserCategory from "lib/category/getUserCategory";
import getUser from "lib/user/getUser";
import { Category } from "types"; import { Category } from "types";
import { BuildCategory, HandleAxiosError } from "utils/front"; import { HandleAxiosError } from "utils/front";
import prisma from "utils/prisma"; import { getSession } from "utils/session";
import styles from "styles/create.module.scss"; import styles from "styles/create.module.scss";
@@ -35,12 +38,10 @@ function EditCategory({ category }: { category: Category }) {
nProgress.start(); nProgress.start();
try { try {
const payload = { name }; const { data } = await axios.put(`${PATHS.API.CATEGORY}/${category.id}`, {
const { data } = await axios.put( name,
`/api/category/edit/${category.id}`, });
payload router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`);
);
router.push(`/?categoryId=${data?.categoryId}`);
setSubmitted(true); setSubmitted(true);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
@@ -75,22 +76,21 @@ function EditCategory({ category }: { category: Category }) {
EditCategory.authRequired = true; EditCategory.authRequired = true;
export default EditCategory; export default EditCategory;
export async function getServerSideProps({ query }) { export async function getServerSideProps({ req, res, query }) {
const { cid } = query; const { cid } = query;
const categoryDB = await prisma.category.findFirst({
where: { id: Number(cid) },
include: { links: true },
});
if (!categoryDB) { const session = await getSession(req, res);
const user = await getUser(session);
const category = await getUserCategory(user, Number(cid));
if (!category) {
return { return {
redirect: { redirect: {
destination: "/", destination: PATHS.HOME,
}, },
}; };
} }
const category = BuildCategory(categoryDB);
return { return {
props: { props: {
category: JSON.parse(JSON.stringify(category)), category: JSON.parse(JSON.stringify(category)),

View File

@@ -8,9 +8,12 @@ import FormLayout from "components/FormLayout";
import PageTransition from "components/PageTransition"; import PageTransition from "components/PageTransition";
import TextBox from "components/TextBox"; import TextBox from "components/TextBox";
import PATHS from "constants/paths";
import getUserCategory from "lib/category/getUserCategory";
import getUser from "lib/user/getUser";
import { Category } from "types"; import { Category } from "types";
import { BuildCategory, HandleAxiosError } from "utils/front"; import { HandleAxiosError } from "utils/front";
import prisma from "utils/prisma"; import { getSession } from "utils/session";
import styles from "styles/create.module.scss"; import styles from "styles/create.module.scss";
@@ -36,8 +39,8 @@ function RemoveCategory({ category }: { category: Category }) {
nProgress.start(); nProgress.start();
try { try {
await axios.delete(`/api/category/remove/${category.id}`); await axios.delete(`${PATHS.API.CATEGORY}/${category.id}`);
router.push("/"); router.push(PATHS.HOME);
setSubmitted(true); setSubmitted(true);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
@@ -80,22 +83,21 @@ function RemoveCategory({ category }: { category: Category }) {
RemoveCategory.authRequired = true; RemoveCategory.authRequired = true;
export default RemoveCategory; export default RemoveCategory;
export async function getServerSideProps({ query }) { export async function getServerSideProps({ req, res, query }) {
const { cid } = query; const { cid } = query;
const categoryDB = await prisma.category.findFirst({
where: { id: Number(cid) },
include: { links: true },
});
if (!categoryDB) { const session = await getSession(req, res);
const user = await getUser(session);
const category = await getUserCategory(user, Number(cid));
if (!category) {
return { return {
redirect: { redirect: {
destination: "/", destination: PATHS.HOME,
}, },
}; };
} }
const category = BuildCategory(categoryDB);
return { return {
props: { props: {
category: JSON.parse(JSON.stringify(category)), category: JSON.parse(JSON.stringify(category)),

View File

@@ -13,9 +13,10 @@ import PATHS from "constants/paths";
import useModal from "hooks/useModal"; import useModal from "hooks/useModal";
import { Category, Link, SearchItem } from "types"; import { Category, Link, SearchItem } from "types";
import { BuildCategory } from "utils/front"; import getUserCategories from "lib/category/getUserCategories";
import getUser from "lib/user/getUser";
import { pushStateVanilla } from "utils/link"; import { pushStateVanilla } from "utils/link";
import prisma from "utils/prisma"; import { getSession } from "utils/session";
interface HomePageProps { interface HomePageProps {
categories: Category[]; categories: Category[];
@@ -185,13 +186,13 @@ function Home(props: HomePageProps) {
); );
} }
export async function getServerSideProps({ query }) { export async function getServerSideProps({ req, res, query }) {
const session = await getSession(req, res);
const queryCategoryId = (query?.categoryId as string) || ""; const queryCategoryId = (query?.categoryId as string) || "";
const categoriesDB = await prisma.category.findMany({
include: { links: true },
});
if (categoriesDB.length === 0) { const user = await getUser(session);
const categories = await getUserCategories(user);
if (categories.length === 0) {
return { return {
redirect: { redirect: {
destination: PATHS.CATEGORY.CREATE, destination: PATHS.CATEGORY.CREATE,
@@ -199,11 +200,9 @@ export async function getServerSideProps({ query }) {
}; };
} }
const categories = categoriesDB.map(BuildCategory);
const currentCategory = categories.find( const currentCategory = categories.find(
({ id }) => id === Number(queryCategoryId) ({ id }) => id === Number(queryCategoryId)
); );
return { return {
props: { props: {
categories: JSON.parse(JSON.stringify(categories)), categories: JSON.parse(JSON.stringify(categories)),

View File

@@ -9,10 +9,13 @@ import PageTransition from "components/PageTransition";
import Selector from "components/Selector"; import Selector from "components/Selector";
import TextBox from "components/TextBox"; import TextBox from "components/TextBox";
import PATHS from "constants/paths";
import useAutoFocus from "hooks/useAutoFocus"; import useAutoFocus from "hooks/useAutoFocus";
import getUserCategories from "lib/category/getUserCategories";
import getUser from "lib/user/getUser";
import { Category, Link } from "types"; import { Category, Link } from "types";
import { BuildCategory, HandleAxiosError, IsValidURL } from "utils/front"; import { HandleAxiosError, IsValidURL } from "utils/front";
import prisma from "utils/prisma"; import { getSession } from "utils/session";
import styles from "styles/create.module.scss"; import styles from "styles/create.module.scss";
@@ -49,8 +52,8 @@ function CreateLink({ categories }: { categories: Category[] }) {
try { try {
const payload = { name, url, favorite, categoryId }; const payload = { name, url, favorite, categoryId };
const { data } = await axios.post("/api/link/create", payload); const { data } = await axios.post(PATHS.API.LINK, payload);
router.push(`/?categoryId=${data?.categoryId}`); router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`);
setSubmitted(true); setSubmitted(true);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
@@ -110,16 +113,15 @@ function CreateLink({ categories }: { categories: Category[] }) {
CreateLink.authRequired = true; CreateLink.authRequired = true;
export default CreateLink; export default CreateLink;
export async function getServerSideProps() { export async function getServerSideProps({ req, res }) {
const categoriesDB = await prisma.category.findMany(); const session = await getSession(req, res);
const categories = categoriesDB.map((categoryDB) => const user = await getUser(session);
BuildCategory(categoryDB)
);
const categories = await getUserCategories(user);
if (categories.length === 0) { if (categories.length === 0) {
return { return {
redirect: { redirect: {
destination: "/", destination: PATHS.HOME,
}, },
}; };
} }

View File

@@ -11,14 +11,14 @@ import TextBox from "components/TextBox";
import useAutoFocus from "hooks/useAutoFocus"; import useAutoFocus from "hooks/useAutoFocus";
import { Category, Link } from "types"; import { Category, Link } from "types";
import { import { HandleAxiosError, IsValidURL } from "utils/front";
BuildCategory, import { getSessionOrThrow } from "utils/session";
BuildLink,
HandleAxiosError,
IsValidURL,
} from "utils/front";
import prisma from "utils/prisma";
import getUserCategories from "lib/category/getUserCategories";
import getUserLink from "lib/link/getUserLink";
import getUserOrThrow from "lib/user/getUserOrThrow";
import PATHS from "constants/paths";
import styles from "styles/create.module.scss"; import styles from "styles/create.module.scss";
function EditLink({ function EditLink({
@@ -73,8 +73,8 @@ function EditLink({
try { try {
const payload = { name, url, favorite, categoryId }; const payload = { name, url, favorite, categoryId };
const { data } = await axios.put(`/api/link/edit/${link.id}`, payload); const { data } = await axios.put(`${PATHS.API.LINK}/${link.id}`, payload);
router.push(`/?categoryId=${data?.categoryId}`); router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`);
setSubmitted(true); setSubmitted(true);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
@@ -133,31 +133,22 @@ function EditLink({
EditLink.authRequired = true; EditLink.authRequired = true;
export default EditLink; export default EditLink;
export async function getServerSideProps({ query }) { export async function getServerSideProps({ req, res, query }) {
const { lid } = query; const { lid } = query;
const categoriesDB = await prisma.category.findMany(); const session = await getSessionOrThrow(req, res);
const categories = categoriesDB.map((categoryDB) => const user = await getUserOrThrow(session);
BuildCategory(categoryDB) const categories = await getUserCategories(user);
);
const linkDB = await prisma.link.findFirst({ const link = await getUserLink(user, Number(lid));
where: { id: Number(lid) }, if (!link) {
include: { category: true },
});
if (!linkDB) {
return { return {
redirect: { redirect: {
destination: "/", destination: PATHS.HOME,
}, },
}; };
} }
const link = BuildLink(linkDB, {
categoryId: linkDB.categoryId,
categoryName: linkDB.category.name,
});
return { return {
props: { props: {
link: JSON.parse(JSON.stringify(link)), link: JSON.parse(JSON.stringify(link)),

View File

@@ -8,9 +8,12 @@ import FormLayout from "components/FormLayout";
import PageTransition from "components/PageTransition"; import PageTransition from "components/PageTransition";
import TextBox from "components/TextBox"; 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 { Link } from "types";
import { BuildLink, HandleAxiosError } from "utils/front"; import { HandleAxiosError } from "utils/front";
import prisma from "utils/prisma"; import { getSession } from "utils/session";
import styles from "styles/create.module.scss"; import styles from "styles/create.module.scss";
@@ -32,8 +35,8 @@ function RemoveLink({ link }: { link: Link }) {
nProgress.start(); nProgress.start();
try { try {
const { data } = await axios.delete(`/api/link/remove/${link.id}`); const { data } = await axios.delete(`${PATHS.API.LINK}/${link.id}`);
router.push(`/?categoryId=${data?.categoryId}`); router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`);
setSubmitted(true); setSubmitted(true);
} catch (error) { } catch (error) {
setError(HandleAxiosError(error)); setError(HandleAxiosError(error));
@@ -94,25 +97,21 @@ function RemoveLink({ link }: { link: Link }) {
RemoveLink.authRequired = true; RemoveLink.authRequired = true;
export default RemoveLink; export default RemoveLink;
export async function getServerSideProps({ query }) { export async function getServerSideProps({ req, res, query }) {
const { lid } = 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 { return {
redirect: { redirect: {
destination: "/", destination: PATHS.HOME,
}, },
}; };
} }
const link = BuildLink(linkDB, {
categoryId: linkDB.categoryId,
categoryName: linkDB.category.name,
});
return { return {
props: { props: {
link: JSON.parse(JSON.stringify(link)), link: JSON.parse(JSON.stringify(link)),

View File

@@ -1,10 +1,11 @@
import { Provider } from "next-auth/providers"; 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 { NextSeo } from "next-seo";
import Link from "next/link"; import Link from "next/link";
import MessageManager from "components/MessageManager/MessageManager"; import MessageManager from "components/MessageManager/MessageManager";
import PATHS from "constants/paths"; import PATHS from "constants/paths";
import { getSession } from "utils/session";
import styles from "styles/login.module.scss"; import styles from "styles/login.module.scss";
@@ -36,8 +37,8 @@ export default function SignIn({ providers }: SignInProps) {
); );
} }
export async function getServerSideProps(context) { export async function getServerSideProps({ req, res }) {
const session = await getSession(context); const session = await getSession(req, res);
if (session) { if (session) {
return { return {
redirect: { redirect: {

10
src/types.d.ts vendored
View File

@@ -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 { export interface Category {
id: number; id: number;
name: string; name: string;
links: Link[]; links: Link[];
nextCategoryId: number; authorId: User["id"];
author: User;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
@@ -20,7 +25,8 @@ export interface Link {
name: string; name: string;
}; };
nextLinkId: number; authorId: User["id"];
author: User;
favorite: boolean; favorite: boolean;
createdAt: Date; createdAt: Date;

View File

@@ -1,50 +1,8 @@
import axios from "axios"; import axios from "axios";
import { VALID_URL_REGEX } from "constants/url";
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,
};
}
export function IsValidURL(url: string): boolean { export function IsValidURL(url: string): boolean {
const regex = new RegExp( const regex = new RegExp(VALID_URL_REGEX);
/^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/
);
return url.match(regex) ? true : false; return url.match(regex) ? true : false;
} }

21
src/utils/session.ts Normal file
View File

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