From 639d35986419e307c51313de74da1d6d99d1e312 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Fri, 7 Feb 2025 19:56:24 +1100 Subject: [PATCH] Validate post and page creation payload --- apps/page/components/subscribe-prompt.tsx | 4 +- .../components/forms/page-form.component.tsx | 32 ++++---------- .../components/forms/post-form.component.tsx | 27 +----------- .../layout/blog-layout.component.tsx | 8 ++-- apps/web/data/schema.ts | 42 +++++++++++++++++++ apps/web/pages/api/pages/new.ts | 14 +++++++ apps/web/pages/api/posts/index.ts | 21 ++++++++++ apps/web/pages/pages/[page_id]/[post_id].tsx | 2 +- apps/web/pages/pages/[page_id]/edit.tsx | 2 +- apps/web/pages/pages/[page_id]/new.tsx | 2 +- apps/web/pages/pages/new.tsx | 2 +- apps/web/utils/useDatabase.ts | 4 ++ 12 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 apps/web/data/schema.ts diff --git a/apps/page/components/subscribe-prompt.tsx b/apps/page/components/subscribe-prompt.tsx index 35837bd..5f65375 100644 --- a/apps/page/components/subscribe-prompt.tsx +++ b/apps/page/components/subscribe-prompt.tsx @@ -21,7 +21,7 @@ export default function SubscribePrompt({ const pageUrl = useMemo(() => getPageUrl(page, settings), [page, settings]); - const NewPageSchema = Yup.object().shape({ + const InputSchema = Yup.object().shape({ email: Yup.string().email().required("Enter a valid email"), }); @@ -29,7 +29,7 @@ export default function SubscribePrompt({ initialValues: { email: "", }, - validationSchema: NewPageSchema, + validationSchema: InputSchema, onSubmit: async (values) => { setLoading(true); try { diff --git a/apps/web/components/forms/page-form.component.tsx b/apps/web/components/forms/page-form.component.tsx index 90fd2a0..b5f962a 100644 --- a/apps/web/components/forms/page-form.component.tsx +++ b/apps/web/components/forms/page-form.component.tsx @@ -1,35 +1,19 @@ +import { + IPage, + PageType, + PageTypeToLabel +} from "@changes-page/supabase/types/page"; +import { Spinner, SpinnerWithSpacing } from "@changes-page/ui"; import classNames from "classnames"; import { FormikProps, useFormik } from "formik"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import slugify from "slugify"; -import { InferType, object, string } from "yup"; -import { - IPage, - PageType, - PageTypeToLabel, - URL_SLUG_REGEX, -} from "@changes-page/supabase/types/page"; +import { InferType } from "yup"; +import { NewPageSchema } from "../../data/schema"; import { PrimaryButton, SecondaryButton } from "../core/buttons.component"; -import { Spinner, SpinnerWithSpacing } from "@changes-page/ui"; import { InfoMessage, InlineErrorMessage } from "./notification.component"; -export const NewPageSchema = object().shape({ - url_slug: string() - .min(4, "Too Short!") - .max(24, "Too Long!") - .required("Enter a valid url") - .matches(URL_SLUG_REGEX, "Enter a valid url"), - title: string() - .required("Enter a valid title") - .min(2, "Title too Short!") - .max(50, "Title too Long!"), - description: string() - .min(2, "Description too Short!") - .max(500, "Description too Long!"), - type: string().required("Enter a valid type"), -}); - export type PageFormikForm = FormikProps>; export default function PageFormComponent({ diff --git a/apps/web/components/forms/post-form.component.tsx b/apps/web/components/forms/post-form.component.tsx index a1e0ff1..4f2bf64 100644 --- a/apps/web/components/forms/post-form.component.tsx +++ b/apps/web/components/forms/post-form.component.tsx @@ -19,7 +19,8 @@ import { useFormik } from "formik"; import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; import ReactMarkdown from "react-markdown"; import { v4 } from "uuid"; -import { InferType, array, boolean, mixed, object, string } from "yup"; +import { InferType } from "yup"; +import { NewPostSchema } from "../../data/schema"; import { track } from "../../utils/analytics"; import { useUserData } from "../../utils/useUser"; import { PrimaryButton } from "../core/buttons.component"; @@ -31,30 +32,6 @@ import AiSuggestTitlePromptDialogComponent from "../dialogs/ai-suggest-title-pro import DateTimePromptDialog from "../dialogs/date-time-prompt-dialog.component"; import SwitchComponent from "./switch.component"; -export const NewPostSchema = object().shape({ - title: string() - .required("Title cannot be empty") - .min(2, "Title too Short!") - .max(75, "Title too Long!"), - content: string() - .required("Content cannot be empty") - .min(2, "Content too Short!") - .max(9669, "Content too Long!"), - tags: array() - .of(mixed().oneOf(Object.values(PostType))) - .required("Enter valid tags"), - status: mixed() - .oneOf(Object.values(PostStatus)) - .required("Enter valid status"), - page_id: string(), - images_folder: string(), - publish_at: string().optional().nullable(), - publication_date: string().optional().nullable(), - allow_reactions: boolean(), - email_notified: boolean(), - notes: string().optional().nullable(), -}); - export type PostFormikForm = InferType; export default function PostFormComponent({ diff --git a/apps/web/components/layout/blog-layout.component.tsx b/apps/web/components/layout/blog-layout.component.tsx index 167dd35..66a7cfb 100644 --- a/apps/web/components/layout/blog-layout.component.tsx +++ b/apps/web/components/layout/blog-layout.component.tsx @@ -1,4 +1,5 @@ import { DateTime } from "@changes-page/utils"; +import { ArrowLeftIcon } from "@heroicons/react/solid"; import classNames from "classnames"; import Head from "next/head"; import Image from "next/image"; @@ -146,9 +147,10 @@ export default function BlogLayout({

- Blog + {" "} + Back to blog {title} @@ -198,7 +200,7 @@ export default function BlogLayout({ /> ) : null} -
+
().oneOf(Object.values(PostType))) + .required("Enter valid tags"), + status: mixed() + .oneOf(Object.values(PostStatus)) + .required("Enter valid status"), + page_id: string(), + images_folder: string(), + publish_at: string().optional().nullable(), + publication_date: string().optional().nullable(), + allow_reactions: boolean(), + email_notified: boolean(), + notes: string().optional().nullable(), +}); \ No newline at end of file diff --git a/apps/web/pages/api/pages/new.ts b/apps/web/pages/api/pages/new.ts index 2823ef5..ee05531 100644 --- a/apps/web/pages/api/pages/new.ts +++ b/apps/web/pages/api/pages/new.ts @@ -1,6 +1,7 @@ import { IErrorResponse } from "@changes-page/supabase/types/api"; import { IPage } from "@changes-page/supabase/types/page"; import type { NextApiRequest, NextApiResponse } from "next"; +import { NewPageSchema } from "../../../data/schema"; import { apiRateLimiter } from "../../../utils/rate-limit"; import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; import { @@ -28,6 +29,19 @@ const createNewPage = async ( }); } + const isValid = await NewPageSchema.isValid({ + url_slug, + title: title.trim(), + description: description.trim(), + type, + }); + + if (!isValid) { + return res.status(400).json({ + error: { statusCode: 400, message: "Invalid request body" }, + }); + } + console.log("createNewPage", user?.id); const data = await createPage({ diff --git a/apps/web/pages/api/posts/index.ts b/apps/web/pages/api/posts/index.ts index cb893eb..d598a7b 100644 --- a/apps/web/pages/api/posts/index.ts +++ b/apps/web/pages/api/posts/index.ts @@ -1,5 +1,6 @@ import { PostStatus } from "@changes-page/supabase/types/page"; import { NextApiRequest, NextApiResponse } from "next"; +import { NewPostSchema } from "../../../data/schema"; import { apiRateLimiter } from "../../../utils/rate-limit"; import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; import { createPost, getUserById } from "../../../utils/useDatabase"; @@ -32,6 +33,26 @@ const createNewPost = async (req: NextApiRequest, res: NextApiResponse) => { }); } + const isValid = await NewPostSchema.isValid({ + page_id, + title: title.trim(), + content: content.trim(), + tags, + status, + images_folder, + publish_at, + notes, + allow_reactions, + email_notified, + publication_date, + }); + + if (!isValid) { + return res.status(400).json({ + error: { statusCode: 400, message: "Invalid request body" }, + }); + } + console.log("createNewPost", user?.id); const post = await createPost({ diff --git a/apps/web/pages/pages/[page_id]/[post_id].tsx b/apps/web/pages/pages/[page_id]/[post_id].tsx index 00a4d7e..d03bfac 100644 --- a/apps/web/pages/pages/[page_id]/[post_id].tsx +++ b/apps/web/pages/pages/[page_id]/[post_id].tsx @@ -5,12 +5,12 @@ import { useState } from "react"; import { InferType } from "yup"; import { notifyError } from "../../../components/core/toast.component"; import PostFormComponent, { - NewPostSchema, PostFormikForm, } from "../../../components/forms/post-form.component"; import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; +import { NewPostSchema } from "../../../data/schema"; import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../utils/useDatabase"; import { useUserData } from "../../../utils/useUser"; diff --git a/apps/web/pages/pages/[page_id]/edit.tsx b/apps/web/pages/pages/[page_id]/edit.tsx index aedde5d..ff8d7b2 100644 --- a/apps/web/pages/pages/[page_id]/edit.tsx +++ b/apps/web/pages/pages/[page_id]/edit.tsx @@ -5,12 +5,12 @@ import { useState } from "react"; import { InferType } from "yup"; import { notifyError } from "../../../components/core/toast.component"; import PageFormComponent, { - NewPageSchema, PageFormikForm, } from "../../../components/forms/page-form.component"; import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; +import { NewPageSchema } from "../../../data/schema"; import { httpPost } from "../../../utils/http"; import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; import { getPage } from "../../../utils/useSSR"; diff --git a/apps/web/pages/pages/[page_id]/new.tsx b/apps/web/pages/pages/[page_id]/new.tsx index 902c812..fecb9c5 100644 --- a/apps/web/pages/pages/[page_id]/new.tsx +++ b/apps/web/pages/pages/[page_id]/new.tsx @@ -5,12 +5,12 @@ import { useState } from "react"; import { InferType } from "yup"; import { notifyError } from "../../../components/core/toast.component"; import PostFormComponent, { - NewPostSchema, PostFormikForm, } from "../../../components/forms/post-form.component"; import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; +import { NewPostSchema } from "../../../data/schema"; import { track } from "../../../utils/analytics"; import { httpPost } from "../../../utils/http"; import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; diff --git a/apps/web/pages/pages/new.tsx b/apps/web/pages/pages/new.tsx index b40761e..cc57490 100644 --- a/apps/web/pages/pages/new.tsx +++ b/apps/web/pages/pages/new.tsx @@ -3,12 +3,12 @@ import { useState } from "react"; import { InferType } from "yup"; import { notifyError } from "../../components/core/toast.component"; import PageFormComponent, { - NewPageSchema, PageFormikForm, } from "../../components/forms/page-form.component"; import AuthLayout from "../../components/layout/auth-layout.component"; import Page from "../../components/layout/page.component"; import { ROUTES } from "../../data/routes.data"; +import { NewPageSchema } from "../../data/schema"; import { track } from "../../utils/analytics"; import { httpPost } from "../../utils/http"; diff --git a/apps/web/utils/useDatabase.ts b/apps/web/utils/useDatabase.ts index 705f1d1..babb1a2 100644 --- a/apps/web/utils/useDatabase.ts +++ b/apps/web/utils/useDatabase.ts @@ -146,6 +146,10 @@ export const updateSubscriptionUsage = async ( ) => { const user = await getUserById(user_id); + if (user.pro_gifted) { + return false; + } + if (!user.stripe_customer_id || !user.stripe_subscription_id) { return false; }