mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 15:05:35 +00:00
Merge pull request #2 from Sonny93/cat_link_author
Assign categories and links to their author
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
my-links-db:
|
||||
my-links-dev-db:
|
||||
image: mysql:latest
|
||||
restart: always
|
||||
env_file:
|
||||
|
||||
@@ -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 ../
|
||||
docker build -f ./Dockerfile -t sonny/my-links ../
|
||||
|
||||
@@ -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"
|
||||
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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"
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</button>
|
||||
</form>
|
||||
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}>
|
||||
← Revenir à l'accueil
|
||||
</Link>
|
||||
{!disableHomeLink && (
|
||||
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}>
|
||||
← Revenir à l'accueil
|
||||
</Link>
|
||||
)}
|
||||
<MessageManager
|
||||
info={infoMessage}
|
||||
error={errorMessage}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function LinkItem({
|
||||
categoryId: link.category.id,
|
||||
};
|
||||
axios
|
||||
.put(`${PATHS.API.LINK.EDIT}/${link.id}`, payload)
|
||||
.put(`${PATHS.API.LINK}/${link.id}`, payload)
|
||||
.then(() => toggleFavorite(link.id))
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
src/constants/url.ts
Normal file
2
src/constants/url.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const VALID_URL_REGEX =
|
||||
/^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/;
|
||||
93
src/lib/api/handler.ts
Normal file
93
src/lib/api/handler.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
17
src/lib/category/getUserCategories.ts
Normal file
17
src/lib/category/getUserCategories.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
10
src/lib/category/getUserCategoriesCount.ts
Normal file
10
src/lib/category/getUserCategoriesCount.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
18
src/lib/category/getUserCategory.ts
Normal file
18
src/lib/category/getUserCategory.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
11
src/lib/category/getUserCategoryByName.ts
Normal file
11
src/lib/category/getUserCategoryByName.ts
Normal 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 },
|
||||
});
|
||||
}
|
||||
19
src/lib/link/getLinkFromCategoryByName.ts
Normal file
19
src/lib/link/getLinkFromCategoryByName.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
14
src/lib/link/getUserLink.ts
Normal file
14
src/lib/link/getUserLink.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
10
src/lib/link/getUserLinks.ts
Normal file
10
src/lib/link/getUserLinks.ts
Normal 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
10
src/lib/user/getUser.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
13
src/lib/user/getUserOrThrow.ts
Normal file
13
src/lib/user/getUserOrThrow.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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"],
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
71
src/pages/api/category/[cid].ts
Normal file
71
src/pages/api/category/[cid].ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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)",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)",
|
||||
});
|
||||
}
|
||||
}
|
||||
82
src/pages/api/link/[lid].ts
Normal file
82
src/pages/api/link/[lid].ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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)",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)",
|
||||
});
|
||||
}
|
||||
}
|
||||
53
src/pages/api/link/index.ts
Normal file
53
src/pages/api/link/index.ts
Normal 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 });
|
||||
}
|
||||
@@ -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)",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
<TextBox
|
||||
name="name"
|
||||
@@ -72,3 +77,15 @@ function CreateCategory() {
|
||||
|
||||
CreateCategory.authRequired = true;
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
import getUserCategory from "lib/category/getUserCategory";
|
||||
import getUser from "lib/user/getUser";
|
||||
import { Category } from "types";
|
||||
import { BuildCategory, HandleAxiosError } from "utils/front";
|
||||
import prisma from "utils/prisma";
|
||||
import { HandleAxiosError } from "utils/front";
|
||||
import { getSession } from "utils/session";
|
||||
|
||||
import styles from "styles/create.module.scss";
|
||||
|
||||
@@ -35,12 +38,10 @@ function EditCategory({ category }: { category: Category }) {
|
||||
nProgress.start();
|
||||
|
||||
try {
|
||||
const payload = { name };
|
||||
const { data } = await axios.put(
|
||||
`/api/category/edit/${category.id}`,
|
||||
payload
|
||||
);
|
||||
router.push(`/?categoryId=${data?.categoryId}`);
|
||||
const { data } = await axios.put(`${PATHS.API.CATEGORY}/${category.id}`, {
|
||||
name,
|
||||
});
|
||||
router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`);
|
||||
setSubmitted(true);
|
||||
} catch (error) {
|
||||
setError(HandleAxiosError(error));
|
||||
@@ -75,22 +76,21 @@ function EditCategory({ category }: { category: Category }) {
|
||||
EditCategory.authRequired = true;
|
||||
export default EditCategory;
|
||||
|
||||
export async function getServerSideProps({ query }) {
|
||||
export async function getServerSideProps({ req, res, query }) {
|
||||
const { cid } = query;
|
||||
const categoryDB = await prisma.category.findFirst({
|
||||
where: { id: Number(cid) },
|
||||
include: { links: true },
|
||||
});
|
||||
|
||||
if (!categoryDB) {
|
||||
const session = await getSession(req, res);
|
||||
const user = await getUser(session);
|
||||
|
||||
const category = await getUserCategory(user, Number(cid));
|
||||
if (!category) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
destination: PATHS.HOME,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const category = BuildCategory(categoryDB);
|
||||
return {
|
||||
props: {
|
||||
category: JSON.parse(JSON.stringify(category)),
|
||||
|
||||
@@ -8,9 +8,12 @@ import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import getUserCategory from "lib/category/getUserCategory";
|
||||
import getUser from "lib/user/getUser";
|
||||
import { Category } from "types";
|
||||
import { BuildCategory, HandleAxiosError } from "utils/front";
|
||||
import prisma from "utils/prisma";
|
||||
import { HandleAxiosError } from "utils/front";
|
||||
import { getSession } from "utils/session";
|
||||
|
||||
import styles from "styles/create.module.scss";
|
||||
|
||||
@@ -36,8 +39,8 @@ function RemoveCategory({ category }: { category: Category }) {
|
||||
nProgress.start();
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/category/remove/${category.id}`);
|
||||
router.push("/");
|
||||
await axios.delete(`${PATHS.API.CATEGORY}/${category.id}`);
|
||||
router.push(PATHS.HOME);
|
||||
setSubmitted(true);
|
||||
} catch (error) {
|
||||
setError(HandleAxiosError(error));
|
||||
@@ -80,22 +83,21 @@ function RemoveCategory({ category }: { category: Category }) {
|
||||
RemoveCategory.authRequired = true;
|
||||
export default RemoveCategory;
|
||||
|
||||
export async function getServerSideProps({ query }) {
|
||||
export async function getServerSideProps({ req, res, query }) {
|
||||
const { cid } = query;
|
||||
const categoryDB = await prisma.category.findFirst({
|
||||
where: { id: Number(cid) },
|
||||
include: { links: true },
|
||||
});
|
||||
|
||||
if (!categoryDB) {
|
||||
const session = await getSession(req, res);
|
||||
const user = await getUser(session);
|
||||
|
||||
const category = await getUserCategory(user, Number(cid));
|
||||
if (!category) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
destination: PATHS.HOME,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const category = BuildCategory(categoryDB);
|
||||
return {
|
||||
props: {
|
||||
category: JSON.parse(JSON.stringify(category)),
|
||||
|
||||
@@ -13,9 +13,10 @@ import PATHS from "constants/paths";
|
||||
import useModal from "hooks/useModal";
|
||||
import { Category, Link, SearchItem } from "types";
|
||||
|
||||
import { BuildCategory } from "utils/front";
|
||||
import getUserCategories from "lib/category/getUserCategories";
|
||||
import getUser from "lib/user/getUser";
|
||||
import { pushStateVanilla } from "utils/link";
|
||||
import prisma from "utils/prisma";
|
||||
import { getSession } from "utils/session";
|
||||
|
||||
interface HomePageProps {
|
||||
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 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 {
|
||||
redirect: {
|
||||
destination: PATHS.CATEGORY.CREATE,
|
||||
@@ -199,11 +200,9 @@ export async function getServerSideProps({ query }) {
|
||||
};
|
||||
}
|
||||
|
||||
const categories = categoriesDB.map(BuildCategory);
|
||||
const currentCategory = categories.find(
|
||||
({ id }) => id === Number(queryCategoryId)
|
||||
);
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories: JSON.parse(JSON.stringify(categories)),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
10
src/types.d.ts
vendored
10
src/types.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
21
src/utils/session.ts
Normal file
21
src/utils/session.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user