Merge remote-tracking branch 'upstream/dev' into tags-in-public-collection

This commit is contained in:
Oliver Schwamb
2024-10-30 12:10:30 +01:00
266 changed files with 18575 additions and 6564 deletions
@@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
import getPermission from "@/lib/api/getPermission";
import { Collection, UsersAndCollections } from "@prisma/client";
import { UsersAndCollections } from "@prisma/client";
import removeFolder from "@/lib/api/storage/removeFolder";
export default async function deleteCollection(
@@ -57,7 +57,8 @@ export default async function deleteCollection(
},
});
await removeFolder({ filePath: `archives/${collectionId}` });
removeFolder({ filePath: `archives/${collectionId}` });
removeFolder({ filePath: `archives/preview/${collectionId}` });
await removeFromOrders(userId, collectionId);
@@ -99,7 +100,8 @@ async function deleteSubCollections(collectionId: number) {
where: { id: subCollection.id },
});
await removeFolder({ filePath: `archives/${subCollection.id}` });
removeFolder({ filePath: `archives/${subCollection.id}` });
removeFolder({ filePath: `archives/preview/${subCollection.id}` });
}
}
@@ -1,15 +1,31 @@
import { prisma } from "@/lib/api/db";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import getPermission from "@/lib/api/getPermission";
import {
UpdateCollectionSchema,
UpdateCollectionSchemaType,
} from "@/lib/shared/schemaValidation";
export default async function updateCollection(
userId: number,
collectionId: number,
data: CollectionIncludingMembersAndLinkCount
body: UpdateCollectionSchemaType
) {
if (!collectionId)
return { response: "Please choose a valid collection.", status: 401 };
const dataValidation = UpdateCollectionSchema.safeParse(body);
if (!dataValidation.success) {
return {
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
const data = dataValidation.data;
const collectionIsAccessible = await getPermission({
userId,
collectionId,
@@ -18,8 +34,6 @@ export default async function updateCollection(
if (!(collectionIsAccessible?.ownerId === userId))
return { response: "Collection is not accessible.", status: 401 };
console.log(data);
if (data.parentId) {
if (data.parentId !== ("root" as any)) {
const findParentCollection = await prisma.collection.findUnique({
@@ -61,6 +75,8 @@ export default async function updateCollection(
name: data.name.trim(),
description: data.description,
color: data.color,
icon: data.icon,
iconWeight: data.iconWeight,
isPublic: data.isPublic,
tagsArePublic: data.tagsArePublic,
parent:
@@ -77,7 +93,7 @@ export default async function updateCollection(
: undefined,
members: {
create: data.members.map((e) => ({
user: { connect: { id: e.user.id || e.userId } },
user: { connect: { id: e.userId } },
canCreate: e.canCreate,
canUpdate: e.canUpdate,
canDelete: e.canDelete,
@@ -1,16 +1,26 @@
import { prisma } from "@/lib/api/db";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import createFolder from "@/lib/api/storage/createFolder";
import {
PostCollectionSchema,
PostCollectionSchemaType,
} from "@/lib/shared/schemaValidation";
export default async function postCollection(
collection: CollectionIncludingMembersAndLinkCount,
body: PostCollectionSchemaType,
userId: number
) {
if (!collection || collection.name.trim() === "")
const dataValidation = PostCollectionSchema.safeParse(body);
if (!dataValidation.success) {
return {
response: "Please enter a valid collection.",
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
const collection = dataValidation.data;
if (collection.parentId) {
const findParentCollection = await prisma.collection.findUnique({
@@ -42,6 +52,8 @@ export default async function postCollection(
name: collection.name.trim(),
description: collection.description,
color: collection.color,
icon: collection.icon,
iconWeight: collection.iconWeight,
parent: collection.parentId
? {
connect: {
@@ -1,11 +1,11 @@
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global";
import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getDashboardData(
userId: number,
query: LinkRequestQuery
) {
let order: any;
let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
@@ -14,7 +14,7 @@ export default async function getDashboardData(
else if (query.sort === Sort.DescriptionZA) order = { description: "desc" };
const pinnedLinks = await prisma.link.findMany({
take: 8,
take: 10,
where: {
AND: [
{
@@ -42,11 +42,11 @@ export default async function getDashboardData(
select: { id: true },
},
},
orderBy: order || { id: "desc" },
orderBy: order,
});
const recentlyAddedLinks = await prisma.link.findMany({
take: 8,
take: 10,
where: {
collection: {
OR: [
@@ -67,10 +67,18 @@ export default async function getDashboardData(
select: { id: true },
},
},
orderBy: order || { id: "desc" },
orderBy: order,
});
const links = [...recentlyAddedLinks, ...pinnedLinks].sort(
const combinedLinks = [...recentlyAddedLinks, ...pinnedLinks];
const uniqueLinks = Array.from(
combinedLinks
.reduce((map, item) => map.set(item.id, item), new Map())
.values()
);
const links = uniqueLinks.sort(
(a, b) => (new Date(b.id) as any) - (new Date(a.id) as any)
);
@@ -0,0 +1,124 @@
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Order, Sort } from "@/types/global";
type Response<D> =
| {
data: D;
message: string;
status: number;
}
| {
data: D;
message: string;
status: number;
};
export default async function getDashboardData(
userId: number,
query: LinkRequestQuery
): Promise<Response<any>> {
let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "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 = { description: "asc" };
else if (query.sort === Sort.DescriptionZA) order = { description: "desc" };
const numberOfPinnedLinks = await prisma.link.count({
where: {
AND: [
{
collection: {
OR: [
{ ownerId: userId },
{
members: {
some: { userId },
},
},
],
},
},
{
pinnedBy: { some: { id: userId } },
},
],
},
});
const pinnedLinks = await prisma.link.findMany({
take: 16,
where: {
AND: [
{
collection: {
OR: [
{ ownerId: userId },
{
members: {
some: { userId },
},
},
],
},
},
{
pinnedBy: { some: { id: userId } },
},
],
},
include: {
tags: true,
collection: true,
pinnedBy: {
where: { id: userId },
select: { id: true },
},
},
orderBy: order || { id: "desc" },
});
const recentlyAddedLinks = await prisma.link.findMany({
take: 16,
where: {
collection: {
OR: [
{ ownerId: userId },
{
members: {
some: { userId },
},
},
],
},
},
include: {
tags: true,
collection: true,
pinnedBy: {
where: { id: userId },
select: { id: true },
},
},
orderBy: order || { id: "desc" },
});
const links = [...recentlyAddedLinks, ...pinnedLinks].sort(
(a, b) => new Date(b.id).getTime() - new Date(a.id).getTime()
);
// Make sure links are unique
const uniqueLinks = links.filter(
(link, index, self) => index === self.findIndex((t) => t.id === link.id)
);
return {
data: {
links: uniqueLinks,
numberOfPinnedLinks,
},
message: "Dashboard data fetched successfully.",
status: 200,
};
}
@@ -1,7 +1,7 @@
import { prisma } from "@/lib/api/db";
import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import removeFile from "@/lib/api/storage/removeFile";
import { removeFiles } from "@/lib/api/manageLinkFiles";
export default async function deleteLinksById(
userId: number,
@@ -43,15 +43,7 @@ export default async function deleteLinksById(
const linkId = linkIds[i];
const collectionIsAccessible = collectionIsAccessibleArray[i];
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.pdf`,
});
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.png`,
});
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
});
if (collectionIsAccessible) removeFiles(linkId, collectionIsAccessible.id);
}
return { response: deletedLinks, status: 200 };
@@ -1,9 +1,10 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import updateLinkById from "../linkId/updateLinkById";
import { UpdateLinkSchemaType } from "@/lib/shared/schemaValidation";
export default async function updateLinks(
userId: number,
links: LinkIncludingShortenedCollectionAndTags[],
links: UpdateLinkSchemaType[],
removePreviousTags: boolean,
newData: Pick<
LinkIncludingShortenedCollectionAndTags,
@@ -22,7 +23,7 @@ export default async function updateLinks(
updatedTags = [...(newData.tags ?? [])];
}
const updatedData: LinkIncludingShortenedCollectionAndTags = {
const updatedData: UpdateLinkSchemaType = {
...link,
tags: updatedTags,
collection: {
+6 -5
View File
@@ -1,10 +1,11 @@
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global";
import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getLink(userId: number, query: LinkRequestQuery) {
const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql");
const POSTGRES_IS_ENABLED =
process.env.DATABASE_URL?.startsWith("postgresql");
let order: any;
let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
@@ -102,7 +103,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) {
}
const links = await prisma.link.findMany({
take: Number(process.env.PAGINATION_TAKE_COUNT) || 20,
take: Number(process.env.PAGINATION_TAKE_COUNT) || 50,
skip: query.cursor ? 1 : undefined,
cursor: query.cursor ? { id: query.cursor } : undefined,
where: {
@@ -145,7 +146,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) {
select: { id: true },
},
},
orderBy: order || { id: "desc" },
orderBy: order,
});
return { response: links, status: 200 };
@@ -1,7 +1,7 @@
import { prisma } from "@/lib/api/db";
import { Link, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import removeFile from "@/lib/api/storage/removeFile";
import { removeFiles } from "@/lib/api/manageLinkFiles";
export default async function deleteLink(userId: number, linkId: number) {
if (!linkId) return { response: "Please choose a valid link.", status: 401 };
@@ -12,7 +12,10 @@ export default async function deleteLink(userId: number, linkId: number) {
(e: UsersAndCollections) => e.userId === userId && e.canDelete
);
if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess))
if (
!collectionIsAccessible ||
!(collectionIsAccessible?.ownerId === userId || memberHasAccess)
)
return { response: "Collection is not accessible.", status: 401 };
const deleteLink: Link = await prisma.link.delete({
@@ -21,15 +24,7 @@ export default async function deleteLink(userId: number, linkId: number) {
},
});
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.pdf`,
});
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.png`,
});
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
});
removeFiles(linkId, collectionIsAccessible.id);
return { response: deleteLink, status: 200 };
}
@@ -1,19 +1,30 @@
import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import moveFile from "@/lib/api/storage/moveFile";
import { moveFiles, removeFiles } from "@/lib/api/manageLinkFiles";
import isValidUrl from "@/lib/shared/isValidUrl";
import {
UpdateLinkSchema,
UpdateLinkSchemaType,
} from "@/lib/shared/schemaValidation";
export default async function updateLinkById(
userId: number,
linkId: number,
data: LinkIncludingShortenedCollectionAndTags
body: UpdateLinkSchemaType
) {
if (!data || !data.collection.id)
const dataValidation = UpdateLinkSchema.safeParse(body);
if (!dataValidation.success) {
return {
response: "Please choose a valid link and collection.",
status: 401,
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
const data = dataValidation.data;
const collectionIsAccessible = await getPermission({ userId, linkId });
@@ -25,17 +36,18 @@ export default async function updateLinkById(
(e: UsersAndCollections) => e.userId === userId
);
// If the user is able to create a link, they can pin it to their dashboard only.
if (canPinPermission) {
// If the user is part of a collection, they can pin it to their dashboard
if (canPinPermission && data.pinnedBy && data.pinnedBy[0]) {
const updatedLink = await prisma.link.update({
where: {
id: linkId,
},
data: {
pinnedBy:
data?.pinnedBy && data.pinnedBy[0]
pinnedBy: data?.pinnedBy
? data.pinnedBy[0]?.id === userId
? { connect: { id: userId } }
: { disconnect: { id: userId } },
: { disconnect: { id: userId } }
: undefined,
},
include: {
collection: true,
@@ -60,23 +72,13 @@ export default async function updateLinkById(
(e: UsersAndCollections) => e.userId === userId && e.canUpdate
);
const targetCollectionsAccessible =
targetCollectionIsAccessible?.ownerId === userId;
const targetCollectionMatchesData = data.collection.id
? data.collection.id === targetCollectionIsAccessible?.id
: true && data.collection.name
? data.collection.name === targetCollectionIsAccessible?.name
: true && data.collection.ownerId
? data.collection.ownerId === targetCollectionIsAccessible?.ownerId
: true;
: true && data.collection.ownerId
? data.collection.ownerId === targetCollectionIsAccessible?.ownerId
: true;
if (!targetCollectionsAccessible)
return {
response: "Target collection is not accessible.",
status: 401,
};
else if (!targetCollectionMatchesData)
if (!targetCollectionMatchesData)
return {
response: "Target collection does not match the data.",
status: 401,
@@ -97,13 +99,41 @@ export default async function updateLinkById(
status: 401,
};
else {
const oldLink = await prisma.link.findUnique({
where: {
id: linkId,
},
});
if (
data.url &&
oldLink &&
oldLink?.url !== data.url &&
isValidUrl(data.url)
) {
await removeFiles(oldLink.id, oldLink.collectionId);
} else if (oldLink?.url !== data.url)
return {
response: "Invalid URL.",
status: 401,
};
const updatedLink = await prisma.link.update({
where: {
id: linkId,
},
data: {
name: data.name,
url: data.url,
description: data.description,
icon: data.icon,
iconWeight: data.iconWeight,
color: data.color,
image: oldLink?.url !== data.url ? null : undefined,
pdf: oldLink?.url !== data.url ? null : undefined,
readable: oldLink?.url !== data.url ? null : undefined,
monolith: oldLink?.url !== data.url ? null : undefined,
preview: oldLink?.url !== data.url ? null : undefined,
collection: {
connect: {
id: data.collection.id,
@@ -128,10 +158,11 @@ export default async function updateLinkById(
},
})),
},
pinnedBy:
data?.pinnedBy && data.pinnedBy[0]
pinnedBy: data?.pinnedBy
? data.pinnedBy[0]?.id === userId
? { connect: { id: userId } }
: { disconnect: { id: userId } },
: { disconnect: { id: userId } }
: undefined,
},
include: {
tags: true,
@@ -146,20 +177,7 @@ export default async function updateLinkById(
});
if (collectionIsAccessible?.id !== data.collection.id) {
await moveFile(
`archives/${collectionIsAccessible?.id}/${linkId}.pdf`,
`archives/${data.collection.id}/${linkId}.pdf`
);
await moveFile(
`archives/${collectionIsAccessible?.id}/${linkId}.png`,
`archives/${data.collection.id}/${linkId}.png`
);
await moveFile(
`archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
`archives/${data.collection.id}/${linkId}_readability.json`
);
await moveFiles(linkId, collectionIsAccessible?.id, data.collection.id);
}
return { response: updatedLink, status: 200 };
+39 -117
View File
@@ -1,114 +1,35 @@
import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import getTitle from "@/lib/shared/getTitle";
import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import fetchTitleAndHeaders from "@/lib/shared/fetchTitleAndHeaders";
import createFolder from "@/lib/api/storage/createFolder";
import validateUrlSize from "../../validateUrlSize";
import setLinkCollection from "../../setLinkCollection";
import {
PostLinkSchema,
PostLinkSchemaType,
} from "@/lib/shared/schemaValidation";
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
export default async function postLink(
link: LinkIncludingShortenedCollectionAndTags,
body: PostLinkSchemaType,
userId: number
) {
try {
new URL(link.url || "");
} catch (error) {
const dataValidation = PostLinkSchema.safeParse(body);
if (!dataValidation.success) {
return {
response:
"Please enter a valid Address for the Link. (It should start with http/https)",
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
if (!link.collection.id && link.collection.name) {
link.collection.name = link.collection.name.trim();
const link = dataValidation.data;
// find the collection with the name and the user's id
const findCollection = await prisma.collection.findFirst({
where: {
name: link.collection.name,
ownerId: userId,
parentId: link.collection.parentId,
},
});
const linkCollection = await setLinkCollection(link, userId);
if (findCollection) {
const collectionIsAccessible = await getPermission({
userId,
collectionId: findCollection.id,
});
const memberHasAccess = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === userId && e.canCreate
);
if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess))
return { response: "Collection is not accessible.", status: 401 };
link.collection.id = findCollection.id;
link.collection.ownerId = findCollection.ownerId;
} else {
const collection = await prisma.collection.create({
data: {
name: link.collection.name,
ownerId: userId,
},
});
link.collection.id = collection.id;
await prisma.user.update({
where: {
id: userId,
},
data: {
collectionOrder: {
push: link.collection.id,
},
},
});
}
} else if (link.collection.id) {
const collectionIsAccessible = await getPermission({
userId,
collectionId: link.collection.id,
});
const memberHasAccess = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === userId && e.canCreate
);
if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess))
return { response: "Collection is not accessible.", status: 401 };
} else if (!link.collection.id) {
link.collection.name = "Unorganized";
link.collection.parentId = null;
// find the collection with the name "Unorganized" and the user's id
const unorganizedCollection = await prisma.collection.findFirst({
where: {
name: "Unorganized",
ownerId: userId,
},
});
link.collection.id = unorganizedCollection?.id;
await prisma.user.update({
where: {
id: userId,
},
data: {
collectionOrder: {
push: link.collection.id,
},
},
});
} else {
return { response: "Uncaught error.", status: 500 };
}
if (!linkCollection)
return { response: "Collection is not accessible.", status: 400 };
const user = await prisma.user.findUnique({
where: {
@@ -117,9 +38,14 @@ export default async function postLink(
});
if (user?.preventDuplicateLinks) {
const url = link.url?.trim().replace(/\/+$/, ""); // trim and remove trailing slashes from the URL
const hasWwwPrefix = url?.includes(`://www.`);
const urlWithoutWww = hasWwwPrefix ? url?.replace(`://www.`, "://") : url;
const urlWithWww = hasWwwPrefix ? url : url?.replace("://", `://www.`);
const existingLink = await prisma.link.findFirst({
where: {
url: link.url?.trim(),
OR: [{ url: urlWithWww }, { url: urlWithoutWww }],
collection: {
ownerId: userId,
},
@@ -136,29 +62,23 @@ export default async function postLink(
const numberOfLinksTheUserHas = await prisma.link.count({
where: {
collection: {
ownerId: userId,
ownerId: linkCollection.ownerId,
},
},
});
if (numberOfLinksTheUserHas + 1 > MAX_LINKS_PER_USER)
if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
link.collection.name = link.collection.name.trim();
const { title, headers } = await fetchTitleAndHeaders(link.url || "");
const description =
link.description && link.description !== ""
? link.description
: link.url
? await getTitle(link.url)
: undefined;
const name =
link.name && link.name !== "" ? link.name : link.url ? title : "";
const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
const contentType = validatedUrl?.get("content-type");
const contentType = headers?.get("content-type");
let linkType = "url";
let imageExtension = "png";
@@ -170,30 +90,32 @@ export default async function postLink(
else if (contentType === "image/png") imageExtension = "png";
}
if (!link.tags) link.tags = [];
const newLink = await prisma.link.create({
data: {
url: link.url?.trim(),
name: link.name,
description,
url: link.url?.trim() || null,
name,
description: link.description,
type: linkType,
collection: {
connect: {
id: link.collection.id,
id: linkCollection.id,
},
},
tags: {
connectOrCreate: link.tags.map((tag) => ({
connectOrCreate: link.tags?.map((tag) => ({
where: {
name_ownerId: {
name: tag.name.trim(),
ownerId: link.collection.ownerId,
ownerId: linkCollection.ownerId,
},
},
create: {
name: tag.name.trim(),
owner: {
connect: {
id: link.collection.ownerId,
id: linkCollection.ownerId,
},
},
},
@@ -22,18 +22,5 @@ export default async function exportData(userId: number) {
const { password, id, ...userData } = user;
function redactIds(obj: any) {
if (Array.isArray(obj)) {
obj.forEach((o) => redactIds(o));
} else if (obj !== null && typeof obj === "object") {
delete obj.id;
for (let key in obj) {
redactIds(obj[key]);
}
}
}
redactIds(userData);
return { response: userData, status: 200 };
}
@@ -2,7 +2,6 @@ import { prisma } from "@/lib/api/db";
import createFolder from "@/lib/api/storage/createFolder";
import { JSDOM } from "jsdom";
import { parse, Node, Element, TextNode } from "himalaya";
import { writeFileSync } from "fs";
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
@@ -31,7 +30,7 @@ export default async function importFromHTMLFile(
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
@@ -63,11 +62,22 @@ async function processBookmarks(
) as Element;
if (collectionName) {
collectionId = await createCollection(
userId,
(collectionName.children[0] as TextNode).content,
parentCollectionId
);
const collectionNameContent = (collectionName.children[0] as TextNode)
?.content;
if (collectionNameContent) {
collectionId = await createCollection(
userId,
collectionNameContent,
parentCollectionId
);
} else {
// Handle the case when the collection name is empty
collectionId = await createCollection(
userId,
"Untitled Collection",
parentCollectionId
);
}
}
await processBookmarks(
userId,
@@ -144,6 +154,8 @@ const createCollection = async (
collectionName: string,
parentId?: number
) => {
collectionName = collectionName.trim().slice(0, 254);
const findCollection = await prisma.collection.findFirst({
where: {
parentId,
@@ -188,6 +200,22 @@ const createLink = async (
tags?: string[],
importDate?: Date
) => {
url = url.trim().slice(0, 254);
try {
new URL(url);
} catch (e) {
return;
}
tags = tags?.map((tag) => tag.trim().slice(0, 49));
name = name?.trim().slice(0, 254);
description = description?.trim().slice(0, 254);
if (importDate) {
const dateString = importDate.toISOString();
if (dateString.length > 50) {
importDate = undefined;
}
}
await prisma.link.create({
data: {
name: name || "",
@@ -26,7 +26,7 @@ export default async function importFromLinkwarden(
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
@@ -44,9 +44,9 @@ export default async function importFromLinkwarden(
id: userId,
},
},
name: e.name,
description: e.description,
color: e.color,
name: e.name?.trim().slice(0, 254),
description: e.description?.trim().slice(0, 254),
color: e.color?.trim().slice(0, 50),
},
});
@@ -54,11 +54,19 @@ export default async function importFromLinkwarden(
// Import Links
for (const link of e.links) {
const newLink = await prisma.link.create({
if (link.url) {
try {
new URL(link.url.trim());
} catch (err) {
continue;
}
}
await prisma.link.create({
data: {
url: link.url,
name: link.name,
description: link.description,
url: link.url?.trim().slice(0, 254),
name: link.name?.trim().slice(0, 254),
description: link.description?.trim().slice(0, 254),
collection: {
connect: {
id: newCollection.id,
@@ -69,12 +77,12 @@ export default async function importFromLinkwarden(
connectOrCreate: link.tags.map((tag) => ({
where: {
name_ownerId: {
name: tag.name.trim(),
name: tag.name?.slice(0, 49),
ownerId: userId,
},
},
create: {
name: tag.name.trim(),
name: tag.name?.trim().slice(0, 49),
owner: {
connect: {
id: userId,
@@ -0,0 +1,122 @@
import { prisma } from "@/lib/api/db";
import createFolder from "@/lib/api/storage/createFolder";
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
type WallabagBackup = {
is_archived: number;
is_starred: number;
tags: String[];
is_public: boolean;
id: number;
title: string;
url: string;
content: string;
created_at: Date;
updated_at: Date;
published_by: string[];
starred_at: Date;
annotations: any[];
mimetype: string;
language: string;
reading_time: number;
domain_name: string;
preview_picture: string;
http_status: string;
headers: Record<string, string>;
}[];
export default async function importFromWallabag(
userId: number,
rawData: string
) {
const data: WallabagBackup = JSON.parse(rawData);
const backup = data.filter((e) => e.url);
let totalImports = backup.length;
const numberOfLinksTheUserHas = await prisma.link.count({
where: {
collection: {
ownerId: userId,
},
},
});
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
await prisma
.$transaction(
async () => {
const newCollection = await prisma.collection.create({
data: {
owner: {
connect: {
id: userId,
},
},
name: "Imports",
},
});
createFolder({ filePath: `archives/${newCollection.id}` });
for (const link of backup) {
if (link.url) {
try {
new URL(link.url.trim());
} catch (err) {
continue;
}
}
await prisma.link.create({
data: {
pinnedBy: link.is_starred
? { connect: { id: userId } }
: undefined,
url: link.url?.trim().slice(0, 254),
name: link.title?.trim().slice(0, 254) || "",
textContent: link.content?.trim() || "",
importDate: link.created_at || null,
collection: {
connect: {
id: newCollection.id,
},
},
tags:
link.tags && link.tags[0]
? {
connectOrCreate: link.tags.map((tag) => ({
where: {
name_ownerId: {
name: tag?.trim().slice(0, 49),
ownerId: userId,
},
},
create: {
name: tag?.trim().slice(0, 49),
owner: {
connect: {
id: userId,
},
},
},
})),
}
: undefined,
},
});
}
},
{ timeout: 30000 }
)
.catch((err) => console.log(err));
return { response: "Success.", status: 200 };
}
@@ -1,12 +1,13 @@
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global";
import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getLink(
query: Omit<LinkRequestQuery, "tagId" | "pinnedOnly">, takeAll = false
) {
const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql");
const POSTGRES_IS_ENABLED =
process.env.DATABASE_URL?.startsWith("postgresql");
let order: any;
let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
@@ -68,7 +69,7 @@ export default async function getLink(
}
const links = await prisma.link.findMany({
take: !takeAll ? Number(process.env.PAGINATION_TAKE_COUNT) || 20 : undefined,
take: Number(process.env.PAGINATION_TAKE_COUNT) || 50,
skip: query.cursor ? 1 : undefined,
cursor: query.cursor ? { id: query.cursor } : undefined,
where: {
@@ -75,6 +75,7 @@ export default async function getPublicUser(
username: lessSensitiveInfo.username,
image: lessSensitiveInfo.image,
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
archiveAsMonolith: lessSensitiveInfo.archiveAsMonolith,
archiveAsPDF: lessSensitiveInfo.archiveAsPDF,
};
@@ -0,0 +1,48 @@
import { prisma } from "@/lib/api/db";
import crypto from "crypto";
import { decode, encode } from "next-auth/jwt";
export default async function createSession(
userId: number,
sessionName?: string
) {
const now = Date.now();
const expiryDate = new Date();
const oneDayInSeconds = 86400;
expiryDate.setDate(expiryDate.getDate() + 73000); // 200 years (not really never)
const expiryDateSecond = 73050 * oneDayInSeconds;
const token = await encode({
token: {
id: userId,
iat: now / 1000,
exp: (expiryDate as any) / 1000,
jti: crypto.randomUUID(),
},
maxAge: expiryDateSecond || 604800,
secret: process.env.NEXTAUTH_SECRET as string,
});
const tokenBody = await decode({
token,
secret: process.env.NEXTAUTH_SECRET as string,
});
await prisma.accessToken.create({
data: {
name: sessionName || "Unknown Device",
userId,
token: tokenBody?.jti as string,
isSession: true,
expires: expiryDate,
},
});
return {
response: {
token,
},
status: 200,
};
}
@@ -1,18 +1,31 @@
import { prisma } from "@/lib/api/db";
import { Tag } from "@prisma/client";
import {
UpdateTagSchema,
UpdateTagSchemaType,
} from "@/lib/shared/schemaValidation";
export default async function updeteTagById(
userId: number,
tagId: number,
data: Tag
body: UpdateTagSchemaType
) {
if (!tagId || !data.name)
return { response: "Please choose a valid name for the tag.", status: 401 };
const dataValidation = UpdateTagSchema.safeParse(body);
if (!dataValidation.success) {
return {
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
const { name } = dataValidation.data;
const tagNameIsTaken = await prisma.tag.findFirst({
where: {
ownerId: userId,
name: data.name,
name: name,
},
});
@@ -39,7 +52,7 @@ export default async function updeteTagById(
id: tagId,
},
data: {
name: data.name,
name: name,
},
});
+1
View File
@@ -9,6 +9,7 @@ export default async function getToken(userId: number) {
select: {
id: true,
name: true,
isSession: true,
expires: true,
createdAt: true,
},
+21 -17
View File
@@ -1,28 +1,32 @@
import { prisma } from "@/lib/api/db";
import {
PostTokenSchemaType,
PostTokenSchema,
} from "@/lib/shared/schemaValidation";
import { TokenExpiry } from "@/types/global";
import crypto from "crypto";
import { decode, encode } from "next-auth/jwt";
export default async function postToken(
body: {
name: string;
expires: TokenExpiry;
},
body: PostTokenSchemaType,
userId: number
) {
console.log(body);
const dataValidation = PostTokenSchema.safeParse(body);
const checkHasEmptyFields = !body.name || body.expires === undefined;
if (checkHasEmptyFields)
if (!dataValidation.success) {
return {
response: "Please fill out all the fields.",
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
const { name, expires } = dataValidation.data;
const checkIfTokenExists = await prisma.accessToken.findFirst({
where: {
name: body.name,
name: name,
revoked: false,
userId,
},
@@ -40,16 +44,16 @@ export default async function postToken(
const oneDayInSeconds = 86400;
let expiryDateSecond = 7 * oneDayInSeconds;
if (body.expires === TokenExpiry.oneMonth) {
if (expires === TokenExpiry.oneMonth) {
expiryDate.setDate(expiryDate.getDate() + 30);
expiryDateSecond = 30 * oneDayInSeconds;
} else if (body.expires === TokenExpiry.twoMonths) {
} else if (expires === TokenExpiry.twoMonths) {
expiryDate.setDate(expiryDate.getDate() + 60);
expiryDateSecond = 60 * oneDayInSeconds;
} else if (body.expires === TokenExpiry.threeMonths) {
} else if (expires === TokenExpiry.threeMonths) {
expiryDate.setDate(expiryDate.getDate() + 90);
expiryDateSecond = 90 * oneDayInSeconds;
} else if (body.expires === TokenExpiry.never) {
} else if (expires === TokenExpiry.never) {
expiryDate.setDate(expiryDate.getDate() + 73000); // 200 years (not really never)
expiryDateSecond = 73050 * oneDayInSeconds;
} else {
@@ -65,17 +69,17 @@ export default async function postToken(
jti: crypto.randomUUID(),
},
maxAge: expiryDateSecond || 604800,
secret: process.env.NEXTAUTH_SECRET,
secret: process.env.NEXTAUTH_SECRET as string,
});
const tokenBody = await decode({
token,
secret: process.env.NEXTAUTH_SECRET,
secret: process.env.NEXTAUTH_SECRET as string,
});
const createToken = await prisma.accessToken.create({
data: {
name: body.name,
name: name,
userId,
token: tokenBody?.jti as string,
expires: expiryDate,
+21
View File
@@ -0,0 +1,21 @@
import { prisma } from "@/lib/api/db";
export default async function getUsers() {
// Get all users
const users = await prisma.user.findMany({
select: {
id: true,
username: true,
email: true,
emailVerified: true,
subscriptions: {
select: {
active: true,
},
},
createdAt: true,
},
});
return { response: users, status: 200 };
}
+93 -60
View File
@@ -1,91 +1,124 @@
import { prisma } from "@/lib/api/db";
import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt";
import isServerAdmin from "../../isServerAdmin";
import { PostUserSchema } from "@/lib/shared/schemaValidation";
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
const stripeEnabled = process.env.STRIPE_SECRET_KEY ? true : false;
interface Data {
response: string | object;
}
interface User {
name: string;
username?: string;
email?: string;
password: string;
status: number;
}
export default async function postUser(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
if (process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true") {
return res.status(400).json({ response: "Registration is disabled." });
res: NextApiResponse
): Promise<Data> {
let isAdmin = await isServerAdmin({ req });
if (process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" && !isAdmin) {
return { response: "Registration is disabled.", status: 400 };
}
const body: User = req.body;
const dataValidation = PostUserSchema().safeParse(req.body);
const checkHasEmptyFields = emailEnabled
? !body.password || !body.name || !body.email
: !body.username || !body.password || !body.name;
if (!dataValidation.success) {
return {
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
if (!body.password || body.password.length < 8)
return res
.status(400)
.json({ response: "Password must be at least 8 characters." });
const { name, email, password } = dataValidation.data;
let { username } = dataValidation.data;
if (checkHasEmptyFields)
return res
.status(400)
.json({ response: "Please fill out all the fields." });
const autoGeneratedUsername = "user" + Math.round(Math.random() * 1000000000);
// Check email (if enabled)
const checkEmail =
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
if (emailEnabled && !checkEmail.test(body.email?.toLowerCase() || ""))
return res.status(400).json({
response: "Please enter a valid email.",
});
// Check username (if email was disabled)
const checkUsername = RegExp("^[a-z0-9_-]{3,31}$");
if (!emailEnabled && !checkUsername.test(body.username?.toLowerCase() || ""))
return res.status(400).json({
response:
"Username has to be between 3-30 characters, no spaces and special characters are allowed.",
});
if (!username) {
username = autoGeneratedUsername;
}
const checkIfUserExists = await prisma.user.findFirst({
where: emailEnabled
? {
email: body.email?.toLowerCase().trim(),
}
: {
username: (body.username as string).toLowerCase().trim(),
where: {
OR: [
{
email: email ? email.toLowerCase().trim() : undefined,
},
{
username: username ? username.toLowerCase().trim() : undefined,
},
],
},
});
if (!checkIfUserExists) {
const autoGeneratedUsername =
"user" + Math.round(Math.random() * 1000000000);
const saltRounds = 10;
const hashedPassword = bcrypt.hashSync(body.password, saltRounds);
const hashedPassword = bcrypt.hashSync(password, saltRounds);
await prisma.user.create({
data: {
name: body.name,
username: emailEnabled
? undefined
: (body.username as string).toLowerCase().trim(),
email: emailEnabled ? body.email?.toLowerCase().trim() : undefined,
password: hashedPassword,
},
});
// Subscription dates
const currentPeriodStart = new Date();
const currentPeriodEnd = new Date();
currentPeriodEnd.setFullYear(currentPeriodEnd.getFullYear() + 1000); // end date is in 1000 years...
return res.status(201).json({ response: "User successfully created." });
} else if (checkIfUserExists) {
return res.status(400).json({
response: `${emailEnabled ? "Email" : "Username"} already exists.`,
});
if (isAdmin) {
const user = await prisma.user.create({
data: {
name: name,
username: emailEnabled
? (username as string) || autoGeneratedUsername
: (username as string),
email: emailEnabled ? email : undefined,
password: hashedPassword,
emailVerified: new Date(),
subscriptions: stripeEnabled
? {
create: {
stripeSubscriptionId:
"fake_sub_" + Math.round(Math.random() * 10000000000000),
active: true,
currentPeriodStart,
currentPeriodEnd,
},
}
: undefined,
},
select: {
id: true,
username: true,
email: true,
emailVerified: true,
subscriptions: {
select: {
active: true,
},
},
createdAt: true,
},
});
return { response: user, status: 201 };
} else {
await prisma.user.create({
data: {
name: name,
username: emailEnabled ? autoGeneratedUsername : (username as string),
email: emailEnabled ? email : undefined,
password: hashedPassword,
},
});
return { response: "User successfully created.", status: 201 };
}
} else {
return { response: "Email or Username already exists.", status: 400 };
}
}
@@ -5,12 +5,10 @@ import Stripe from "stripe";
import { DeleteUserBody } from "@/types/global";
import removeFile from "@/lib/api/storage/removeFile";
const keycloakEnabled = process.env.KEYCLOAK_CLIENT_SECRET;
const authentikEnabled = process.env.AUTHENTIK_CLIENT_SECRET;
export default async function deleteUserById(
userId: number,
body: DeleteUserBody
body: DeleteUserBody,
isServerAdmin?: boolean
) {
// First, we retrieve the user from the database
const user = await prisma.user.findUnique({
@@ -24,16 +22,20 @@ export default async function deleteUserById(
};
}
// Then, we check if the provided password matches the one stored in the database (disabled in Keycloak integration)
if (!keycloakEnabled && !authentikEnabled) {
const isPasswordValid = bcrypt.compareSync(
body.password,
user.password as string
);
if (!isServerAdmin) {
if (user.password) {
const isPasswordValid = bcrypt.compareSync(body.password, user.password);
if (!isPasswordValid) {
if (!isPasswordValid && !isServerAdmin) {
return {
response: "Invalid credentials.",
status: 401, // Unauthorized
};
}
} else {
return {
response: "Invalid credentials.",
response:
"User has no password. Please reset your password from the forgot password page.",
status: 401, // Unauthorized
};
}
@@ -43,6 +45,11 @@ export default async function deleteUserById(
await prisma
.$transaction(
async (prisma) => {
// Delete Access Tokens
await prisma.accessToken.deleteMany({
where: { userId },
});
// Delete whitelisted users
await prisma.whitelistedUser.deleteMany({
where: { userId },
@@ -70,7 +77,11 @@ export default async function deleteUserById(
});
// Delete archive folders
removeFolder({ filePath: `archives/${collection.id}` });
await removeFolder({ filePath: `archives/${collection.id}` });
await removeFolder({
filePath: `archives/preview/${collection.id}`,
});
}
// Delete collections after cleaning up related data
@@ -80,9 +91,11 @@ export default async function deleteUserById(
// Delete subscription
if (process.env.STRIPE_SECRET_KEY)
await prisma.subscription.delete({
where: { userId },
});
await prisma.subscription
.delete({
where: { userId },
})
.catch((err) => console.log(err));
await prisma.usersAndCollections.deleteMany({
where: {
+134 -137
View File
@@ -3,172 +3,172 @@ 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 "@/lib/api/updateCustomerEmail";
import createFolder from "@/lib/api/storage/createFolder";
import sendChangeEmailVerificationRequest from "@/lib/api/sendChangeEmailVerificationRequest";
import { i18n } from "next-i18next.config";
import { UpdateUserSchema } from "@/lib/shared/schemaValidation";
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
export default async function updateUserById(
userId: number,
data: AccountSettings
body: AccountSettings
) {
const ssoUser = await prisma.account.findFirst({
const dataValidation = UpdateUserSchema().safeParse(body);
if (!dataValidation.success) {
return {
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
const data = dataValidation.data;
const userIsTaken = await prisma.user.findFirst({
where: {
userId: userId,
},
});
const user = await prisma.user.findUnique({
where: {
id: userId,
id: { not: userId },
OR: emailEnabled
? [
{
username: data.username.toLowerCase(),
},
{
email: data.email?.toLowerCase(),
},
]
: [
{
username: data.username.toLowerCase(),
},
],
},
});
if (ssoUser) {
// deny changes to SSO-defined properties
if (data.email !== user?.email) {
if (userIsTaken) {
if (data.email?.toLowerCase().trim() === userIsTaken.email?.trim())
return {
response: "SSO users cannot change their email.",
response: "Email is taken.",
status: 400,
};
else if (
data.username?.toLowerCase().trim() === userIsTaken.username?.trim()
)
return {
response: "Username is taken.",
status: 400,
};
return {
response: "Username/Email is taken.",
status: 400,
};
}
// Avatar Settings
if (
data.image?.startsWith("data:image/jpeg;base64") &&
data.image.length < 1572864
) {
try {
const base64Data = data.image.replace(/^data:image\/jpeg;base64,/, "");
createFolder({ filePath: `uploads/avatar` });
await createFile({
filePath: `uploads/avatar/${userId}.jpg`,
data: base64Data,
isBase64: true,
});
} catch (err) {
console.log("Error saving image:", err);
}
} else if (data.image?.length && data.image?.length >= 1572864) {
console.log("A file larger than 1.5MB was uploaded.");
return {
response: "A file larger than 1.5MB was uploaded.",
status: 400,
};
} else if (data.image == "") {
removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
}
// Email Settings
const user = await prisma.user.findUnique({
where: { id: userId },
select: { email: true, password: true, name: true },
});
if (user && user.email && data.email && data.email !== user.email) {
if (!data.password) {
return {
response: "Invalid password.",
status: 400,
};
}
if (data.newPassword) {
// Verify password
if (!user.password) {
return {
response: "SSO Users cannot change their password.",
response:
"User has no password. Please reset your password from the forgot password page.",
status: 400,
};
}
if (data.name !== user?.name) {
const passwordMatch = bcrypt.compareSync(data.password, user.password);
if (!passwordMatch) {
return {
response: "SSO Users cannot change their name.",
response: "Password is incorrect.",
status: 400,
};
}
if (data.username !== user?.username) {
sendChangeEmailVerificationRequest(
user.email,
data.email,
data.name?.trim() || user.name
);
}
// Password Settings
if (data.newPassword || data.oldPassword) {
if (!data.oldPassword || !data.newPassword)
return {
response: "SSO Users cannot change their username.",
response: "Please fill out all the fields.",
status: 400,
};
}
if (data.image?.startsWith("data:image/jpeg;base64")) {
else if (!user?.password)
return {
response: "SSO Users cannot change their avatar.",
response:
"User has no password. Please reset your password from the forgot password page.",
status: 400,
};
}
} else {
// verify only for non-SSO users
// SSO users cannot change their email, password, name, username, or avatar
if (emailEnabled && !data.email)
else if (!bcrypt.compareSync(data.oldPassword, user.password))
return {
response: "Email invalid.",
response: "Old password is incorrect.",
status: 400,
};
else if (!data.username)
return {
response: "Username invalid.",
status: 400,
};
if (data.newPassword && data.newPassword?.length < 8)
else if (data.newPassword?.length < 8)
return {
response: "Password must be at least 8 characters.",
status: 400,
};
// Check email (if enabled)
const checkEmail =
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
if (emailEnabled && !checkEmail.test(data.email?.toLowerCase() || ""))
else if (data.newPassword === data.oldPassword)
return {
response: "Please enter a valid email.",
response: "New password must be different from the old password.",
status: 400,
};
const checkUsername = RegExp("^[a-z0-9_-]{3,31}$");
if (!checkUsername.test(data.username.toLowerCase()))
return {
response:
"Username has to be between 3-30 characters, no spaces and special characters are allowed.",
status: 400,
};
const userIsTaken = await prisma.user.findFirst({
where: {
id: { not: userId },
OR: emailEnabled
? [
{
username: data.username.toLowerCase(),
},
{
email: data.email?.toLowerCase(),
},
]
: [
{
username: data.username.toLowerCase(),
},
],
},
});
if (userIsTaken) {
if (data.email?.toLowerCase().trim() === userIsTaken.email?.trim())
return {
response: "Email is taken.",
status: 400,
};
else if (
data.username?.toLowerCase().trim() === userIsTaken.username?.trim()
)
return {
response: "Username is taken.",
status: 400,
};
return {
response: "Username/Email is taken.",
status: 400,
};
}
// Avatar Settings
if (data.image?.startsWith("data:image/jpeg;base64")) {
if (data.image.length < 1572864) {
try {
const base64Data = data.image.replace(
/^data:image\/jpeg;base64,/,
""
);
createFolder({ filePath: `uploads/avatar` });
await createFile({
filePath: `uploads/avatar/${userId}.jpg`,
data: base64Data,
isBase64: true,
});
} catch (err) {
console.log("Error saving image:", err);
}
} else {
console.log("A file larger than 1.5MB was uploaded.");
return {
response: "A file larger than 1.5MB was uploaded.",
status: 400,
};
}
} else if (data.image == "") {
removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
}
}
const previousEmail = (
await prisma.user.findUnique({ where: { id: userId } })
)?.email;
// Other settings
// Other settings / Apply changes
const saltRounds = 10;
const newHashedPassword = bcrypt.hashSync(data.newPassword || "", saltRounds);
@@ -179,14 +179,20 @@ export default async function updateUserById(
},
data: {
name: data.name,
username: data.username?.toLowerCase().trim(),
email: data.email?.toLowerCase().trim(),
username: data.username,
isPrivate: data.isPrivate,
image: data.image ? `uploads/avatar/${userId}.jpg` : "",
collectionOrder: data.collectionOrder.filter(
image:
data.image && data.image.startsWith("http")
? data.image
: data.image
? `uploads/avatar/${userId}.jpg`
: "",
collectionOrder: data.collectionOrder?.filter(
(value, index, self) => self.indexOf(value) === index
),
locale: i18n.locales.includes(data.locale || "") ? data.locale : "en",
archiveAsScreenshot: data.archiveAsScreenshot,
archiveAsMonolith: data.archiveAsMonolith,
archiveAsPDF: data.archiveAsPDF,
archiveAsWaybackMachine: data.archiveAsWaybackMachine,
linksRouteTo: data.linksRouteTo,
@@ -244,15 +250,6 @@ export default async function updateUserById(
});
}
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
if (STRIPE_SECRET_KEY && emailEnabled && previousEmail !== data.email)
await updateCustomerEmail(
STRIPE_SECRET_KEY,
previousEmail as string,
data.email as string
);
const response: Omit<AccountSettings, "password"> = {
...userInfo,
whitelistedUsers: newWhitelistedUsernames,