added zod for post requests
This commit is contained in:
parent
a5b1952e0d
commit
1cf7421b76
|
@ -7,7 +7,7 @@ import { useTags } from "@/hooks/store/tags";
|
|||
type Props = {
|
||||
onChange: any;
|
||||
defaultValue?: {
|
||||
value: number;
|
||||
value?: number;
|
||||
label: string;
|
||||
}[];
|
||||
autoFocus?: boolean;
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
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) {
|
||||
return {
|
||||
response:
|
||||
"Please enter a valid Address for the Link. (It should start with http/https)",
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
const dataValidation = PostLinkSchema.safeParse(body);
|
||||
|
||||
if (!dataValidation.success) {
|
||||
return {
|
||||
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);
|
||||
|
||||
if (!linkCollection)
|
||||
|
|
|
@ -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 || "",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>;
|
|
@ -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": {
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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.",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -113,6 +113,12 @@ export default function Account() {
|
|||
}
|
||||
);
|
||||
|
||||
if (user.locale !== account.locale) {
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setSubmitLoader(false);
|
||||
};
|
||||
|
||||
|
|
|
@ -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)"
|
||||
}
|
|
@ -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"
|
||||
|
|
Ŝarĝante…
Reference in New Issue