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..1601e20 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -1,10 +1,11 @@ -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)); console.log(query); + const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); // Sorting logic let order: any; if (query.sort === Sort.DateNewestFirst) @@ -37,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: { @@ -58,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: { @@ -66,7 +67,7 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.name ? query.searchQuery : undefined, - mode: "insensitive", + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined }, }, { @@ -75,7 +76,7 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.url ? query.searchQuery : undefined, - mode: "insensitive", + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined }, }, { @@ -84,7 +85,7 @@ export default async function getLink(userId: number, body: string) { query.searchQuery && query.searchFilter?.description ? query.searchQuery : undefined, - mode: "insensitive", + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined }, }, { @@ -92,44 +93,44 @@ 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: "insensitive", - } - : undefined, - OR: [ - { ownerId: userId }, // Tags owned by the user - { - links: { - some: { - name: { - contains: - query.searchQuery && - query.searchFilter?.tags - ? query.searchQuery - : undefined, - mode: "insensitive", - }, - collection: { - members: { - some: { - userId, // Tags from collections where the user is a member - }, - }, + 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, + }, }, ], }, @@ -137,8 +138,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 || { @@ -146,5 +147,5 @@ export default async function getLink(userId: number, body: string) { }, }); - return { response: links, status: 200 }; + return {response: links, status: 200}; } 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..c1c04d8 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 {