From 922d145570dc4aa72b3d5fc3495abc640576fa3f Mon Sep 17 00:00:00 2001 From: michael welnick Date: Wed, 2 Aug 2023 23:52:37 +0000 Subject: [PATCH 01/20] bare bones docker setup --- .devcontainer/devcontainer.json | 22 ++++++++++++++++++++++ .env.sample | 5 ++++- .gitignore | 3 +++ Dockerfile | 29 +++++++++++++++++++++++++++++ docker-compose.yml | 16 ++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6aff804 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root" +} diff --git a/.env.sample b/.env.sample index ea08bfa..451abee 100644 --- a/.env.sample +++ b/.env.sample @@ -25,4 +25,7 @@ PRICE_ID= NEXT_PUBLIC_TRIAL_PERIOD_DAYS= NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL= BASE_URL=http://localhost:3000 -NEXT_PUBLIC_PRICING= \ No newline at end of file +NEXT_PUBLIC_PRICING= + +# Docker postgres settings +POSTGRES_PASSWORD= diff --git a/.gitignore b/.gitignore index 3cec5f6..e10e251 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts /test-results/ /playwright-report/ /playwright/.cache/ + +# docker +pgdata \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dd4e519 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# playwright doesnt support debian image +FROM ubuntu:focal + +run apt-get update && apt-get install wget xz-utils -y + +RUN mkdir /data + +WORKDIR /data + +RUN wget https://nodejs.org/dist/v20.5.0/node-v20.5.0-linux-x64.tar.xz -O nodejs.tar.xz \ + && mkdir /opt/nodejs \ + && tar -xf nodejs.tar.xz --strip-components 1 -C /opt/nodejs \ + && rm nodejs.tar.xz +ENV PATH="$PATH:/opt/nodejs/bin" +RUN npm install -g yarn + +COPY ./package.json ./yarn.lock ./playwright.config.ts . + +RUN yarn +RUN npx playwright install-deps + +COPY . . + +RUN yarn build + +CMD yarn prisma migrate deploy && yarn start + +# RUN apt-get update && apt-get install \ +# git \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4300c10 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.5" +services: + linkwarden: + env_file: .env + restart: always + build: . + ports: + - 3000:3000 + volumes: + - /var/lib/elasticsearch/data + postgres: + image: postgres + env_file: .env + restart: always + volumes: + - ./pgdata:/var/lib/postgresql/data From d3300d7cc992a3aff7be738634e8854340624a17 Mon Sep 17 00:00:00 2001 From: michael welnick Date: Thu, 3 Aug 2023 09:21:08 -0700 Subject: [PATCH 02/20] dockerignore and do primsa generate --- .dockerignore | 7 +++++++ Dockerfile | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5d4b3d1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +pgdata +.env +.devcontainer +docker-compose.yml +Dockerfile +README.md diff --git a/Dockerfile b/Dockerfile index dd4e519..6613e0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,9 +21,7 @@ RUN npx playwright install-deps COPY . . +RUN yarn prisma generate RUN yarn build CMD yarn prisma migrate deploy && yarn start - -# RUN apt-get update && apt-get install \ -# git \ No newline at end of file From fc4d27d431961d466827e72c97b0fd43f8b848a9 Mon Sep 17 00:00:00 2001 From: michael welnick Date: Thu, 3 Aug 2023 10:38:44 -0700 Subject: [PATCH 03/20] use data volume --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4300c10..4a90f97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,4 +13,6 @@ services: env_file: .env restart: always volumes: - - ./pgdata:/var/lib/postgresql/data + - pgdata:/var/lib/postgresql/data +volumes: + pgdata: \ No newline at end of file From 1bb1d8140d35ea52db796e38e53c2e11f6f4e36c Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Aug 2023 14:03:06 -0400 Subject: [PATCH 04/20] major bug fixed --- hooks/useInitialData.tsx | 5 ++++- pages/register.tsx | 21 ++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index b79d5a8..a0bd1c4 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -13,7 +13,10 @@ export default function useInitialData() { const { setAccount } = useAccountStore(); useEffect(() => { - if (status === "authenticated" && data.user.isSubscriber) { + if ( + status === "authenticated" && + (!process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE || data.user.isSubscriber) + ) { setCollections(); setTags(); // setLinks(); diff --git a/pages/register.tsx b/pages/register.tsx index d02ddbc..d2236ab 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { toast } from "react-hot-toast"; import SubmitButton from "@/components/SubmitButton"; import { signIn } from "next-auth/react"; -import Image from "next/image"; +import { useRouter } from "next/router"; import CenteredForm from "@/layouts/CenteredForm"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; @@ -18,6 +18,7 @@ type FormData = { export default function Register() { const [submitLoader, setSubmitLoader] = useState(false); + const router = useRouter(); const [form, setForm] = useState({ name: "", @@ -28,7 +29,7 @@ export default function Register() { }); async function registerUser() { - const checkHasEmptyFields = () => { + const checkFields = () => { if (emailEnabled) { return ( form.name !== "" && @@ -46,14 +47,7 @@ export default function Register() { } }; - const sendConfirmation = async () => { - await signIn("email", { - email: form.email, - callbackUrl: "/", - }); - }; - - if (checkHasEmptyFields()) { + if (checkFields()) { if (form.password !== form.passwordConfirmation) return toast.error("Passwords do not match."); else if (form.password.length < 8) @@ -78,7 +72,12 @@ export default function Register() { setSubmitLoader(false); if (response.ok) { - if (form.email) await sendConfirmation(); + if (form.email && emailEnabled) + await signIn("email", { + email: form.email, + callbackUrl: "/", + }); + else if (!emailEnabled) router.push("/login"); toast.success("User Created!"); } else { From 5ba3fd7b6cc353a239c44f69ecd41da1866c3556 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Aug 2023 14:25:46 -0400 Subject: [PATCH 05/20] minor change --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4a90f97..4d2912c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ version: "3.5" services: linkwarden: env_file: .env + environment: + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres restart: always build: . ports: @@ -15,4 +17,4 @@ services: volumes: - pgdata:/var/lib/postgresql/data volumes: - pgdata: \ No newline at end of file + pgdata: From a8009734a98fe50067a7cfdad13be0a1eca70995 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Aug 2023 17:54:04 -0400 Subject: [PATCH 06/20] added docker bind mounts + bug fix --- Dockerfile | 4 +-- docker-compose.yml | 24 +++++++-------- lib/api/controllers/users/updateUser.ts | 41 +++++++++++++++++-------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6613e0d..0a875e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # playwright doesnt support debian image FROM ubuntu:focal -run apt-get update && apt-get install wget xz-utils -y +RUN apt-get update && apt-get install wget xz-utils -y RUN mkdir /data @@ -14,7 +14,7 @@ RUN wget https://nodejs.org/dist/v20.5.0/node-v20.5.0-linux-x64.tar.xz -O nodejs ENV PATH="$PATH:/opt/nodejs/bin" RUN npm install -g yarn -COPY ./package.json ./yarn.lock ./playwright.config.ts . +COPY ./package.json ./yarn.lock ./playwright.config.ts ./ RUN yarn RUN npx playwright install-deps diff --git a/docker-compose.yml b/docker-compose.yml index 4d2912c..219d689 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,18 @@ version: "3.5" services: - linkwarden: - env_file: .env - environment: - - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres - restart: always - build: . - ports: - - 3000:3000 - volumes: - - /var/lib/elasticsearch/data postgres: image: postgres env_file: .env restart: always volumes: - - pgdata:/var/lib/postgresql/data -volumes: - pgdata: + - ./pgdata:/var/lib/postgresql/data + linkwarden: + env_file: .env + restart: always + build: . + ports: + - 3000:3000 + volumes: + - ./data:/data/data + depends_on: + - postgres diff --git a/lib/api/controllers/users/updateUser.ts b/lib/api/controllers/users/updateUser.ts index 8f4eb0f..1f41fb5 100644 --- a/lib/api/controllers/users/updateUser.ts +++ b/lib/api/controllers/users/updateUser.ts @@ -3,7 +3,11 @@ import { AccountSettings } from "@/types/global"; import bcrypt from "bcrypt"; import removeFile from "@/lib/api/storage/removeFile"; import createFile from "@/lib/api/storage/createFile"; -import updateCustomerEmail from "../../updateCustomerEmail"; +import updateCustomerEmail from "@/lib/api/updateCustomerEmail"; +import createFolder from "@/lib/api/storage/createFolder"; + +const emailEnabled = + process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false; export default async function updateUser( user: AccountSettings, @@ -14,9 +18,14 @@ export default async function updateUser( isSubscriber: boolean; } ) { - if (!user.username || !user.email) + if (emailEnabled && !user.email) return { - response: "Username/Email invalid.", + response: "Email invalid.", + status: 400, + }; + else if (!user.username) + return { + response: "Username invalid.", status: 400, }; @@ -32,14 +41,18 @@ export default async function updateUser( const userIsTaken = await prisma.user.findFirst({ where: { id: { not: sessionUser.id }, - OR: [ - { - username: user.username.toLowerCase(), - }, - { - email: user.email.toLowerCase(), - }, - ], + OR: emailEnabled + ? [ + { + username: user.username.toLowerCase(), + }, + { + email: user.email?.toLowerCase(), + }, + ] + : { + username: user.username.toLowerCase(), + }, }, }); @@ -58,6 +71,8 @@ export default async function updateUser( try { const base64Data = profilePic.replace(/^data:image\/jpeg;base64,/, ""); + createFolder({ filePath: `uploads/avatar` }); + await createFile({ filePath: `uploads/avatar/${sessionUser.id}.jpg`, data: base64Data, @@ -102,12 +117,12 @@ export default async function updateUser( const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const PRICE_ID = process.env.PRICE_ID; - if (STRIPE_SECRET_KEY && PRICE_ID) + if (STRIPE_SECRET_KEY && PRICE_ID && emailEnabled) await updateCustomerEmail( STRIPE_SECRET_KEY, PRICE_ID, sessionUser.email, - user.email + user.email as string ); const { password, ...userInfo } = updatedUser; From 4be3125f9a1883c59cd803af53e9c1d2c4f641be Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Aug 2023 18:31:40 -0400 Subject: [PATCH 07/20] easier docker setup --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 219d689..7116bf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: - ./pgdata:/var/lib/postgresql/data linkwarden: env_file: .env + environment: + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres restart: always build: . ports: From 32e7bfe09caf01c82be194309e15d51dd6de69a9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Aug 2023 19:01:20 -0400 Subject: [PATCH 08/20] Update confirmation.tsx --- pages/confirmation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/confirmation.tsx b/pages/confirmation.tsx index 59f4e0b..e9025c0 100644 --- a/pages/confirmation.tsx +++ b/pages/confirmation.tsx @@ -15,7 +15,7 @@ export default function EmailConfirmaion() {

- If you didn't recieve anything, go to the{" "} + If you didn't receive anything, go to the{" "} Password Recovery {" "} From 21525b292015cceca38b238358442a451fde430b Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Aug 2023 19:44:49 -0400 Subject: [PATCH 09/20] updated readme --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e1d53cb..5a0778e 100644 --- a/README.md +++ b/README.md @@ -54,16 +54,15 @@ We highly recommend you **not** to use the old version as it is no longer mainta There are _many_ upcoming features, below are only _some_ of the 100% planned ones: -- 🐳 Docker version. - 🌒 Dark mode. - 📦 Import/Export your data. - 🧩 Browser extention. -Also make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/projects/1). +Also make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/projects/1). ## Docs -Currently, the Documentation is a bit targeted towards a more tech-savvy audience and has so much room to improve, you can find it [here](https://docs.linkwarden.app). +For information on how to get started or ot set up your own instance please visit the [documenation](https://docs.linkwarden.app). ## Development From 264ea03e633566edae816d95909f3ac8ebef1b79 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Aug 2023 19:48:23 -0400 Subject: [PATCH 10/20] fixed typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a0778e..e1bf05c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

-[Homepage](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app/getting-started) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/linkwarden/linkwarden#roadmap) | [Screenshots](https://github.com/linkwarden/linkwarden#screenshots) | [Support ❤](https://github.com/linkwarden/linkwarden#support-) +[Website](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app/getting-started) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/linkwarden/linkwarden#roadmap) | [Screenshots](https://github.com/linkwarden/linkwarden#screenshots) | [Support ❤](https://github.com/linkwarden/linkwarden#support-)
@@ -62,7 +62,7 @@ Also make sure to check out our [public roadmap](https://github.com/orgs/linkwar ## Docs -For information on how to get started or ot set up your own instance please visit the [documenation](https://docs.linkwarden.app). +For information on how to get started or to set up your own instance, please visit the [documentation](https://docs.linkwarden.app). ## Development From 22093c0c2982c31a237ac3547d5935c3bbacd02a Mon Sep 17 00:00:00 2001 From: Jordan Higuera Higuera Date: Thu, 3 Aug 2023 21:33:51 -0700 Subject: [PATCH 11/20] add sqlite compatibility + fix whitespace bug collections --- .gitignore | 2 + lib/api/controllers/links/getLinks.ts | 5 -- lib/api/controllers/links/postLink.ts | 7 +- lib/api/controllers/users/getUsers.ts | 13 ++- lib/api/controllers/users/updateUser.ts | 47 +++++++++- pages/api/avatar/[id].ts | 7 +- prisma/schema.prisma | 115 +++++++++++++----------- types/global.ts | 1 + 8 files changed, 130 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index e10e251..77956c8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,8 @@ next-env.d.ts # generated files and folders /data +.idea +prisma/dev.db # tests /tests diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index d5ed6df..22df9fb 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -66,7 +66,6 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.name ? query.searchQuery : undefined, - mode: "insensitive", }, }, { @@ -75,7 +74,6 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.url ? query.searchQuery : undefined, - mode: "insensitive", }, }, { @@ -84,7 +82,6 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.description ? query.searchQuery : undefined, - mode: "insensitive", }, }, { @@ -100,7 +97,6 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.tags ? { contains: query.searchQuery, - mode: "insensitive", } : undefined, OR: [ @@ -114,7 +110,6 @@ export default async function getLink(userId: number, body: string) { query.searchFilter?.tags ? query.searchQuery : undefined, - mode: "insensitive", }, collection: { members: { diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 3a5bfbd..3653eb9 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -20,12 +20,15 @@ export default async function postLink( }; } - link.collection.name = link.collection.name.trim(); - + // This has to move above we assign link.collection.name + // Because if the link is null (write then delete text on collection) + // It will try to do trim on empty string and will throw and error, this prevents it. if (!link.collection.name) { link.collection.name = "Unnamed Collection"; } + link.collection.name = link.collection.name.trim(); + if (link.collection.id) { const collectionIsAccessible = (await getPermission( userId, diff --git a/lib/api/controllers/users/getUsers.ts b/lib/api/controllers/users/getUsers.ts index 381a713..9d752a6 100644 --- a/lib/api/controllers/users/getUsers.ts +++ b/lib/api/controllers/users/getUsers.ts @@ -17,14 +17,23 @@ export default async function getUser({ id: params.lookupId, username: params.lookupUsername?.toLowerCase(), }, + include: { + whitelistedUsers: { + select: { + username: true + } + } + } }); if (!user) return { response: "User not found.", status: 404 }; + const whitelistedUsernames = user.whitelistedUsers?.map(usernames => usernames.username); + if ( !isSelf && user?.isPrivate && - !user.whitelistedUsers.includes(username.toLowerCase()) + !whitelistedUsernames.includes(username.toLowerCase()) ) { return { response: "This profile is private.", status: 401 }; } @@ -33,7 +42,7 @@ export default async function getUser({ const data = isSelf ? // If user is requesting its own data - lessSensitiveInfo + {...lessSensitiveInfo, whitelistedUsers: whitelistedUsernames} : { // If user is requesting someone elses data id: lessSensitiveInfo.id, diff --git a/lib/api/controllers/users/updateUser.ts b/lib/api/controllers/users/updateUser.ts index 1f41fb5..91674ba 100644 --- a/lib/api/controllers/users/updateUser.ts +++ b/lib/api/controllers/users/updateUser.ts @@ -106,14 +106,56 @@ export default async function updateUser( username: user.username.toLowerCase(), email: user.email?.toLowerCase(), isPrivate: user.isPrivate, - whitelistedUsers: user.whitelistedUsers, password: user.newPassword && user.newPassword !== "" ? newHashedPassword : undefined, }, + include: { + whitelistedUsers: true + } }); + + const { whitelistedUsers, password, ...userInfo } = updatedUser; + + // If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed + const newWhitelistedUsernames: string[] = user.whitelistedUsers || []; + + // Get the current whitelisted usernames + const currentWhitelistedUsernames: string[] = whitelistedUsers.map((user) => user.username); + + // Find the usernames to be deleted (present in current but not in new) + const usernamesToDelete: string[] = currentWhitelistedUsernames.filter( + (username) => !newWhitelistedUsernames.includes(username) + ); + + // Find the usernames to be created (present in new but not in current) + const usernamesToCreate: string[] = newWhitelistedUsernames.filter( + (username) => !currentWhitelistedUsernames.includes(username) && username.trim() !== '' + ); + + // Delete whitelistedUsers that are not present in the new list + await prisma.whitelistedUser.deleteMany({ + where: { + userId: sessionUser.id, + username: { + in: usernamesToDelete, + }, + }, + }); + + // Create new whitelistedUsers that are not in the current list, no create many ;( + for (const username of usernamesToCreate) { + await prisma.whitelistedUser.create({ + data: { + username, + userId: sessionUser.id, + }, + }); + } + + const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const PRICE_ID = process.env.PRICE_ID; @@ -125,10 +167,9 @@ export default async function updateUser( user.email as string ); - const { password, ...userInfo } = updatedUser; - const response: Omit = { ...userInfo, + whitelistedUsers: newWhitelistedUsernames, profilePic: `/api/avatar/${userInfo.id}?${Date.now()}`, }; diff --git a/pages/api/avatar/[id].ts b/pages/api/avatar/[id].ts index 73b155b..ac4f1d3 100644 --- a/pages/api/avatar/[id].ts +++ b/pages/api/avatar/[id].ts @@ -33,11 +33,16 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { where: { id: queryId, }, + include: { + whitelistedUsers: true + } }); + const whitelistedUsernames = targetUser?.whitelistedUsers.map(whitelistedUsername => whitelistedUsername.username); + if ( targetUser?.isPrivate && - !targetUser.whitelistedUsers.includes(username) + !whitelistedUsernames?.includes(username) ) { return res .setHeader("Content-Type", "text/plain") diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 43d8b90..da10e49 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,22 +4,22 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL") + url = env("DATABASE_URL") } model Account { - id String @id @default(cuid()) - userId Int - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? + id String @id @default(cuid()) + userId Int + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -35,29 +35,37 @@ model Session { } model User { - id Int @id @default(autoincrement()) - name String + id Int @id @default(autoincrement()) + name String - username String? @unique + username String? @unique - email String? @unique - emailVerified DateTime? - image String? + email String? @unique + emailVerified DateTime? + image String? - accounts Account[] - sessions Session[] - - password String - collections Collection[] + accounts Account[] + sessions Session[] - tags Tag[] + password String + collections Collection[] - pinnedLinks Link[] - - collectionsJoined UsersAndCollections[] - isPrivate Boolean @default(false) - whitelistedUsers String[] @default([]) - createdAt DateTime @default(now()) + tags Tag[] + + pinnedLinks Link[] + + collectionsJoined UsersAndCollections[] + isPrivate Boolean @default(false) + whitelistedUsers whitelistedUser[] + createdAt DateTime @default(now()) +} + +model whitelistedUser { + id Int @id @default(autoincrement()) + + username String @default("") + User User? @relation(fields: [userId], references: [id]) + userId Int? } model VerificationToken { @@ -69,27 +77,26 @@ model VerificationToken { } model Collection { - id Int @id @default(autoincrement()) - name String - description String @default("") - color String @default("#0ea5e9") - isPublic Boolean @default(false) + id Int @id @default(autoincrement()) + name String + description String @default("") + color String @default("#0ea5e9") + isPublic Boolean @default(false) - - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - members UsersAndCollections[] - links Link[] - createdAt DateTime @default(now()) + owner User @relation(fields: [ownerId], references: [id]) + ownerId Int + members UsersAndCollections[] + links Link[] + createdAt DateTime @default(now()) @@unique([name, ownerId]) } model UsersAndCollections { - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) userId Int - collection Collection @relation(fields: [collectionId], references: [id]) + collection Collection @relation(fields: [collectionId], references: [id]) collectionId Int canCreate Boolean @@ -100,24 +107,24 @@ model UsersAndCollections { } model Link { - id Int @id @default(autoincrement()) - name String - url String + id Int @id @default(autoincrement()) + name String + url String description String @default("") pinnedBy User[] - collection Collection @relation(fields: [collectionId], references: [id]) + collection Collection @relation(fields: [collectionId], references: [id]) collectionId Int - tags Tag[] - createdAt DateTime @default(now()) + tags Tag[] + createdAt DateTime @default(now()) } model Tag { - id Int @id @default(autoincrement()) - name String - links Link[] - owner User @relation(fields: [ownerId], references: [id]) + id Int @id @default(autoincrement()) + name String + links Link[] + owner User @relation(fields: [ownerId], references: [id]) ownerId Int @@unique([name, ownerId]) diff --git a/types/global.ts b/types/global.ts index 95b2640..04b68e0 100644 --- a/types/global.ts +++ b/types/global.ts @@ -36,6 +36,7 @@ export interface CollectionIncludingMembersAndLinkCount export interface AccountSettings extends User { profilePic: string; newPassword?: string; + whitelistedUsers: string[] } interface LinksIncludingTags extends Link { From 895ef8e60f7e66e2b7be470cb18d66908dba9dac Mon Sep 17 00:00:00 2001 From: Jordan Higuera Higuera Date: Fri, 4 Aug 2023 10:08:04 -0700 Subject: [PATCH 12/20] add mode insensitive in case we are using postgresql + rename table --- lib/api/controllers/links/getLinks.ts | 279 +++++++++++++------------- prisma/schema.prisma | 6 +- 2 files changed, 146 insertions(+), 139 deletions(-) diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index 22df9fb..4f9b1e6 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -1,145 +1,152 @@ -import { prisma } from "@/lib/api/db"; -import { LinkRequestQuery, Sort } from "@/types/global"; +import {prisma} from "@/lib/api/db"; +import {LinkRequestQuery, Sort} from "@/types/global"; +import * as process from "process"; export default async function getLink(userId: number, body: string) { - const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); - console.log(query); + const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); + console.log(query); - // Sorting logic - let order: any; - if (query.sort === Sort.DateNewestFirst) - order = { - createdAt: "desc", - }; - else if (query.sort === Sort.DateOldestFirst) - order = { - createdAt: "asc", - }; - else if (query.sort === Sort.NameAZ) - order = { - name: "asc", - }; - else if (query.sort === Sort.NameZA) - order = { - name: "desc", - }; - else if (query.sort === Sort.DescriptionAZ) - order = { - name: "asc", - }; - else if (query.sort === Sort.DescriptionZA) - order = { - name: "desc", - }; + const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); + // Sorting logic + let order: any; + if (query.sort === Sort.DateNewestFirst) + order = { + createdAt: "desc", + }; + else if (query.sort === Sort.DateOldestFirst) + order = { + createdAt: "asc", + }; + else if (query.sort === Sort.NameAZ) + order = { + name: "asc", + }; + else if (query.sort === Sort.NameZA) + order = { + name: "desc", + }; + else if (query.sort === Sort.DescriptionAZ) + order = { + name: "asc", + }; + else if (query.sort === Sort.DescriptionZA) + order = { + name: "desc", + }; - const links = await prisma.link.findMany({ - take: Number(process.env.PAGINATION_TAKE_COUNT) || 20, - skip: query.cursor ? 1 : undefined, - cursor: query.cursor - ? { - id: query.cursor, - } - : undefined, - where: { - collection: { - id: query.collectionId ? query.collectionId : undefined, // If collectionId was defined, filter by collection - OR: [ - { - ownerId: userId, - }, - { - members: { - some: { - userId, - }, - }, - }, - ], - }, - [query.searchQuery ? "OR" : "AND"]: [ - { - pinnedBy: query.pinnedOnly ? { some: { id: userId } } : undefined, - }, - { - name: { - contains: - query.searchQuery && query.searchFilter?.name - ? query.searchQuery - : undefined, - }, - }, - { - url: { - contains: - query.searchQuery && query.searchFilter?.url - ? query.searchQuery - : undefined, - }, - }, - { - description: { - contains: - query.searchQuery && query.searchFilter?.description - ? query.searchQuery - : undefined, - }, - }, - { - tags: - query.searchQuery && !query.searchFilter?.tags - ? undefined - : { - some: query.tagId - ? { - // If tagId was defined, filter by tag - id: query.tagId, - name: - query.searchQuery && query.searchFilter?.tags - ? { - contains: query.searchQuery, - } - : undefined, - OR: [ - { ownerId: userId }, // Tags owned by the user - { - links: { - some: { - name: { - contains: - query.searchQuery && - query.searchFilter?.tags - ? query.searchQuery - : undefined, - }, - collection: { - members: { - some: { - userId, // Tags from collections where the user is a member - }, - }, - }, - }, + const links = await prisma.link.findMany({ + take: Number(process.env.PAGINATION_TAKE_COUNT) || 20, + skip: query.cursor ? 1 : undefined, + cursor: query.cursor + ? { + id: query.cursor, + } + : undefined, + where: { + collection: { + id: query.collectionId ? query.collectionId : undefined, // If collectionId was defined, filter by collection + OR: [ + { + ownerId: userId, + }, + { + members: { + some: { + userId, }, - }, - ], - } - : undefined, + }, + }, + ], + }, + [query.searchQuery ? "OR" : "AND"]: [ + { + pinnedBy: query.pinnedOnly ? {some: {id: userId}} : undefined, }, + { + name: { + contains: + query.searchQuery && query.searchFilter?.name + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + }, + }, + { + url: { + contains: + query.searchQuery && query.searchFilter?.url + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + }, + }, + { + description: { + contains: + query.searchQuery && query.searchFilter?.description + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + }, + }, + { + tags: + query.searchQuery && !query.searchFilter?.tags + ? undefined + : { + some: query.tagId + ? { + // If tagId was defined, filter by tag + id: query.tagId, + name: + query.searchQuery && query.searchFilter?.tags + ? { + contains: query.searchQuery, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + } + : undefined, + OR: [ + {ownerId: userId}, // Tags owned by the user + { + links: { + some: { + name: { + contains: + query.searchQuery && + query.searchFilter?.tags + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + }, + collection: { + members: { + some: { + userId, // Tags from collections where the user is a member + }, + }, + }, + }, + }, + }, + ], + } + : undefined, + }, + }, + ], }, - ], - }, - include: { - tags: true, - collection: true, - pinnedBy: { - where: { id: userId }, - select: { id: true }, - }, - }, - orderBy: order || { - createdAt: "desc", - }, - }); + include: { + tags: true, + collection: true, + pinnedBy: { + where: {id: userId}, + select: {id: true}, + }, + }, + orderBy: order || { + createdAt: "desc", + }, + }); - return { response: links, status: 200 }; + return {response: links, status: 200}; } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index da10e49..dc9170b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,7 +3,7 @@ generator client { } datasource db { - provider = "postgresql" + provider = "sqlite" url = env("DATABASE_URL") } @@ -56,11 +56,11 @@ model User { collectionsJoined UsersAndCollections[] isPrivate Boolean @default(false) - whitelistedUsers whitelistedUser[] + whitelistedUsers WhitelistedUser[] createdAt DateTime @default(now()) } -model whitelistedUser { +model WhitelistedUser { id Int @id @default(autoincrement()) username String @default("") From 8747331c43677ac44dc43f1effbf01729fe50ff6 Mon Sep 17 00:00:00 2001 From: Jordan Higuera Higuera Date: Fri, 4 Aug 2023 10:13:23 -0700 Subject: [PATCH 13/20] remove unused import --- lib/api/controllers/links/getLinks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index 4f9b1e6..ee78512 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -1,6 +1,5 @@ import {prisma} from "@/lib/api/db"; import {LinkRequestQuery, Sort} from "@/types/global"; -import * as process from "process"; export default async function getLink(userId: number, body: string) { const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); From 91f9fcb500e399aed74388df65f4fc90517e6a79 Mon Sep 17 00:00:00 2001 From: Jordan Higuera Higuera Date: Fri, 4 Aug 2023 14:41:53 -0700 Subject: [PATCH 14/20] changed default provider --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dc9170b..c1c04d8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,7 +3,7 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" url = env("DATABASE_URL") } From 9405445332b75bb641094a0c0eea7fd7c450ac7c Mon Sep 17 00:00:00 2001 From: Jordan Higuera Higuera Date: Fri, 4 Aug 2023 15:10:31 -0700 Subject: [PATCH 15/20] identation fix --- lib/api/controllers/links/getLinks.ts | 280 +++++++++++++------------- 1 file changed, 140 insertions(+), 140 deletions(-) diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index ee78512..1601e20 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -2,150 +2,150 @@ import {prisma} from "@/lib/api/db"; import {LinkRequestQuery, Sort} from "@/types/global"; export default async function getLink(userId: number, body: string) { - const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); - console.log(query); + const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); + console.log(query); - const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); - // Sorting logic - let order: any; - if (query.sort === Sort.DateNewestFirst) - order = { - createdAt: "desc", - }; - else if (query.sort === Sort.DateOldestFirst) - order = { - createdAt: "asc", - }; - else if (query.sort === Sort.NameAZ) - order = { - name: "asc", - }; - else if (query.sort === Sort.NameZA) - order = { - name: "desc", - }; - else if (query.sort === Sort.DescriptionAZ) - order = { - name: "asc", - }; - else if (query.sort === Sort.DescriptionZA) - order = { - name: "desc", - }; + const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); + // Sorting logic + let order: any; + if (query.sort === Sort.DateNewestFirst) + order = { + createdAt: "desc", + }; + else if (query.sort === Sort.DateOldestFirst) + order = { + createdAt: "asc", + }; + else if (query.sort === Sort.NameAZ) + order = { + name: "asc", + }; + else if (query.sort === Sort.NameZA) + order = { + name: "desc", + }; + else if (query.sort === Sort.DescriptionAZ) + order = { + name: "asc", + }; + else if (query.sort === Sort.DescriptionZA) + order = { + name: "desc", + }; - const links = await prisma.link.findMany({ - take: Number(process.env.PAGINATION_TAKE_COUNT) || 20, - skip: query.cursor ? 1 : undefined, - cursor: query.cursor - ? { - id: query.cursor, - } - : undefined, - where: { - collection: { - id: query.collectionId ? query.collectionId : undefined, // If collectionId was defined, filter by collection - OR: [ - { - ownerId: userId, - }, - { - members: { - some: { - userId, + const links = await prisma.link.findMany({ + take: Number(process.env.PAGINATION_TAKE_COUNT) || 20, + skip: query.cursor ? 1 : undefined, + cursor: query.cursor + ? { + id: query.cursor, + } + : undefined, + where: { + collection: { + id: query.collectionId ? query.collectionId : undefined, // If collectionId was defined, filter by collection + OR: [ + { + ownerId: userId, + }, + { + members: { + some: { + userId, + }, + }, + }, + ], + }, + [query.searchQuery ? "OR" : "AND"]: [ + { + pinnedBy: query.pinnedOnly ? {some: {id: userId}} : undefined, + }, + { + name: { + contains: + query.searchQuery && query.searchFilter?.name + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + }, + }, + { + url: { + contains: + query.searchQuery && query.searchFilter?.url + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + }, + }, + { + description: { + contains: + query.searchQuery && query.searchFilter?.description + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + }, + }, + { + tags: + query.searchQuery && !query.searchFilter?.tags + ? undefined + : { + some: query.tagId + ? { + // If tagId was defined, filter by tag + id: query.tagId, + name: + query.searchQuery && query.searchFilter?.tags + ? { + contains: query.searchQuery, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + } + : undefined, + OR: [ + {ownerId: userId}, // Tags owned by the user + { + links: { + some: { + name: { + contains: + query.searchQuery && + query.searchFilter?.tags + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined }, + collection: { + members: { + some: { + userId, // Tags from collections where the user is a member + }, + }, + }, + }, }, - }, - ], - }, - [query.searchQuery ? "OR" : "AND"]: [ - { - pinnedBy: query.pinnedOnly ? {some: {id: userId}} : undefined, - }, - { - name: { - contains: - query.searchQuery && query.searchFilter?.name - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined - }, - }, - { - url: { - contains: - query.searchQuery && query.searchFilter?.url - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined - }, - }, - { - description: { - contains: - query.searchQuery && query.searchFilter?.description - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined - }, - }, - { - tags: - query.searchQuery && !query.searchFilter?.tags - ? undefined - : { - some: query.tagId - ? { - // If tagId was defined, filter by tag - id: query.tagId, - name: - query.searchQuery && query.searchFilter?.tags - ? { - contains: query.searchQuery, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined - } - : undefined, - OR: [ - {ownerId: userId}, // Tags owned by the user - { - links: { - some: { - name: { - contains: - query.searchQuery && - query.searchFilter?.tags - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined - }, - collection: { - members: { - some: { - userId, // Tags from collections where the user is a member - }, - }, - }, - }, - }, - }, - ], - } - : undefined, - }, - }, - ], + }, + ], + } + : undefined, + }, }, - include: { - tags: true, - collection: true, - pinnedBy: { - where: {id: userId}, - select: {id: true}, - }, - }, - orderBy: order || { - createdAt: "desc", - }, - }); + ], + }, + include: { + tags: true, + collection: true, + pinnedBy: { + where: {id: userId}, + select: {id: true}, + }, + }, + orderBy: order || { + createdAt: "desc", + }, + }); - return {response: links, status: 200}; + return {response: links, status: 200}; } From a56b8e24daa81179de3a6b9e69f23352b3f6eb06 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 4 Aug 2023 19:26:57 -0400 Subject: [PATCH 16/20] updated migration file --- lib/api/controllers/links/getLinks.ts | 92 ++++++++++--------- .../migration.sql | 20 ++++ 2 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 prisma/migrations/20230804230549_created_whitelisted_users_table/migration.sql diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index 1601e20..e61f2a1 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -1,5 +1,5 @@ -import {prisma} from "@/lib/api/db"; -import {LinkRequestQuery, Sort} from "@/types/global"; +import { prisma } from "@/lib/api/db"; +import { LinkRequestQuery, Sort } from "@/types/global"; export default async function getLink(userId: number, body: string) { const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); @@ -38,8 +38,8 @@ export default async function getLink(userId: number, body: string) { skip: query.cursor ? 1 : undefined, cursor: query.cursor ? { - id: query.cursor, - } + id: query.cursor, + } : undefined, where: { collection: { @@ -59,7 +59,7 @@ export default async function getLink(userId: number, body: string) { }, [query.searchQuery ? "OR" : "AND"]: [ { - pinnedBy: query.pinnedOnly ? {some: {id: userId}} : undefined, + pinnedBy: query.pinnedOnly ? { some: { id: userId } } : undefined, }, { name: { @@ -67,7 +67,7 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.name ? query.searchQuery : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, }, }, { @@ -76,7 +76,7 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.url ? query.searchQuery : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, }, }, { @@ -85,7 +85,7 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.description ? query.searchQuery : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, }, }, { @@ -93,44 +93,48 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && !query.searchFilter?.tags ? undefined : { - some: query.tagId - ? { - // If tagId was defined, filter by tag - id: query.tagId, - name: - query.searchQuery && query.searchFilter?.tags - ? { - contains: query.searchQuery, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined - } - : undefined, - OR: [ - {ownerId: userId}, // Tags owned by the user - { - links: { - some: { - name: { - contains: - query.searchQuery && - query.searchFilter?.tags - ? query.searchQuery + some: query.tagId + ? { + // If tagId was defined, filter by tag + id: query.tagId, + name: + query.searchQuery && query.searchFilter?.tags + ? { + contains: query.searchQuery, + mode: POSTGRES_IS_ENABLED + ? "insensitive" : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined - }, - collection: { - members: { - some: { - userId, // Tags from collections where the user is a member + } + : undefined, + OR: [ + { ownerId: userId }, // Tags owned by the user + { + links: { + some: { + name: { + contains: + query.searchQuery && + query.searchFilter?.tags + ? query.searchQuery + : undefined, + mode: POSTGRES_IS_ENABLED + ? "insensitive" + : undefined, + }, + collection: { + members: { + some: { + userId, // Tags from collections where the user is a member + }, + }, }, }, }, }, - }, - }, - ], - } - : undefined, - }, + ], + } + : undefined, + }, }, ], }, @@ -138,8 +142,8 @@ export default async function getLink(userId: number, body: string) { tags: true, collection: true, pinnedBy: { - where: {id: userId}, - select: {id: true}, + where: { id: userId }, + select: { id: true }, }, }, orderBy: order || { @@ -147,5 +151,5 @@ export default async function getLink(userId: number, body: string) { }, }); - return {response: links, status: 200}; + return { response: links, status: 200 }; } diff --git a/prisma/migrations/20230804230549_created_whitelisted_users_table/migration.sql b/prisma/migrations/20230804230549_created_whitelisted_users_table/migration.sql new file mode 100644 index 0000000..c3bc00e --- /dev/null +++ b/prisma/migrations/20230804230549_created_whitelisted_users_table/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - You are about to drop the column `whitelistedUsers` on the `User` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "whitelistedUsers"; + +-- CreateTable +CREATE TABLE "WhitelistedUser" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL DEFAULT '', + "userId" INTEGER, + + CONSTRAINT "WhitelistedUser_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "WhitelistedUser" ADD CONSTRAINT "WhitelistedUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; From 02b7a901603ac17917a7999ce935f2ffab1e7420 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 4 Aug 2023 20:32:13 -0400 Subject: [PATCH 17/20] minor fix --- lib/api/controllers/users/updateUser.ts | 23 +++++++++++++---------- lib/api/db.ts | 13 +++++++------ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/api/controllers/users/updateUser.ts b/lib/api/controllers/users/updateUser.ts index 91674ba..f953557 100644 --- a/lib/api/controllers/users/updateUser.ts +++ b/lib/api/controllers/users/updateUser.ts @@ -50,9 +50,11 @@ export default async function updateUser( email: user.email?.toLowerCase(), }, ] - : { - username: user.username.toLowerCase(), - }, + : [ + { + username: user.username.toLowerCase(), + }, + ], }, }); @@ -112,27 +114,29 @@ export default async function updateUser( : undefined, }, include: { - whitelistedUsers: true - } + whitelistedUsers: true, + }, }); - const { whitelistedUsers, password, ...userInfo } = updatedUser; // If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed const newWhitelistedUsernames: string[] = user.whitelistedUsers || []; // Get the current whitelisted usernames - const currentWhitelistedUsernames: string[] = whitelistedUsers.map((user) => user.username); + const currentWhitelistedUsernames: string[] = whitelistedUsers.map( + (user) => user.username + ); // Find the usernames to be deleted (present in current but not in new) const usernamesToDelete: string[] = currentWhitelistedUsernames.filter( - (username) => !newWhitelistedUsernames.includes(username) + (username) => !newWhitelistedUsernames.includes(username) ); // Find the usernames to be created (present in new but not in current) const usernamesToCreate: string[] = newWhitelistedUsernames.filter( - (username) => !currentWhitelistedUsernames.includes(username) && username.trim() !== '' + (username) => + !currentWhitelistedUsernames.includes(username) && username.trim() !== "" ); // Delete whitelistedUsers that are not present in the new list @@ -155,7 +159,6 @@ export default async function updateUser( }); } - const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const PRICE_ID = process.env.PRICE_ID; diff --git a/lib/api/db.ts b/lib/api/db.ts index 49e21fa..01c4c7d 100644 --- a/lib/api/db.ts +++ b/lib/api/db.ts @@ -10,9 +10,10 @@ export const prisma = if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; -if (process.env.NODE_ENV !== "production") - prisma.$on("query" as any, (e: any) => { - console.log("Query: " + e.query); - console.log("Params: " + e.params); - console.log("\x1b[31m", `Duration: ${e.duration}ms`, "\x1b[0m"); // For benchmarking - }); +// For benchmarking | uncomment when needed +// if (process.env.NODE_ENV !== "production") +// prisma.$on("query" as any, (e: any) => { +// console.log("Query: " + e.query); +// console.log("Params: " + e.params); +// console.log("\x1b[31m", `Duration: ${e.duration}ms`, "\x1b[0m"); +// }); From 159075b38b273a617f6029a69c31a4a24596a948 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 6 Aug 2023 00:58:18 -0400 Subject: [PATCH 18/20] bug fix --- lib/api/archive.ts | 10 ++----- lib/api/controllers/links/postLink.ts | 2 +- lib/api/controllers/links/updateLink.ts | 13 +++++++++ lib/api/storage/moveFile.ts | 37 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 lib/api/storage/moveFile.ts diff --git a/lib/api/archive.ts b/lib/api/archive.ts index 24c3ff6..80aa655 100644 --- a/lib/api/archive.ts +++ b/lib/api/archive.ts @@ -2,11 +2,7 @@ import { Page, chromium, devices } from "playwright"; import { prisma } from "@/lib/api/db"; import createFile from "@/lib/api/storage/createFile"; -export default async function archive( - url: string, - collectionId: number, - linkId: number -) { +export default async function archive(linkId: number, url: string) { const browser = await chromium.launch(); const context = await browser.newContext(devices["Desktop Chrome"]); const page = await context.newPage(); @@ -35,12 +31,12 @@ export default async function archive( createFile({ data: screenshot, - filePath: `archives/${collectionId}/${linkId}.png`, + filePath: `archives/${linkExists.collectionId}/${linkId}.png`, }); createFile({ data: pdf, - filePath: `archives/${collectionId}/${linkId}.pdf`, + filePath: `archives/${linkExists.collectionId}/${linkId}.pdf`, }); } diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 3653eb9..f81a59f 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -97,7 +97,7 @@ export default async function postLink( createFolder({ filePath: `archives/${newLink.collectionId}` }); - archive(newLink.url, newLink.collectionId, newLink.id); + archive(newLink.id, newLink.url); return { response: newLink, status: 200 }; } diff --git a/lib/api/controllers/links/updateLink.ts b/lib/api/controllers/links/updateLink.ts index cdd0f67..24836c1 100644 --- a/lib/api/controllers/links/updateLink.ts +++ b/lib/api/controllers/links/updateLink.ts @@ -2,6 +2,7 @@ import { prisma } from "@/lib/api/db"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { Collection, Link, UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; +import moveFile from "@/lib/api/storage/moveFile"; export default async function updateLink( link: LinkIncludingShortenedCollectionAndTags, @@ -98,6 +99,18 @@ export default async function updateLink( }, }); + if (targetLink.collection.id !== link.collection.id) { + await moveFile( + `archives/${targetLink.collection.id}/${link.id}.pdf`, + `archives/${link.collection.id}/${link.id}.pdf` + ); + + await moveFile( + `archives/${targetLink.collection.id}/${link.id}.png`, + `archives/${link.collection.id}/${link.id}.png` + ); + } + return { response: updatedLink, status: 200 }; } } diff --git a/lib/api/storage/moveFile.ts b/lib/api/storage/moveFile.ts new file mode 100644 index 0000000..bbd8887 --- /dev/null +++ b/lib/api/storage/moveFile.ts @@ -0,0 +1,37 @@ +import fs from "fs"; +import path from "path"; +import s3Client from "./s3Client"; +import removeFile from "./removeFile"; + +export default async function moveFile(from: string, to: string) { + if (s3Client) { + const Bucket = process.env.BUCKET_NAME; + + const copyParams = { + Bucket: Bucket, + CopySource: `/${Bucket}/${from}`, + Key: to, + }; + + try { + s3Client.copyObject(copyParams, async (err: any) => { + if (err) { + console.error("Error copying the object:", err); + } else { + await removeFile({ filePath: from }); + } + }); + } catch (err) { + console.log("Error:", err); + } + } else { + const storagePath = process.env.STORAGE_FOLDER || "data"; + + const directory = (file: string) => + path.join(process.cwd(), storagePath + "/" + file); + + fs.rename(directory(from), directory(to), (err) => { + if (err) console.log("Error copying file:", err); + }); + } +} From d008c441b7a77859f827c8a1f744d1c50e3609c6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 10 Aug 2023 12:16:44 -0400 Subject: [PATCH 19/20] Feat/import export (#136) * added import/export functionality --- .env.sample | 1 + components/Modal/User/PrivacySettings.tsx | 88 +++++++++++++++++++ components/Sidebar.tsx | 12 +-- lib/api/archive.ts | 55 ++++++------ .../controllers/collections/postCollection.ts | 8 -- lib/api/controllers/data/getData.ts | 24 +++++ lib/api/controllers/data/postData.ts | 86 ++++++++++++++++++ lib/api/controllers/links/postLink.ts | 5 +- lib/api/storage/readFile.ts | 53 ++++++++--- lib/client/avatarExists.ts | 2 +- pages/api/archives/[...params].ts | 8 +- pages/api/avatar/[id].ts | 32 +++---- pages/api/data/index.ts | 31 +++++++ prisma/schema.prisma | 29 +++--- types/enviornment.d.ts | 1 + types/global.ts | 10 ++- 16 files changed, 352 insertions(+), 93 deletions(-) create mode 100644 lib/api/controllers/data/getData.ts create mode 100644 lib/api/controllers/data/postData.ts create mode 100644 pages/api/data/index.ts diff --git a/.env.sample b/.env.sample index 451abee..4bf7c56 100644 --- a/.env.sample +++ b/.env.sample @@ -6,6 +6,7 @@ NEXTAUTH_URL=http://localhost:3000 PAGINATION_TAKE_COUNT= STORAGE_FOLDER= +AUTOSCROLL_TIMEOUT= # AWS S3 Settings SPACES_KEY= diff --git a/components/Modal/User/PrivacySettings.tsx b/components/Modal/User/PrivacySettings.tsx index 1f66103..e209ae2 100644 --- a/components/Modal/User/PrivacySettings.tsx +++ b/components/Modal/User/PrivacySettings.tsx @@ -6,6 +6,9 @@ import { signOut, useSession } from "next-auth/react"; import { faPenToSquare } from "@fortawesome/free-regular-svg-icons"; import SubmitButton from "../../SubmitButton"; import { toast } from "react-hot-toast"; +import Link from "next/link"; +import ClickAwayHandler from "@/components/ClickAwayHandler"; +import useInitialData from "@/hooks/useInitialData"; type Props = { toggleSettingsModal: Function; @@ -21,6 +24,7 @@ export default function PrivacySettings({ const { update, data } = useSession(); const { account, updateAccount } = useAccountStore(); + const [importDropdown, setImportDropdown] = useState(false); const [submitLoader, setSubmitLoader] = useState(false); const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState( @@ -46,6 +50,38 @@ export default function PrivacySettings({ return wordsArray; }; + const postJSONFile = async (e: any) => { + const file: File = e.target.files[0]; + + if (file) { + var reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = async function (e) { + const load = toast.loading("Importing..."); + + const response = await fetch("/api/data", { + method: "POST", + body: e.target?.result, + }); + + const data = await response.json(); + + toast.dismiss(load); + + toast.success("Imported the Bookmarks! Reloading the page..."); + + setImportDropdown(false); + + setTimeout(() => { + location.reload(); + }, 2000); + }; + reader.onerror = function (e) { + console.log("Error:", e); + }; + } + }; + const submit = async () => { setSubmitLoader(true); @@ -115,6 +151,58 @@ export default function PrivacySettings({ )} +
+

Import/Export Data

+ +
+
setImportDropdown(true)} + className="w-fit relative" + id="import-dropdown" + > +
+ Import From +
+ {importDropdown ? ( + { + const target = e.target as HTMLInputElement; + if (target.id !== "import-dropdown") setImportDropdown(false); + }} + className={`absolute top-7 left-0 w-36 py-1 shadow-md border border-sky-100 bg-gray-50 rounded-md flex flex-col z-20`} + > +
+ +
+
+ ) : null} +
+ + +
+ Export Data +
+ +
+
+ -

Dashboard

+

+ Dashboard +

-

- All Links -

+

Links

-

- All Collections +

+ Collections

diff --git a/lib/api/archive.ts b/lib/api/archive.ts index 80aa655..1163f26 100644 --- a/lib/api/archive.ts +++ b/lib/api/archive.ts @@ -10,7 +10,10 @@ export default async function archive(linkId: number, url: string) { try { await page.goto(url, { waitUntil: "domcontentloaded" }); - await autoScroll(page); + await page.evaluate( + autoScroll, + Number(process.env.AUTOSCROLL_TIMEOUT) || 30 + ); const linkExists = await prisma.link.findUnique({ where: { @@ -47,29 +50,31 @@ export default async function archive(linkId: number, url: string) { } } -const autoScroll = async (page: Page) => { - await page.evaluate(async () => { - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error("Auto scroll took too long (more than 20 seconds).")); - }, 20000); - }); - - const scrollingPromise = new Promise((resolve) => { - let totalHeight = 0; - let distance = 100; - let scrollDown = setInterval(() => { - let scrollHeight = document.body.scrollHeight; - window.scrollBy(0, distance); - totalHeight += distance; - if (totalHeight >= scrollHeight) { - clearInterval(scrollDown); - window.scroll(0, 0); - resolve(); - } - }, 100); - }); - - await Promise.race([scrollingPromise, timeoutPromise]); +const autoScroll = async (AUTOSCROLL_TIMEOUT: number) => { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject( + new Error( + `Auto scroll took too long (more than ${AUTOSCROLL_TIMEOUT} seconds).` + ) + ); + }, AUTOSCROLL_TIMEOUT * 1000); }); + + const scrollingPromise = new Promise((resolve) => { + let totalHeight = 0; + let distance = 100; + let scrollDown = setInterval(() => { + let scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= scrollHeight) { + clearInterval(scrollDown); + window.scroll(0, 0); + resolve(); + } + }, 100); + }); + + await Promise.race([scrollingPromise, timeoutPromise]); }; diff --git a/lib/api/controllers/collections/postCollection.ts b/lib/api/controllers/collections/postCollection.ts index 917f02f..86ca379 100644 --- a/lib/api/controllers/collections/postCollection.ts +++ b/lib/api/controllers/collections/postCollection.ts @@ -40,14 +40,6 @@ export default async function postCollection( name: collection.name.trim(), description: collection.description, color: collection.color, - members: { - create: collection.members.map((e) => ({ - user: { connect: { id: e.user.id } }, - canCreate: e.canCreate, - canUpdate: e.canUpdate, - canDelete: e.canDelete, - })), - }, }, include: { _count: { diff --git a/lib/api/controllers/data/getData.ts b/lib/api/controllers/data/getData.ts new file mode 100644 index 0000000..6b9e2b0 --- /dev/null +++ b/lib/api/controllers/data/getData.ts @@ -0,0 +1,24 @@ +import { prisma } from "@/lib/api/db"; + +export default async function getData(userId: number) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + collections: { + include: { + links: { + include: { + tags: true, + }, + }, + }, + }, + }, + }); + + if (!user) return { response: "User not found.", status: 404 }; + + const { password, id, image, ...userData } = user; + + return { response: userData, status: 200 }; +} diff --git a/lib/api/controllers/data/postData.ts b/lib/api/controllers/data/postData.ts new file mode 100644 index 0000000..b9af119 --- /dev/null +++ b/lib/api/controllers/data/postData.ts @@ -0,0 +1,86 @@ +import { prisma } from "@/lib/api/db"; +import { Backup } from "@/types/global"; +import createFolder from "@/lib/api/storage/createFolder"; + +export default async function getData(userId: number, data: Backup) { + // Import collections + try { + data.collections.forEach(async (e) => { + e.name = e.name.trim(); + + const findCollection = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + collections: { + where: { + name: e.name, + }, + }, + }, + }); + + const checkIfCollectionExists = findCollection?.collections[0]; + + let collectionId = findCollection?.collections[0]?.id; + + if (!checkIfCollectionExists) { + const newCollection = await prisma.collection.create({ + data: { + owner: { + connect: { + id: userId, + }, + }, + name: e.name, + description: e.description, + color: e.color, + }, + }); + + createFolder({ filePath: `archives/${newCollection.id}` }); + + collectionId = newCollection.id; + } + + // Import Links + e.links.forEach(async (e) => { + const newLink = await prisma.link.create({ + data: { + url: e.url, + name: e.name, + description: e.description, + collection: { + connect: { + id: collectionId, + }, + }, + tags: { + connectOrCreate: e.tags.map((tag) => ({ + where: { + name_ownerId: { + name: tag.name.trim(), + ownerId: userId, + }, + }, + create: { + name: tag.name.trim(), + owner: { + connect: { + id: userId, + }, + }, + }, + })), + }, + }, + }); + }); + }); + } catch (err) { + console.log(err); + } + + return { response: "Success.", status: 200 }; +} diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index f81a59f..66d8e91 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -20,9 +20,6 @@ export default async function postLink( }; } - // This has to move above we assign link.collection.name - // Because if the link is null (write then delete text on collection) - // It will try to do trim on empty string and will throw and error, this prevents it. if (!link.collection.name) { link.collection.name = "Unnamed Collection"; } @@ -54,7 +51,7 @@ export default async function postLink( ? link.description : await getTitle(link.url); - const newLink: Link = await prisma.link.create({ + const newLink = await prisma.link.create({ data: { url: link.url, name: link.name, diff --git a/lib/api/storage/readFile.ts b/lib/api/storage/readFile.ts index 0cad83b..57dd950 100644 --- a/lib/api/storage/readFile.ts +++ b/lib/api/storage/readFile.ts @@ -9,12 +9,14 @@ import s3Client from "./s3Client"; import util from "util"; type ReturnContentTypes = - | "text/plain" + | "text/html" | "image/jpeg" | "image/png" | "application/pdf"; -export default async function readFile({ filePath }: { filePath: string }) { +export default async function readFile(filePath: string) { + const isRequestingAvatar = filePath.startsWith("uploads/avatar"); + let contentType: ReturnContentTypes; if (s3Client) { @@ -28,6 +30,7 @@ export default async function readFile({ filePath }: { filePath: string }) { | { file: Buffer | string; contentType: ReturnContentTypes; + status: number; } | undefined; @@ -38,11 +41,12 @@ export default async function readFile({ filePath }: { filePath: string }) { try { await headObjectAsync(bucketParams); } catch (err) { - contentType = "text/plain"; + contentType = "text/html"; returnObject = { - file: "File not found, it's possible that the file you're looking for either doesn't exist or hasn't been created yet.", + file: isRequestingAvatar ? "File not found." : fileNotFoundTemplate, contentType, + status: isRequestingAvatar ? 200 : 400, }; } @@ -60,14 +64,14 @@ export default async function readFile({ filePath }: { filePath: string }) { // if (filePath.endsWith(".jpg")) contentType = "image/jpeg"; } - returnObject = { file: data as Buffer, contentType }; + returnObject = { file: data as Buffer, contentType, status: 200 }; } return returnObject; } catch (err) { console.log("Error:", err); - contentType = "text/plain"; + contentType = "text/html"; return { file: "An internal occurred, please contact support.", contentType, @@ -77,13 +81,7 @@ export default async function readFile({ filePath }: { filePath: string }) { const storagePath = process.env.STORAGE_FOLDER || "data"; const creationPath = path.join(process.cwd(), storagePath + "/" + filePath); - const file = fs.existsSync(creationPath) - ? fs.readFileSync(creationPath) - : "File not found, it's possible that the file you're looking for either doesn't exist or hasn't been created yet."; - - if (file.toString().startsWith("File not found")) { - contentType = "text/plain"; - } else if (filePath.endsWith(".pdf")) { + if (filePath.endsWith(".pdf")) { contentType = "application/pdf"; } else if (filePath.endsWith(".png")) { contentType = "image/png"; @@ -92,7 +90,16 @@ export default async function readFile({ filePath }: { filePath: string }) { contentType = "image/jpeg"; } - return { file, contentType }; + if (!fs.existsSync(creationPath)) + return { + file: isRequestingAvatar ? "File not found." : fileNotFoundTemplate, + contentType: "text/html", + status: isRequestingAvatar ? 200 : 400, + }; + else { + const file = fs.readFileSync(creationPath); + return { file, contentType, status: 200 }; + } } } @@ -105,3 +112,21 @@ const streamToBuffer = (stream: any) => { stream.on("end", () => resolve(Buffer.concat(chunks))); }); }; + +const fileNotFoundTemplate = ` + + + + + File not found + + +

File not found

+

It is possible that the file you're looking for either doesn't exist or hasn't been created yet.

+

Some possible reasons are:

+
    +
  • You are trying to access a file too early, before it has been fully archived.
  • +
  • The file doesn't exist either because it encountered an error while being archived, or it simply doesn't exist.
  • +
+ + `; diff --git a/lib/client/avatarExists.ts b/lib/client/avatarExists.ts index 7490f3b..2e82fa6 100644 --- a/lib/client/avatarExists.ts +++ b/lib/client/avatarExists.ts @@ -1,4 +1,4 @@ export default async function avatarExists(fileUrl: string): Promise { const response = await fetch(fileUrl, { method: "HEAD" }); - return !(response.headers.get("content-type") === "text/plain"); + return !(response.headers.get("content-type") === "text/html"); } diff --git a/pages/api/archives/[...params].ts b/pages/api/archives/[...params].ts index 757f635..3e659a6 100644 --- a/pages/api/archives/[...params].ts +++ b/pages/api/archives/[...params].ts @@ -31,10 +31,10 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { .status(401) .json({ response: "You don't have access to this collection." }); - const { file, contentType } = await readFile({ - filePath: `archives/${collectionId}/${linkId}`, - }); - res.setHeader("Content-Type", contentType).status(200); + const { file, contentType, status } = await readFile( + `archives/${collectionId}/${linkId}` + ); + res.setHeader("Content-Type", contentType).status(status as number); return res.send(file); } diff --git a/pages/api/avatar/[id].ts b/pages/api/avatar/[id].ts index ac4f1d3..a006dbf 100644 --- a/pages/api/avatar/[id].ts +++ b/pages/api/avatar/[id].ts @@ -13,7 +13,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { if (!userId || !username) return res - .setHeader("Content-Type", "text/plain") + .setHeader("Content-Type", "text/html") .status(401) .send("You must be logged in."); else if (session?.user?.isSubscriber === false) @@ -24,7 +24,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { if (!queryId) return res - .setHeader("Content-Type", "text/plain") + .setHeader("Content-Type", "text/html") .status(401) .send("Invalid parameters."); @@ -34,27 +34,27 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { id: queryId, }, include: { - whitelistedUsers: true - } + whitelistedUsers: true, + }, }); - const whitelistedUsernames = targetUser?.whitelistedUsers.map(whitelistedUsername => whitelistedUsername.username); + const whitelistedUsernames = targetUser?.whitelistedUsers.map( + (whitelistedUsername) => whitelistedUsername.username + ); - if ( - targetUser?.isPrivate && - !whitelistedUsernames?.includes(username) - ) { + if (targetUser?.isPrivate && !whitelistedUsernames?.includes(username)) { return res - .setHeader("Content-Type", "text/plain") + .setHeader("Content-Type", "text/html") .send("This profile is private."); } } - const { file, contentType } = await readFile({ - filePath: `uploads/avatar/${queryId}.jpg`, - }); + const { file, contentType, status } = await readFile( + `uploads/avatar/${queryId}.jpg` + ); - res.setHeader("Content-Type", contentType); - - return res.send(file); + return res + .setHeader("Content-Type", contentType) + .status(status as number) + .send(file); } diff --git a/pages/api/data/index.ts b/pages/api/data/index.ts new file mode 100644 index 0000000..c01245f --- /dev/null +++ b/pages/api/data/index.ts @@ -0,0 +1,31 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import getData from "@/lib/api/controllers/data/getData"; +import postData from "@/lib/api/controllers/data/postData"; + +export default async function users(req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession(req, res, authOptions); + + if (!session?.user.id) { + return res.status(401).json({ response: "You must be logged in." }); + } else if (session?.user?.isSubscriber === false) + res.status(401).json({ + response: + "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", + }); + + if (req.method === "GET") { + const data = await getData(session.user.id); + if (data.status === 200) + return res + .setHeader("Content-Type", "application/json") + .setHeader("Content-Disposition", "attachment; filename=backup.json") + .status(data.status) + .json(data.response); + } else if (req.method === "POST") { + console.log(JSON.parse(req.body)); + const data = await postData(session.user.id, JSON.parse(req.body)); + return res.status(data.status).json({ response: data.response }); + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c1c04d8..eca457b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -93,31 +93,32 @@ model Collection { } model UsersAndCollections { - user User @relation(fields: [userId], references: [id]) - userId Int + user User @relation(fields: [userId], references: [id]) + userId Int collection Collection @relation(fields: [collectionId], references: [id]) collectionId Int - canCreate Boolean - canUpdate Boolean - canDelete Boolean + canCreate Boolean + canUpdate Boolean + canDelete Boolean @@id([userId, collectionId]) } model Link { - id Int @id @default(autoincrement()) - name String - url String - description String @default("") + id Int @id @default(autoincrement()) + name String + url String + description String @default("") - pinnedBy User[] + pinnedBy User[] - collection Collection @relation(fields: [collectionId], references: [id]) - collectionId Int - tags Tag[] - createdAt DateTime @default(now()) + collection Collection @relation(fields: [collectionId], references: [id]) + collectionId Int + tags Tag[] + + createdAt DateTime @default(now()) } model Tag { diff --git a/types/enviornment.d.ts b/types/enviornment.d.ts index 047994c..1b2bb51 100644 --- a/types/enviornment.d.ts +++ b/types/enviornment.d.ts @@ -6,6 +6,7 @@ declare global { NEXTAUTH_URL: string; PAGINATION_TAKE_COUNT?: string; STORAGE_FOLDER?: string; + AUTOSCROLL_TIMEOUT?: string; SPACES_KEY?: string; SPACES_SECRET?: string; diff --git a/types/global.ts b/types/global.ts index 04b68e0..c4adcb5 100644 --- a/types/global.ts +++ b/types/global.ts @@ -36,7 +36,7 @@ export interface CollectionIncludingMembersAndLinkCount export interface AccountSettings extends User { profilePic: string; newPassword?: string; - whitelistedUsers: string[] + whitelistedUsers: string[]; } interface LinksIncludingTags extends Link { @@ -77,3 +77,11 @@ export type PublicLinkRequestQuery = { cursor?: number; collectionId: number; }; + +interface CollectionIncludingLinks extends Collection { + links: LinksIncludingTags[]; +} + +export interface Backup extends Omit { + collections: CollectionIncludingLinks[]; +} From 543dfd156c0462800723983e0f471fc19043ea57 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 10 Aug 2023 13:36:23 -0400 Subject: [PATCH 20/20] minor fix in the docker compose file --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 7116bf6..81457f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: - ./pgdata:/var/lib/postgresql/data linkwarden: env_file: .env + platform: linux/x86_64 environment: - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres restart: always