diff --git a/lib/api/controllers/collections/collectionId/updateCollectionById.ts b/lib/api/controllers/collections/collectionId/updateCollectionById.ts index 564f263..deffe69 100644 --- a/lib/api/controllers/collections/collectionId/updateCollectionById.ts +++ b/lib/api/controllers/collections/collectionId/updateCollectionById.ts @@ -1,15 +1,32 @@ 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, @@ -76,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, diff --git a/lib/api/controllers/links/bulk/updateLinks.ts b/lib/api/controllers/links/bulk/updateLinks.ts index a214c30..4920c00 100644 --- a/lib/api/controllers/links/bulk/updateLinks.ts +++ b/lib/api/controllers/links/bulk/updateLinks.ts @@ -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: { diff --git a/lib/api/controllers/links/linkId/updateLinkById.ts b/lib/api/controllers/links/linkId/updateLinkById.ts index 1a3d20d..11d01e9 100644 --- a/lib/api/controllers/links/linkId/updateLinkById.ts +++ b/lib/api/controllers/links/linkId/updateLinkById.ts @@ -1,20 +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 { 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 }); @@ -33,10 +43,11 @@ export default async function updateLinkById( id: linkId, }, data: { - pinnedBy: - data?.pinnedBy && data.pinnedBy[0].id === userId + pinnedBy: data?.pinnedBy + ? data.pinnedBy[0]?.id === userId ? { connect: { id: userId } } - : { disconnect: { id: userId } }, + : { disconnect: { id: userId } } + : undefined, }, include: { collection: true, @@ -63,11 +74,9 @@ export default async function updateLinkById( 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 (!targetCollectionMatchesData) return { @@ -149,10 +158,11 @@ export default async function updateLinkById( }, })), }, - pinnedBy: - data?.pinnedBy && data.pinnedBy[0]?.id === userId + pinnedBy: data?.pinnedBy + ? data.pinnedBy[0]?.id === userId ? { connect: { id: userId } } - : { disconnect: { id: userId } }, + : { disconnect: { id: userId } } + : undefined, }, include: { tags: true, diff --git a/lib/api/controllers/tags/tagId/updeteTagById.ts b/lib/api/controllers/tags/tagId/updeteTagById.ts index f2d2608..196904a 100644 --- a/lib/api/controllers/tags/tagId/updeteTagById.ts +++ b/lib/api/controllers/tags/tagId/updeteTagById.ts @@ -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, }, }); diff --git a/lib/api/controllers/users/userId/updateUserById.ts b/lib/api/controllers/users/userId/updateUserById.ts index ebae737..c62a594 100644 --- a/lib/api/controllers/users/userId/updateUserById.ts +++ b/lib/api/controllers/users/userId/updateUserById.ts @@ -6,42 +6,27 @@ import createFile from "@/lib/api/storage/createFile"; 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 ) { - if (emailEnabled && !data.email) - return { - response: "Email invalid.", - status: 400, - }; - else if (!data.username) - return { - response: "Username invalid.", - status: 400, - }; + const dataValidation = UpdateUserSchema().safeParse(body); - // Check email (if enabled) - const checkEmail = - /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; - if (emailEnabled && !checkEmail.test(data.email?.toLowerCase() || "")) + if (!dataValidation.success) { return { - response: "Please enter a valid email.", + response: `Error: ${ + dataValidation.error.issues[0].message + } [${dataValidation.error.issues[0].path.join(", ")}]`, 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 data = dataValidation.data; const userIsTaken = await prisma.user.findFirst({ where: { @@ -116,7 +101,7 @@ export default async function updateUserById( const user = await prisma.user.findUnique({ where: { id: userId }, - select: { email: true, password: true }, + select: { email: true, password: true, name: true }, }); if (user && user.email && data.email && data.email !== user.email) { @@ -148,7 +133,7 @@ export default async function updateUserById( sendChangeEmailVerificationRequest( user.email, data.email, - data.name.trim() + data.name?.trim() || user.name ); } @@ -193,8 +178,8 @@ export default async function updateUserById( id: userId, }, data: { - name: data.name.trim(), - username: data.username?.toLowerCase().trim(), + name: data.name, + username: data.username, isPrivate: data.isPrivate, image: data.image && data.image.startsWith("http") @@ -202,10 +187,10 @@ export default async function updateUserById( : data.image ? `uploads/avatar/${userId}.jpg` : "", - collectionOrder: data.collectionOrder.filter( + collectionOrder: data.collectionOrder?.filter( (value, index, self) => self.indexOf(value) === index ), - locale: i18n.locales.includes(data.locale) ? data.locale : "en", + locale: i18n.locales.includes(data.locale || "") ? data.locale : "en", archiveAsScreenshot: data.archiveAsScreenshot, archiveAsMonolith: data.archiveAsMonolith, archiveAsPDF: data.archiveAsPDF, diff --git a/lib/shared/schemaValidation.ts b/lib/shared/schemaValidation.ts index 014eee3..ee105ab 100644 --- a/lib/shared/schemaValidation.ts +++ b/lib/shared/schemaValidation.ts @@ -1,4 +1,5 @@ import { ArchivedFormat, TokenExpiry } from "@/types/global"; +import { LinksRouteTo } from "@prisma/client"; import { z } from "zod"; // const stringField = z.string({ @@ -33,19 +34,53 @@ export const PostUserSchema = () => { return z.object({ name: z.string().trim().min(1).max(50), - password: z.string().min(8), + password: z.string().min(8).max(2048), email: emailEnabled ? z.string().trim().email().toLowerCase() : z.string().optional(), username: z .string() .trim() + .toLowerCase() .min(3) .max(50) .regex(/^[a-z0-9_-]{3,50}$/), }); }; +export const UpdateUserSchema = () => { + const emailEnabled = + process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false; + + return z.object({ + name: z.string().trim().min(1).max(50).optional(), + email: emailEnabled + ? z.string().trim().email().toLowerCase() + : z.string().optional(), + username: z + .string() + .trim() + .toLowerCase() + .min(3) + .max(30) + .regex(/^[a-z0-9_-]{3,30}$/), + image: z.string().optional(), + password: z.string().min(8).max(2048).optional(), + newPassword: z.string().min(8).max(2048).optional(), + oldPassword: z.string().min(8).max(2048).optional(), + archiveAsScreenshot: z.boolean().optional(), + archiveAsPDF: z.boolean().optional(), + archiveAsMonolith: z.boolean().optional(), + archiveAsWaybackMachine: z.boolean().optional(), + locale: z.string().max(20).optional(), + isPrivate: z.boolean().optional(), + preventDuplicateLinks: z.boolean().optional(), + collectionOrder: z.array(z.number()).optional(), + linksRouteTo: z.nativeEnum(LinksRouteTo).optional(), + whitelistedUsers: z.array(z.string().max(50)).optional(), + }); +}; + export const PostSessionSchema = z.object({ username: z.string().min(3).max(50), password: z.string().min(8), @@ -54,13 +89,13 @@ export const PostSessionSchema = z.object({ export const PostLinkSchema = z.object({ type: z.enum(["url", "pdf", "image"]), - url: z.string().trim().max(255).url().optional(), - name: z.string().trim().max(255).optional(), - description: z.string().trim().max(255).optional(), + url: z.string().trim().max(2048).url().optional(), + name: z.string().trim().max(2048).optional(), + description: z.string().trim().max(2048).optional(), collection: z .object({ id: z.number().optional(), - name: z.string().trim().max(255).optional(), + name: z.string().trim().max(2048).optional(), }) .optional(), tags: @@ -76,6 +111,35 @@ export const PostLinkSchema = z.object({ export type PostLinkSchemaType = z.infer; +export const UpdateLinkSchema = z.object({ + id: z.number(), + name: z.string().trim().max(2048).optional(), + url: z.string().trim().max(2048).optional(), + description: z.string().trim().max(2048).optional(), + icon: z.string().trim().max(50).nullish(), + iconWeight: z.string().trim().max(50).nullish(), + color: z.string().trim().max(10).nullish(), + collection: z.object({ + id: z.number(), + ownerId: z.number(), + }), + tags: z.array( + z.object({ + id: z.number().optional(), + name: z.string().trim().max(50), + }) + ), + pinnedBy: z + .array( + z.object({ + id: z.number(), + }) + ) + .optional(), +}); + +export type UpdateLinkSchemaType = z.infer; + const ACCEPTED_TYPES = [ "image/jpeg", "image/jpg", @@ -103,12 +167,39 @@ export const UploadFileSchema = z.object({ }); export const PostCollectionSchema = z.object({ - name: z.string().trim().max(255), - description: z.string().trim().max(255).optional(), - color: z.string().trim().max(7).optional(), + name: z.string().trim().max(2048), + description: z.string().trim().max(2048).optional(), + color: z.string().trim().max(10).optional(), icon: z.string().trim().max(50).optional(), iconWeight: z.string().trim().max(50).optional(), parentId: z.number().optional(), }); export type PostCollectionSchemaType = z.infer; + +export const UpdateCollectionSchema = z.object({ + id: z.number(), + name: z.string().trim().max(2048), + description: z.string().trim().max(2048).optional(), + color: z.string().trim().max(10).optional(), + isPublic: z.boolean().optional(), + icon: z.string().trim().max(50).nullish(), + iconWeight: z.string().trim().max(50).nullish(), + parentId: z.number().nullish(), + members: z.array( + z.object({ + userId: z.number(), + canCreate: z.boolean(), + canUpdate: z.boolean(), + canDelete: z.boolean(), + }) + ), +}); + +export type UpdateCollectionSchemaType = z.infer; + +export const UpdateTagSchema = z.object({ + name: z.string().trim().max(50), +}); + +export type UpdateTagSchemaType = z.infer; diff --git a/pages/api/v1/links/index.ts b/pages/api/v1/links/index.ts index 2000e79..dc7a217 100644 --- a/pages/api/v1/links/index.ts +++ b/pages/api/v1/links/index.ts @@ -60,6 +60,7 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) { req.body.removePreviousTags, req.body.newData ); + return res.status(updated.status).json({ response: updated.response, }); diff --git a/pages/api/v1/tags/[id].ts b/pages/api/v1/tags/[id].ts index 606daf0..52f86b4 100644 --- a/pages/api/v1/tags/[id].ts +++ b/pages/api/v1/tags/[id].ts @@ -9,6 +9,11 @@ export default async function tags(req: NextApiRequest, res: NextApiResponse) { const tagId = Number(req.query.id); + if (!tagId) + return res.status(400).json({ + response: "Please choose a valid name for the tag.", + }); + if (req.method === "PUT") { if (process.env.NEXT_PUBLIC_DEMO === "true") return res.status(400).json({ diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 2d92518..5a6e4f9 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -17,6 +17,7 @@ import { i18n } from "next-i18next.config"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useUpdateUser, useUser } from "@/hooks/store/user"; +import { z } from "zod"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; @@ -80,6 +81,16 @@ export default function Account() { }; const submit = async (password?: string) => { + if (!/^[a-z0-9_-]{3,50}$/.test(user.username || "")) { + return toast.error(t("username_invalid_guide")); + } + + const emailSchema = z.string().trim().email().toLowerCase(); + const emailValidation = emailSchema.safeParse(user.email || ""); + if (!emailValidation.success) { + return toast.error(t("email_invalid")); + } + setSubmitLoader(true); const load = toast.loading(t("applying_settings")); @@ -207,6 +218,7 @@ export default function Account() {

{t("email")}

setUser({ ...user, email: e.target.value })} /> diff --git a/public/locales/en/common.json b/public/locales/en/common.json index ac1fbd2..f717bf7 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -394,5 +394,7 @@ "upload_preview_image": "Upload Preview Image", "columns": "Columns", "default": "Default", - "invalid_url_guide":"Please enter a valid Address for the Link. (It should start with http/https)" + "invalid_url_guide":"Please enter a valid Address for the Link. (It should start with http/https)", + "email_invalid": "Please enter a valid email address.", + "username_invalid_guide": "Username has to be at least 3 characters, no spaces and special characters are allowed." } \ No newline at end of file