added zod for post requests

This commit is contained in:
daniel31x13 2024-09-14 16:00:19 -04:00
parent a5b1952e0d
commit 1cf7421b76
24 changed files with 350 additions and 180 deletions

View File

@ -7,7 +7,7 @@ import { useTags } from "@/hooks/store/tags";
type Props = {
onChange: any;
defaultValue?: {
value: number;
value?: number;
label: string;
}[];
autoFocus?: boolean;

View File

@ -3,14 +3,13 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection";
import TagSelection from "@/components/InputSelect/TagSelection";
import TextInput from "@/components/TextInput";
import unescapeString from "@/lib/client/unescapeString";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useAddLink } from "@/hooks/store/links";
import toast from "react-hot-toast";
import { PostLinkSchemaType } from "@/lib/shared/schemaValidation";
type Props = {
onClose: Function;
@ -18,30 +17,19 @@ type Props = {
export default function NewLinkModal({ onClose }: Props) {
const { t } = useTranslation();
const { data } = useSession();
const initial = {
name: "",
url: "",
description: "",
type: "url",
tags: [],
preview: "",
image: "",
pdf: "",
readable: "",
monolith: "",
textContent: "",
icon: "",
iconWeight: "",
color: "",
collection: {
id: undefined,
name: "",
ownerId: data?.user.id as number,
},
} as LinkIncludingShortenedCollectionAndTags;
} as PostLinkSchemaType;
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const [link, setLink] = useState<PostLinkSchemaType>(initial);
const addLink = useAddLink();
@ -51,10 +39,10 @@ export default function NewLinkModal({ onClose }: Props) {
const { data: collections = [] } = useCollections();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
if (e?.__isNew__) e.value = undefined;
setLink({
...link,
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
collection: { id: e?.value, name: e?.label },
});
};
@ -68,23 +56,19 @@ export default function NewLinkModal({ onClose }: Props) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
if (
currentCollection &&
currentCollection.ownerId &&
router.asPath.startsWith("/collections/")
)
if (currentCollection && currentCollection.ownerId)
setLink({
...initial,
collection: {
id: currentCollection.id,
name: currentCollection.name,
ownerId: currentCollection.ownerId,
},
});
} else
setLink({
...initial,
collection: { name: "Unorganized", ownerId: data?.user.id as number },
collection: { name: "Unorganized" },
});
}, []);
@ -99,7 +83,7 @@ export default function NewLinkModal({ onClose }: Props) {
toast.dismiss(load);
if (error) {
toast.error(error.message);
toast.error(t(error.message));
} else {
onClose();
toast.success(t("link_created"));
@ -127,12 +111,12 @@ export default function NewLinkModal({ onClose }: Props) {
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">{t("collection")}</p>
{link.collection.name && (
{link.collection?.name && (
<CollectionSelection
onChange={setCollection}
defaultValue={{
label: link.collection.name,
value: link.collection.id,
value: link.collection?.id,
label: link.collection?.name || "Unorganized",
}}
/>
)}
@ -155,7 +139,7 @@ export default function NewLinkModal({ onClose }: Props) {
<p className="mb-2">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => ({
defaultValue={link.tags?.map((e) => ({
label: e.name,
value: e.id,
}))}
@ -164,7 +148,7 @@ export default function NewLinkModal({ onClose }: Props) {
<div className="sm:col-span-2">
<p className="mb-2">{t("description")}</p>
<textarea
value={unescapeString(link.description) as string}
value={unescapeString(link.description || "") || ""}
onChange={(e) =>
setLink({ ...link, description: e.target.value })
}

View File

@ -14,6 +14,7 @@ import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUploadFile } from "@/hooks/store/links";
import { PostLinkSchemaType } from "@/lib/shared/schemaValidation";
type Props = {
onClose: Function;
@ -25,27 +26,16 @@ export default function UploadFileModal({ onClose }: Props) {
const initial = {
name: "",
url: "",
description: "",
type: "url",
tags: [],
preview: "",
image: "",
pdf: "",
readable: "",
monolith: "",
textContent: "",
icon: "",
iconWeight: "",
color: "",
collection: {
id: undefined,
name: "",
ownerId: data?.user.id as number,
},
} as LinkIncludingShortenedCollectionAndTags;
} as PostLinkSchemaType;
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const [link, setLink] = useState<PostLinkSchemaType>(initial);
const [file, setFile] = useState<File>();
const uploadFile = useUploadFile();
@ -55,11 +45,11 @@ export default function UploadFileModal({ onClose }: Props) {
const { data: collections = [] } = useCollections();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
if (e?.__isNew__) e.value = undefined;
setLink({
...link,
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
collection: { id: e?.value, name: e?.label },
});
};
@ -77,6 +67,7 @@ export default function UploadFileModal({ onClose }: Props) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
if (
currentCollection &&
currentCollection.ownerId &&
@ -87,13 +78,12 @@ export default function UploadFileModal({ onClose }: Props) {
collection: {
id: currentCollection.id,
name: currentCollection.name,
ownerId: currentCollection.ownerId,
},
});
} else
setLink({
...initial,
collection: { name: "Unorganized", ownerId: data?.user.id as number },
collection: { name: "Unorganized" },
});
}, [router, collections]);
@ -166,12 +156,12 @@ export default function UploadFileModal({ onClose }: Props) {
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">{t("collection")}</p>
{link.collection.name && (
{link.collection?.name && (
<CollectionSelection
onChange={setCollection}
defaultValue={{
label: link.collection.name,
value: link.collection.id,
value: link.collection?.id,
label: link.collection?.name || "Unorganized",
}}
/>
)}
@ -193,16 +183,16 @@ export default function UploadFileModal({ onClose }: Props) {
<p className="mb-2">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => ({
label: e.name,
defaultValue={link.tags?.map((e) => ({
value: e.id,
label: e.name,
}))}
/>
</div>
<div className="sm:col-span-2">
<p className="mb-2">{t("description")}</p>
<textarea
value={unescapeString(link.description) as string}
value={unescapeString(link.description || "") || ""}
onChange={(e) =>
setLink({ ...link, description: e.target.value })
}

View File

@ -13,7 +13,7 @@ import {
} from "@/types/global";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import Jimp from "jimp";
import { PostLinkSchemaType } from "@/lib/shared/schemaValidation";
const useLinks = (params: LinkRequestQuery = {}) => {
const router = useRouter();
@ -104,7 +104,15 @@ const useAddLink = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => {
mutationFn: async (link: PostLinkSchemaType) => {
if (link.url || link.type === "url") {
try {
new URL(link.url || "");
} catch (error) {
throw new Error("invalid_url_guide");
}
}
const response = await fetch("/api/v1/links", {
method: "POST",
headers: {
@ -124,7 +132,7 @@ const useAddLink = () => {
if (!oldData?.links) return undefined;
return {
...oldData,
links: [data, ...oldData.links],
links: [data, ...oldData?.links],
};
});
@ -311,7 +319,7 @@ const useBulkDeleteLinks = () => {
},
onSuccess: (data) => {
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData.links) return undefined;
if (!oldData?.links) return undefined;
return oldData.links.filter((e: any) => !data.includes(e.id));
});
@ -389,7 +397,7 @@ const useUploadFile = () => {
if (!oldData?.links) return undefined;
return {
...oldData,
links: [data, ...oldData.links],
links: [data, ...oldData?.links],
};
});

View File

@ -27,6 +27,9 @@ const useAddToken = () => {
const response = await fetch("/api/v1/tokens", {
body: JSON.stringify(body),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();

View File

@ -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({

View File

@ -1,26 +1,30 @@
import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import fetchTitleAndHeaders from "@/lib/shared/fetchTitleAndHeaders";
import createFolder from "@/lib/api/storage/createFolder";
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
) {
if (link.url || link.type === "url") {
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,
};
}
}
const link = dataValidation.data;
const linkCollection = await setLinkCollection(link, userId);

View File

@ -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;
@ -155,6 +154,8 @@ const createCollection = async (
collectionName: string,
parentId?: number
) => {
collectionName = collectionName.trim().slice(0, 254);
const findCollection = await prisma.collection.findFirst({
where: {
parentId,
@ -199,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 || "",

View File

@ -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) {
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,

View File

@ -67,14 +67,22 @@ export default async function importFromWallabag(
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,
name: link.title || "",
textContent: link.content || "",
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: {
@ -87,12 +95,12 @@ export default async function importFromWallabag(
connectOrCreate: link.tags.map((tag) => ({
where: {
name_ownerId: {
name: tag.trim(),
name: tag?.trim().slice(0, 49),
ownerId: userId,
},
},
create: {
name: tag.trim(),
name: tag?.trim().slice(0, 49),
owner: {
connect: {
id: userId,

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 {
@ -75,7 +79,7 @@ export default async function postToken(
const createToken = await prisma.accessToken.create({
data: {
name: body.name,
name: name,
userId,
token: tokenBody?.jti as string,
expires: expiryDate,

View File

@ -2,6 +2,7 @@ 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;
@ -12,13 +13,6 @@ interface Data {
status: number;
}
interface User {
name: string;
username?: string;
email?: string;
password: string;
}
export default async function postUser(
req: NextApiRequest,
res: NextApiResponse
@ -29,49 +23,34 @@ export default async function postUser(
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 { response: "Password must be at least 8 characters.", status: 400 };
if (checkHasEmptyFields)
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 { response: "Please enter a valid email.", status: 400 };
// Check username (if email was disabled)
const checkUsername = RegExp("^[a-z0-9_-]{3,31}$");
const { name, email, password } = dataValidation.data;
let { username } = dataValidation.data;
const autoGeneratedUsername = "user" + Math.round(Math.random() * 1000000000);
if (body.username && !checkUsername.test(body.username?.toLowerCase()))
return {
response:
"Username has to be between 3-30 characters, no spaces and special characters are allowed.",
status: 400,
};
else if (!body.username) {
body.username = autoGeneratedUsername;
if (!username) {
username = autoGeneratedUsername;
}
const checkIfUserExists = await prisma.user.findFirst({
where: {
OR: [
{
email: body.email ? body.email.toLowerCase().trim() : undefined,
email: email ? email.toLowerCase().trim() : undefined,
},
{
username: body.username
? body.username.toLowerCase().trim()
: undefined,
username: username ? username.toLowerCase().trim() : undefined,
},
],
},
@ -83,7 +62,7 @@ export default async function postUser(
const saltRounds = 10;
const hashedPassword = bcrypt.hashSync(body.password, saltRounds);
const hashedPassword = bcrypt.hashSync(password, saltRounds);
// Subscription dates
const currentPeriodStart = new Date();
@ -93,12 +72,11 @@ export default async function postUser(
if (isAdmin) {
const user = await prisma.user.create({
data: {
name: body.name,
name: name,
username: emailEnabled
? (body.username as string).toLowerCase().trim() ||
autoGeneratedUsername
: (body.username as string).toLowerCase().trim(),
email: emailEnabled ? body.email?.toLowerCase().trim() : undefined,
? (username as string) || autoGeneratedUsername
: (username as string),
email: emailEnabled ? email : undefined,
password: hashedPassword,
emailVerified: new Date(),
subscriptions: stripeEnabled
@ -131,11 +109,9 @@ export default async function postUser(
} 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,
name: name,
username: emailEnabled ? autoGeneratedUsername : (username as string),
email: emailEnabled ? email : undefined,
password: hashedPassword,
},
});

View File

@ -1,13 +1,10 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { prisma } from "./db";
import getPermission from "./getPermission";
import { UsersAndCollections } from "@prisma/client";
import { PostLinkSchemaType } from "../shared/schemaValidation";
const setLinkCollection = async (
link: LinkIncludingShortenedCollectionAndTags,
userId: number
) => {
if (link?.collection?.id && typeof link?.collection?.id === "number") {
const setLinkCollection = async (link: PostLinkSchemaType, userId: number) => {
if (link.collection?.id && typeof link.collection?.id === "number") {
const existingCollection = await prisma.collection.findUnique({
where: {
id: link.collection.id,
@ -29,7 +26,7 @@ const setLinkCollection = async (
return null;
return existingCollection;
} else if (link?.collection?.name) {
} else if (link.collection?.name) {
if (link.collection.name === "Unorganized") {
const firstTopLevelUnorganizedCollection =
await prisma.collection.findFirst({

View File

@ -0,0 +1,114 @@
import { ArchivedFormat, TokenExpiry } from "@/types/global";
import { z } from "zod";
// const stringField = z.string({
// errorMap: (e) => ({
// message: `Invalid ${e.path}.`,
// }),
// });
export const ForgotPasswordSchema = z.object({
email: z.string().email(),
});
export const ResetPasswordSchema = z.object({
token: z.string(),
password: z.string().min(8),
});
export const VerifyEmailSchema = z.object({
token: z.string(),
});
export const PostTokenSchema = z.object({
name: z.string().max(50),
expires: z.nativeEnum(TokenExpiry),
});
export type PostTokenSchemaType = z.infer<typeof PostTokenSchema>;
export const PostUserSchema = () => {
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
return z.object({
name: z.string().trim().min(1).max(50),
password: z.string().min(8),
email: emailEnabled
? z.string().trim().email().toLowerCase()
: z.string().optional(),
username: z
.string()
.trim()
.min(3)
.max(50)
.regex(/^[a-z0-9_-]{3,50}$/),
});
};
export const PostSessionSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(8),
sessionName: z.string().trim().max(50).optional(),
});
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(),
collection: z
.object({
id: z.number().optional(),
name: z.string().trim().max(255).optional(),
})
.optional(),
tags:
z
.array(
z.object({
id: z.number().optional(),
name: z.string().trim().max(50),
})
)
.optional() || [],
});
export type PostLinkSchemaType = z.infer<typeof PostLinkSchema>;
const ACCEPTED_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"application/pdf",
];
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10
);
const MAX_FILE_SIZE = NEXT_PUBLIC_MAX_FILE_BUFFER * 1024 * 1024;
export const UploadFileSchema = z.object({
file: z
.any()
.refine((files) => files?.length == 1, "File is required.")
.refine(
(files) => files?.[0]?.size <= MAX_FILE_SIZE,
`Max file size is ${MAX_FILE_SIZE}MB.`
)
.refine(
(files) => ACCEPTED_TYPES.includes(files?.[0]?.mimetype),
`Only ${ACCEPTED_TYPES.join(", ")} files are accepted.`
),
id: z.number(),
format: z.nativeEnum(ArchivedFormat),
});
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(),
icon: z.string().trim().max(50).optional(),
iconWeight: z.string().trim().max(50).optional(),
parentId: z.number().optional(),
});
export type PostCollectionSchemaType = z.infer<typeof PostCollectionSchema>;

View File

@ -80,6 +80,7 @@
"stripe": "^12.13.0",
"tailwind-merge": "^2.3.0",
"vaul": "^0.9.1",
"zod": "^3.23.8",
"zustand": "^4.3.8"
},
"devDependencies": {

View File

@ -11,6 +11,7 @@ import fs from "fs";
import verifyToken from "@/lib/api/verifyToken";
import generatePreview from "@/lib/api/generatePreview";
import createFolder from "@/lib/api/storage/createFolder";
import { UploadFileSchema } from "@/lib/shared/schemaValidation";
export const config = {
api: {
@ -138,6 +139,20 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
"image/jpeg",
];
const dataValidation = UploadFileSchema.safeParse({
id: Number(req.query.linkId),
format: Number(req.query.format),
file: files.file,
});
if (!dataValidation.success) {
return res.status(400).json({
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
});
}
if (
err ||
!files.file ||

View File

@ -1,5 +1,6 @@
import { prisma } from "@/lib/api/db";
import sendPasswordResetRequest from "@/lib/api/sendPasswordResetRequest";
import { ForgotPasswordSchema } from "@/lib/shared/schemaValidation";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function forgotPassword(
@ -13,14 +14,18 @@ export default async function forgotPassword(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const email = req.body.email;
const dataValidation = ForgotPasswordSchema.safeParse(req.body);
if (!email) {
if (!dataValidation.success) {
return res.status(400).json({
response: "Invalid email.",
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
});
}
const { email } = dataValidation.data;
const recentPasswordRequestsCount = await prisma.passwordResetToken.count({
where: {
identifier: email,
@ -45,7 +50,7 @@ export default async function forgotPassword(
if (!user || !user.email) {
return res.status(400).json({
response: "Invalid email.",
response: "No user found with that email.",
});
}

View File

@ -1,6 +1,7 @@
import { prisma } from "@/lib/api/db";
import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt";
import { ResetPasswordSchema } from "@/lib/shared/schemaValidation";
export default async function resetPassword(
req: NextApiRequest,
@ -13,20 +14,17 @@ export default async function resetPassword(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const token = req.body.token;
const password = req.body.password;
const dataValidation = ResetPasswordSchema.safeParse(req.body);
if (!password || password.length < 8) {
if (!dataValidation.success) {
return res.status(400).json({
response: "Password must be at least 8 characters.",
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
});
}
if (!token || typeof token !== "string") {
return res.status(400).json({
response: "Invalid token.",
});
}
const { token, password } = dataValidation.data;
// Hashed password
const saltRounds = 10;

View File

@ -1,5 +1,6 @@
import { prisma } from "@/lib/api/db";
import updateCustomerEmail from "@/lib/api/updateCustomerEmail";
import { VerifyEmailSchema } from "@/lib/shared/schemaValidation";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function verifyEmail(
@ -13,14 +14,18 @@ export default async function verifyEmail(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const token = req.query.token;
const dataValidation = VerifyEmailSchema.safeParse(req.query);
if (!token || typeof token !== "string") {
if (!dataValidation.success) {
return res.status(400).json({
response: "Invalid token.",
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
});
}
const { token } = dataValidation.data;
// Check token in db
const verifyToken = await prisma.verificationToken.findFirst({
where: {

View File

@ -1,12 +1,23 @@
import type { NextApiRequest, NextApiResponse } from "next";
import verifyByCredentials from "@/lib/api/verifyByCredentials";
import createSession from "@/lib/api/controllers/session/createSession";
import { PostSessionSchema } from "@/lib/shared/schemaValidation";
export default async function session(
req: NextApiRequest,
res: NextApiResponse
) {
const { username, password, sessionName } = req.body;
const dataValidation = PostSessionSchema.safeParse(req.body);
if (!dataValidation.success) {
return res.status(400).json({
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
});
}
const { username, password, sessionName } = dataValidation.data;
const user = await verifyByCredentials({ username, password });

View File

@ -17,7 +17,7 @@ export default async function tokens(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const token = await postToken(JSON.parse(req.body), user.id);
const token = await postToken(req.body, user.id);
return res.status(token.status).json({ response: token.response });
} else if (req.method === "GET") {
const token = await getTokens(user.id);

View File

@ -113,6 +113,12 @@ export default function Account() {
}
);
if (user.locale !== account.locale) {
setTimeout(() => {
location.reload();
}, 1000);
}
setSubmitLoader(false);
};

View File

@ -393,5 +393,6 @@
"change_icon": "Change Icon",
"upload_preview_image": "Upload Preview Image",
"columns": "Columns",
"default": "Default"
"default": "Default",
"invalid_url_guide":"Please enter a valid Address for the Link. (It should start with http/https)"
}

View File

@ -6511,6 +6511,11 @@ zod@3.21.4:
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zustand@^4.3.8:
version "4.3.8"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4"