Merge branch 'dev' of https://github.com/linkwarden/linkwarden into feat/single-file

This commit is contained in:
daniel31x13
2024-06-18 12:19:52 -04:00
161 changed files with 7449 additions and 2996 deletions
@@ -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: [
{
@@ -46,7 +46,7 @@ export default async function getDashboardData(
});
const recentlyAddedLinks = await prisma.link.findMany({
take: 8,
take: 10,
where: {
collection: {
OR: [
@@ -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,18 +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`,
});
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.html`,
});
if (collectionIsAccessible) removeFiles(linkId, collectionIsAccessible.id);
}
return { response: deletedLinks, status: 200 };
@@ -2,6 +2,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 +13,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,18 +25,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`,
});
removeFile({
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.html`,
});
removeFiles(linkId, collectionIsAccessible.id);
return { response: deleteLink, status: 200 };
}
@@ -2,7 +2,7 @@ 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 } from "@/lib/api/manageLinkFiles";
export default async function updateLinkById(
userId: number,
@@ -146,25 +146,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 moveFile(
`archives/${collectionIsAccessible?.id}/${linkId}.html`,
`archives/${data.collection.id}/${linkId}.html`
);
await moveFiles(linkId, collectionIsAccessible?.id, data.collection.id);
}
return { response: updatedLink, status: 200 };
+30 -18
View File
@@ -12,14 +12,16 @@ export default async function postLink(
link: LinkIncludingShortenedCollectionAndTags,
userId: number
) {
try {
new URL(link.url || "");
} catch (error) {
return {
response:
"Please enter a valid Address for the Link. (It should start with http/https)",
status: 400,
};
if (link.url || link.type === "url") {
try {
new URL(link.url || "");
} catch (error) {
return {
response:
"Please enter a valid Address for the Link. (It should start with http/https)",
status: 400,
};
}
}
if (!link.collection.id && link.collection.name) {
@@ -117,15 +119,24 @@ 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.`);
console.log(url, urlWithoutWww, urlWithWww);
const existingLink = await prisma.link.findFirst({
where: {
url: link.url?.trim(),
OR: [{ url: urlWithWww }, { url: urlWithoutWww }],
collection: {
ownerId: userId,
},
},
});
console.log(url, urlWithoutWww, urlWithWww, "DONE!");
if (existingLink)
return {
response: "Link already exists",
@@ -149,12 +160,13 @@ export default async function postLink(
link.collection.name = link.collection.name.trim();
const description =
link.description && link.description !== ""
? link.description
: link.url
? await getTitle(link.url)
: undefined;
const title =
!(link.name && link.name !== "") && link.url
? await getTitle(link.url)
: "";
const name =
link.name && link.name !== "" ? link.name : link.url ? title : "";
const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
@@ -172,9 +184,9 @@ export default async function postLink(
const newLink = await prisma.link.create({
data: {
url: link.url?.trim(),
name: link.name,
description,
url: link.url?.trim().replace(/\/+$/, "") || null,
name,
description: link.description,
type: linkType,
collection: {
connect: {
@@ -2,6 +2,7 @@ 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;
@@ -36,7 +37,9 @@ export default async function importFromHTMLFile(
const jsonData = parse(document.documentElement.outerHTML);
for (const item of jsonData) {
const processedArray = processNodes(jsonData);
for (const item of processedArray) {
console.log(item);
await processBookmarks(userId, item as Element);
}
@@ -74,7 +77,9 @@ async function processBookmarks(
} else if (item.type === "element" && item.tagName === "a") {
// process link
const linkUrl = item?.attributes.find((e) => e.key === "href")?.value;
const linkUrl = item?.attributes.find(
(e) => e.key.toLowerCase() === "href"
)?.value;
const linkName = (
item?.children.find((e) => e.type === "text") as TextNode
)?.content;
@@ -82,14 +87,33 @@ async function processBookmarks(
.find((e) => e.key === "tags")
?.value.split(",");
// set date if available
const linkDateValue = item?.attributes.find(
(e) => e.key.toLowerCase() === "add_date"
)?.value;
const linkDate = linkDateValue
? new Date(Number(linkDateValue) * 1000)
: undefined;
let linkDesc =
(
(
item?.children?.find(
(e) => e.type === "element" && e.tagName === "dd"
) as Element
)?.children[0] as TextNode
)?.content || "";
if (linkUrl && parentCollectionId) {
await createLink(
userId,
linkUrl,
parentCollectionId,
linkName,
"",
linkTags
linkDesc,
linkTags,
linkDate
);
} else if (linkUrl) {
// create a collection named "Imported Bookmarks" and add the link to it
@@ -100,8 +124,9 @@ async function processBookmarks(
linkUrl,
collectionId,
linkName,
"",
linkTags
linkDesc,
linkTags,
linkDate
);
}
@@ -160,7 +185,8 @@ const createLink = async (
collectionId: number,
name?: string,
description?: string,
tags?: string[]
tags?: string[],
importDate?: Date
) => {
await prisma.link.create({
data: {
@@ -193,6 +219,48 @@ const createLink = async (
}),
}
: undefined,
importDate: importDate || undefined,
},
});
};
function processNodes(nodes: Node[]) {
const findAndProcessDL = (node: Node) => {
if (node.type === "element" && node.tagName === "dl") {
processDLChildren(node);
} else if (
node.type === "element" &&
node.children &&
node.children.length
) {
node.children.forEach((child) => findAndProcessDL(child));
}
};
const processDLChildren = (dlNode: Element) => {
dlNode.children.forEach((child, i) => {
if (child.type === "element" && child.tagName === "dt") {
const nextSibling = dlNode.children[i + 1];
if (
nextSibling &&
nextSibling.type === "element" &&
nextSibling.tagName === "dd"
) {
const aElement = child.children.find(
(el) => el.type === "element" && el.tagName === "a"
);
if (aElement && aElement.type === "element") {
// Add the 'dd' element as a child of the 'a' element
aElement.children.push(nextSibling);
// Remove the 'dd' from the parent 'dl' to avoid duplicate processing
dlNode.children.splice(i + 1, 1);
// Adjust the loop counter due to the removal
}
}
}
});
};
nodes.forEach(findAndProcessDL);
return nodes;
}
@@ -54,7 +54,7 @@ export default async function importFromLinkwarden(
// Import Links
for (const link of e.links) {
const newLink = await prisma.link.create({
await prisma.link.create({
data: {
url: link.url,
name: link.name,
@@ -0,0 +1,115 @@
import { prisma } from "@/lib/api/db";
import { Backup } from "@/types/global";
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: `Error: Each user 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) {
await prisma.link.create({
data: {
pinnedBy: link.is_starred
? { connect: { id: userId } }
: undefined,
url: link.url,
name: link.title || "",
textContent: link.content || "",
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(),
ownerId: userId,
},
},
create: {
name: tag.trim(),
owner: {
connect: {
id: userId,
},
},
},
})),
}
: undefined,
},
});
}
},
{ timeout: 30000 }
)
.catch((err) => console.log(err));
return { response: "Success.", status: 200 };
}
+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 };
}
+86 -36
View File
@@ -1,12 +1,15 @@
import { prisma } from "@/lib/api/db";
import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt";
import isServerAdmin from "../../isServerAdmin";
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;
status: number;
}
interface User {
@@ -18,10 +21,12 @@ interface User {
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;
@@ -31,61 +36,106 @@ export default async function postUser(
: !body.username || !body.password || !body.name;
if (!body.password || body.password.length < 8)
return res
.status(400)
.json({ response: "Password must be at least 8 characters." });
return { response: "Password must be at least 8 characters.", status: 400 };
if (checkHasEmptyFields)
return res
.status(400)
.json({ response: "Please fill out all the fields." });
return { response: "Please fill out all the fields.", status: 400 };
// 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.",
});
return { response: "Please enter a valid email.", status: 400 };
// 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({
return {
response:
"Username has to be between 3-30 characters, no spaces and special characters are allowed.",
});
status: 400,
};
const checkIfUserExists = await prisma.user.findFirst({
where: emailEnabled
? {
email: body.email?.toLowerCase().trim(),
}
: {
username: (body.username as string).toLowerCase().trim(),
where: {
OR: [
{
email: body.email ? body.email.toLowerCase().trim() : undefined,
},
{
username: body.username
? body.username.toLowerCase().trim()
: undefined,
},
],
},
});
if (!checkIfUserExists) {
const autoGeneratedUsername =
"user" + Math.round(Math.random() * 1000000000);
const saltRounds = 10;
const hashedPassword = bcrypt.hashSync(body.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: body.name,
username: emailEnabled
? autoGeneratedUsername
: (body.username as string).toLowerCase().trim(),
email: emailEnabled ? body.email?.toLowerCase().trim() : 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: body.name,
username: emailEnabled
? autoGeneratedUsername
: (body.username as string).toLowerCase().trim(),
email: emailEnabled ? body.email?.toLowerCase().trim() : 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,23 @@ 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 as string
);
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 +48,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 },
@@ -71,6 +81,10 @@ export default async function deleteUserById(
// Delete archive folders
removeFolder({ filePath: `archives/${collection.id}` });
await removeFolder({
filePath: `archives/preview/${collection.id}`,
});
}
// Delete collections after cleaning up related data
@@ -80,9 +94,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: {
+146 -135
View File
@@ -3,8 +3,9 @@ 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";
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
@@ -13,162 +14,176 @@ export default async function updateUserById(
userId: number,
data: AccountSettings
) {
const ssoUser = await prisma.account.findFirst({
if (emailEnabled && !data.email)
return {
response: "Email invalid.",
status: 400,
};
else if (!data.username)
return {
response: "Username invalid.",
status: 400,
};
// Check email (if enabled)
const checkEmail =
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
if (emailEnabled && !checkEmail.test(data.email?.toLowerCase() || ""))
return {
response: "Please enter a valid email.",
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: {
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 },
});
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()
);
}
// 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);
@@ -178,14 +193,19 @@ export default async function updateUserById(
id: userId,
},
data: {
name: data.name,
name: data.name.trim(),
username: data.username?.toLowerCase().trim(),
email: data.email?.toLowerCase().trim(),
isPrivate: data.isPrivate,
image: data.image ? `uploads/avatar/${userId}.jpg` : "",
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,
archiveAsSinglefile: data.archiveAsSinglefile,
archiveAsPDF: data.archiveAsPDF,
@@ -245,15 +265,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,