From 20001c18730da4c6232194ad4831c3930b5cc475 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Thu, 30 Jun 2022 20:47:40 +0100 Subject: [PATCH] =?UTF-8?q?fix=C2=B4:=20crossbell=20renovation=20-=20remov?= =?UTF-8?q?e=20useless=20files=20and=20fix=20build=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/EmailPostModal.tsx | 57 ----- src/components/common/LoginModal.tsx | 96 -------- src/components/common/SubscribeModal.tsx | 125 ----------- src/components/dashboard/AvatarForm.tsx | 21 +- src/components/dashboard/DashboardLayout.tsx | 1 - src/components/dashboard/PagesManager.tsx | 1 - src/components/dashboard/SiteSwitcher.tsx | 1 - src/components/main/MainLayout.tsx | 2 - src/components/site/SiteLayout.tsx | 4 +- src/components/site/SitePage.tsx | 16 +- src/hooks/useUploadFile.ts | 7 +- src/lib/env.ts | 3 +- src/lib/mailgun.server.ts | 209 ------------------ src/lib/trpc.ts | 32 --- src/models/page.model.ts | 1 - src/models/site.model.ts | 1 - src/pages/_app.tsx | 5 +- src/pages/_site/[site]/[page].tsx | 5 +- src/pages/_site/[site]/archives.tsx | 2 - src/pages/_site/[site]/index.tsx | 2 - src/pages/api/login-complete.ts | 70 ------ src/pages/api/login.ts | 131 ----------- src/pages/api/logout.ts | 14 -- src/pages/api/trpc/[trpc].ts | 10 - src/pages/dashboard/[subdomain]/editor.tsx | 2 - .../[subdomain]/settings/general.tsx | 3 +- .../[subdomain]/settings/navigation.tsx | 1 - src/pages/dashboard/new-site.tsx | 1 - src/router/auth.ts | 36 --- src/router/index.ts | 59 ----- src/router/membership.ts | 23 -- src/router/page.ts | 37 ---- src/router/site.ts | 205 ----------------- src/router/user.ts | 62 ------ 34 files changed, 19 insertions(+), 1226 deletions(-) delete mode 100644 src/components/common/EmailPostModal.tsx delete mode 100644 src/components/common/LoginModal.tsx delete mode 100644 src/components/common/SubscribeModal.tsx delete mode 100644 src/lib/mailgun.server.ts delete mode 100644 src/lib/trpc.ts delete mode 100644 src/pages/api/login-complete.ts delete mode 100644 src/pages/api/login.ts delete mode 100644 src/pages/api/logout.ts delete mode 100644 src/pages/api/trpc/[trpc].ts delete mode 100644 src/router/auth.ts delete mode 100644 src/router/index.ts delete mode 100644 src/router/membership.ts delete mode 100644 src/router/page.ts delete mode 100644 src/router/site.ts delete mode 100644 src/router/user.ts diff --git a/src/components/common/EmailPostModal.tsx b/src/components/common/EmailPostModal.tsx deleted file mode 100644 index 6fffc3d0..00000000 --- a/src/components/common/EmailPostModal.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { PageEmailStatus } from "@prisma/client" -import { useForm } from "react-hook-form" -import toast from "react-hot-toast" -import { useStore } from "~/lib/store" -import { trpc } from "~/lib/trpc" -import { Button } from "../ui/Button" -import { Input } from "../ui/Input" -import { Modal } from "../ui/Modal" - -export const EmailPostModal: React.FC<{ - pageId: string -}> = ({ pageId }) => { - const [open, setOpen] = useStore((store) => [ - store.emailPostModalOpened, - store.setEmailPostModalOpened, - ]) - - const { handleSubmit, register } = useForm({ - defaultValues: { - subject: "", - }, - }) - const scheduleEmailForPost = trpc.useMutation("site.scheduleEmailForPost") - - const onSubmit = handleSubmit(async (values) => { - await scheduleEmailForPost.mutateAsync({ - pageId, - emailSubject: values.subject, - }) - toast.success("Scheduled!") - setOpen(false) - }) - - return ( - - { -
-
- -
-
- -
-
- } -
- ) -} diff --git a/src/components/common/LoginModal.tsx b/src/components/common/LoginModal.tsx deleted file mode 100644 index 786740ec..00000000 --- a/src/components/common/LoginModal.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Button } from "~/components/ui/Button" -import { useStore } from "~/lib/store" -import { trpc } from "~/lib/trpc" -import { useForm } from "react-hook-form" -import { Modal } from "../ui/Modal" -import { UniLink } from "../ui/UniLink" -import { DOCS_DOMAIN } from "~/lib/env" - -export const LoginModal: React.FC = () => { - const [loginModalOpened, setLoginModalOpened] = useStore((store) => [ - store.loginModalOpened, - store.setLoginModalOpened, - ]) - const { - mutate: requestLoginLink, - status: requestLoginLinkStatus, - data, - error, - } = trpc.useMutation("auth.requestLoginLink") - - const form = useForm({ - defaultValues: { - email: "", - }, - }) - - const handleSubmit = form.handleSubmit((values) => { - requestLoginLink({ - email: values.email, - url: location.href, - }) - }) - - return ( - -
- {data && ( -
- We just emailed you with a link to log in, please check your inbox - and spam folder in case you can{`'`}t find it. -
- )} - {error &&
{error.message}
} -
-
- - -
-
- -
-
- By clicking Continue, you agree to our{" "} - - Terms of Service - {" "} - and{" "} - - Privacy Policy - - . -
-
-
-
- ) -} - -export default LoginModal diff --git a/src/components/common/SubscribeModal.tsx b/src/components/common/SubscribeModal.tsx deleted file mode 100644 index 55ef0752..00000000 --- a/src/components/common/SubscribeModal.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useEffect } from "react" -import { useStore } from "~/lib/store" -import { Button } from "../ui/Button" -import { Modal } from "../ui/Modal" -import toast from "react-hot-toast" -import { trpc } from "~/lib/trpc" -import { Input } from "../ui/Input" -import { useForm } from "react-hook-form" -import { useRouter } from "next/router" - -export const SubscribeModal: React.FC<{ - siteId: string - subscription?: { - email?: boolean - } | null - isLoggedIn: boolean -}> = ({ siteId, subscription, isLoggedIn }) => { - const router = useRouter() - const [open, setOpen] = useStore((store) => [ - store.subscribeModalOpened, - store.setSubscribeModalOpened, - ]) - const subscribe = trpc.useMutation("site.subscribe") - const unsubscribe = trpc.useMutation("site.unsubscribe") - const trpcContext = trpc.useContext() - - const subscribeForm = useForm({ - defaultValues: { - newUserEmail: "", - email: subscription?.email ?? true, - }, - }) - - const handleSubscribe = subscribeForm.handleSubmit((values) => { - subscribe.mutate({ - siteId, - email: values.email, - newUser: isLoggedIn - ? undefined - : { - email: values.newUserEmail, - url: location.href, - }, - }) - }) - - useEffect(() => { - if (subscribe.isSuccess && isLoggedIn) { - subscribe.reset() - toast.success(subscription ? "Updated!" : "Subscribed!") - trpcContext.invalidateQueries("site.subscription") - } - }, [subscribe, isLoggedIn, subscription, trpcContext]) - - useEffect(() => { - if (unsubscribe.isSuccess) { - unsubscribe.reset() - toast.success("Unsubscribed!") - trpcContext.invalidateQueries("site.subscription") - } - }, [unsubscribe, trpcContext]) - - return ( - { - setOpen(open) - if (!open && "subscription" in router.query) { - const query = new URLSearchParams(window.location.search) - query.delete("subscription") - const search = query.toString() - router.replace( - `${window.location.pathname}${search ? `?${search}` : ""}`, - ) - } - }} - > - {subscribe.isSuccess && !isLoggedIn ? ( -
-

- We have sent you an email with a link to confirm your subscription. -

-

Please check your inbox (and spam folder).

-
- ) : ( -
- {!isLoggedIn && ( -
- -
- )} -
- -
-
- - {subscription && ( - - )} -
-
- )} -
- ) -} diff --git a/src/components/dashboard/AvatarForm.tsx b/src/components/dashboard/AvatarForm.tsx index 1aff7102..76e50ace 100644 --- a/src/components/dashboard/AvatarForm.tsx +++ b/src/components/dashboard/AvatarForm.tsx @@ -6,21 +6,18 @@ import { Avatar } from "~/components/ui/Avatar" import { Button } from "~/components/ui/Button" import toast from "react-hot-toast" import createPica from "pica" -import { trpc } from "~/lib/trpc" import { UploadFile, useUploadFile } from "~/hooks/useUploadFile" +import { updateSite } from "~/models/site.model" const AvatarEditorModal: React.FC<{ isOpen: boolean image?: File | null setIsOpen: (open: boolean) => void - site?: string + site: string uploadFile: UploadFile }> = ({ isOpen, setIsOpen, image, site, uploadFile }) => { const editorRef = useRef(null) const [isSaving, setIsSaving] = useState(false) - const ctx = trpc.useContext() - const updateProfile = trpc.useMutation("user.updateProfile") - const updateSite = trpc.useMutation("site.updateSite") const cropAndSave = async () => { if (!editorRef.current) return @@ -41,15 +38,15 @@ const AvatarEditorModal: React.FC<{ const { key } = await uploadFile(blob, image!.name) // Save the image to profile / site - if (site) { - await updateSite.mutateAsync({ site, icon: key }) + const res = await updateSite({ site, icon: key }) + + if (res.code === 0) { + setIsOpen(false) + toast.success("Updated!") } else { - await updateProfile.mutateAsync({ avatar: key }) + toast.error("Failed to update site" + ": " + res.message) } - setIsOpen(false) - toast.success("Updated!") - ctx.invalidateQueries() } catch (error: any) { console.error(error) toast.error(error.message) @@ -97,7 +94,7 @@ const AvatarEditorModal: React.FC<{ export const AvatarForm: React.FC<{ filename: string | undefined | null name: string - site?: string + site: string }> = ({ filename, name, site }) => { const [isOpen, setIsOpen] = useState(false) const inputEl = useRef(null) diff --git a/src/components/dashboard/DashboardLayout.tsx b/src/components/dashboard/DashboardLayout.tsx index c831a99c..9b46d7f8 100644 --- a/src/components/dashboard/DashboardLayout.tsx +++ b/src/components/dashboard/DashboardLayout.tsx @@ -4,7 +4,6 @@ import { useRouter } from "next/router" import React, { useState, useEffect } from "react" import { APP_NAME } from "~/lib/env" import { getSiteLink } from "~/lib/helpers" -import { trpc } from "~/lib/trpc" import { SEOHead } from "../common/SEOHead" import { DashboardIcon } from "../icons/DashboardIcon" import { UniLink } from "../ui/UniLink" diff --git a/src/components/dashboard/PagesManager.tsx b/src/components/dashboard/PagesManager.tsx index b1945ea2..ab76b6c7 100644 --- a/src/components/dashboard/PagesManager.tsx +++ b/src/components/dashboard/PagesManager.tsx @@ -7,7 +7,6 @@ import clsx from "clsx" import { PageVisibilityEnum } from "~/lib/types" import { DashboardMain } from "./DashboardMain" import { useRouter } from "next/router" -import { trpc } from "~/lib/trpc" import Link from "next/link" import toast from "react-hot-toast" import { EmptyState } from "../ui/EmptyState" diff --git a/src/components/dashboard/SiteSwitcher.tsx b/src/components/dashboard/SiteSwitcher.tsx index f0bdceea..e746dcf9 100644 --- a/src/components/dashboard/SiteSwitcher.tsx +++ b/src/components/dashboard/SiteSwitcher.tsx @@ -1,7 +1,6 @@ import { Popover } from "@headlessui/react" import Link from "next/link" import { useEffect, useMemo } from "react" -import { trpc } from "~/lib/trpc" import { getUserContentsUrl } from "~/lib/user-contents" import { Avatar } from "../ui/Avatar" import type { Profile } from "unidata.js" diff --git a/src/components/main/MainLayout.tsx b/src/components/main/MainLayout.tsx index 40cc7b26..3d904612 100644 --- a/src/components/main/MainLayout.tsx +++ b/src/components/main/MainLayout.tsx @@ -1,4 +1,3 @@ -import { logout } from "~/lib/auth.client" import { APP_DESCRIPTION, APP_NAME, @@ -18,7 +17,6 @@ export function MainLayout({ children?: React.ReactNode title?: string }) { - const setLoginModalOpened = useStore((store) => store.setLoginModalOpened) const discordLink = `https://${OUR_DOMAIN}/discord` const companyLinks = [ { text: "Blog", href: `https://blog.${OUR_DOMAIN}` }, diff --git a/src/components/site/SiteLayout.tsx b/src/components/site/SiteLayout.tsx index 218786cc..97e82ca3 100644 --- a/src/components/site/SiteLayout.tsx +++ b/src/components/site/SiteLayout.tsx @@ -1,14 +1,12 @@ import clsx from "clsx" import React, { useEffect } from "react" import { getUserContentsUrl } from "~/lib/user-contents" -import { SubscribeModal } from "../common/SubscribeModal" import { SEOHead } from "../common/SEOHead" -import { SiteNavigationItem, Viewer } from "~/lib/types" +import { SiteNavigationItem, Viewer, Profile } from "~/lib/types" import { SiteFooter } from "./SiteFooter" import { SiteHeader } from "./SiteHeader" import { useRouter } from "next/router" import { useStore } from "~/lib/store" -import { Profile } from "unidata.js" export type SiteLayoutProps = { site: Profile diff --git a/src/components/site/SitePage.tsx b/src/components/site/SitePage.tsx index d1debd6b..04ac7e94 100644 --- a/src/components/site/SitePage.tsx +++ b/src/components/site/SitePage.tsx @@ -2,23 +2,11 @@ import type { PageType } from "~/lib/db.server" import { Rendered } from "~/markdown" import { PageContent } from "../common/PageContent" import { PostMeta } from "./PostMeta" -import { Profile } from "unidata.js" +import { Profile, Note } from "~/lib/types" export const SitePage: React.FC<{ site: Profile, - page: { - tags?: string[] - date_published: string - title?: string - related_urls?: string[] - authors?: string[] - body?: { - content?: string - } - metadata?: { - owner?: string - } - } + page: Note }> = ({ site, page }) => { return ( <> diff --git a/src/hooks/useUploadFile.ts b/src/hooks/useUploadFile.ts index 91b1bec6..dbb262ec 100644 --- a/src/hooks/useUploadFile.ts +++ b/src/hooks/useUploadFile.ts @@ -1,19 +1,18 @@ import { useCallback } from "react" -import { WEB3_STORAGE_API_TOKEN } from "~/lib/env" import { Web3Storage } from 'web3.storage' export const useUploadFile = () => { const uploadFile = useCallback( async (blob, filename) => { - const file = new File([blob], filename); + const file = new File([blob], filename) const web3Storage = new Web3Storage({ - token: WEB3_STORAGE_API_TOKEN, + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDAyMDIwODZmRjU5OUU0Y0YyMzM4MkUzNjg1Y0NmZUEyOGNBODBCOTAiLCJpc3MiOiJ3ZWIzLXN0b3JhZ2UiLCJpYXQiOjE2NTIzNjM1Njk3NDUsIm5hbWUiOiJVbmlkYXRhIn0.XmsAuXvbTj4BFhZlJK4xXfbd0ltVZJCEhqdYcW_kLOo', } as any) const cid = await web3Storage.put([file], { name: file.name, maxRetries: 3, wrapWithDirectory: false, - }); + }) return { key: `https://gateway.ipfs.io/ipfs/${cid}` } diff --git a/src/lib/env.ts b/src/lib/env.ts index 4d1e92ae..060cf63b 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -7,5 +7,4 @@ export const SITE_URL = `${IS_PROD ? "https" : "http"}://${OUR_DOMAIN}` export const R2_URL = process.env.NEXT_PUBLIC_R2_URL export const APP_DESCRIPTION = process.env.APP_DESCRIPTION export const DISCORD_LINK = process.env.NEXT_PUBLIC_DISCORD_LINK -export const GITHUB_LINK = process.env.NEXT_PUBLIC_GITHUB_LINK -export const WEB3_STORAGE_API_TOKEN = process.env.WEB3_STORAGE_API_TOKEN \ No newline at end of file +export const GITHUB_LINK = process.env.NEXT_PUBLIC_GITHUB_LINK \ No newline at end of file diff --git a/src/lib/mailgun.server.ts b/src/lib/mailgun.server.ts deleted file mode 100644 index 2f094cd2..00000000 --- a/src/lib/mailgun.server.ts +++ /dev/null @@ -1,209 +0,0 @@ -import Mailgun from "mailgun.js" -import FormData from "form-data" -import { singleton } from "./singleton.server" -import { - MAILGUN_APIKEY, - MAILGUN_DOMAIN_NEWSLETTER, - MAILGUN_DOMAIN_TRANSANCTION, -} from "~/lib/env.server" -import type { MailgunMessageData } from "mailgun.js/interfaces/Messages" -import { IS_PROD } from "./constants" -import { APP_NAME, OUR_DOMAIN, SITE_URL } from "./env" -import { SubscribeFormData } from "./types" -import { getSite } from "~/models/site.model" -import { type Site } from "~/lib/db.server" -import { getSiteLink } from "./helpers" -import { generateLoginToken } from "./token.server" - -enum MAIL_STREAM { - NEWSLETTER = "newsletter", - TRANSANCTION = "transaction", -} - -const enableMailgun = Boolean( - MAILGUN_APIKEY && MAILGUN_DOMAIN_TRANSANCTION && MAILGUN_DOMAIN_NEWSLETTER, -) - -const getClient = () => - singleton("mailgun", () => { - const mg = new Mailgun(FormData) - const client = mg.client({ - username: "api", - key: MAILGUN_APIKEY, - timeout: 60000, - }) - return client - }) - -const sendEmail = async (message: MailgunMessageData, stream: MAIL_STREAM) => { - console.log(message) - - if (!enableMailgun) { - console.error( - "not sending email because no mailgun apikey or domain configured", - ) - return - } - - const client = getClient() - - await client.messages - .create( - stream === MAIL_STREAM.NEWSLETTER - ? MAILGUN_DOMAIN_NEWSLETTER! - : MAILGUN_DOMAIN_TRANSANCTION!, - message, - ) - .then(console.log) -} - -const sendTransanctionEmail = async (message: MailgunMessageData) => - sendEmail(message, MAIL_STREAM.TRANSANCTION) - -const sendNewsletterEmail = async (message: MailgunMessageData) => - sendEmail(message, MAIL_STREAM.NEWSLETTER) - -export const sendLoginEmail = async (payload: { - email: string - url: string - toSubscribeSiteId?: string -}) => { - const { protocol, host, pathname } = new URL(payload.url) - const token = await generateLoginToken( - payload.toSubscribeSiteId - ? { - type: "subscribe", - email: payload.email, - siteId: payload.toSubscribeSiteId, - } - : { - type: "login", - email: payload.email, - }, - ) - const query = new URLSearchParams([ - ["token", token], - ["next", `${protocol}//${host}${pathname}`], - ]) - - const loginLink = `${ - IS_PROD ? "https" : "http" - }://${OUR_DOMAIN}/api/login?${query.toString()}` - - let subject = `Sign in to ${APP_NAME}` - - let html = ` -

Hello,

- -

We received a request to sign in to ${APP_NAME} using this email address, please click the link below to sign in:

- - Sign in to ${APP_NAME} - -

This link will expire in 10 minutes. If you did not request this link, you can safely ignore this email.

- -

Thanks,

- -

Your Proselog Team

- ` - - if (payload.toSubscribeSiteId) { - const site = await getSite(payload.toSubscribeSiteId) - subject = `Confirm your subscription to ${site.name}` - html = ` -

Please confirm your subscription to "${site.name}" by clicking the link below:

- - confirm - -

This link will expire in 7 days. If you did not request this link, you can safely ignore this email.

` - } - - const message: MailgunMessageData = { - from: `${APP_NAME} `, - to: payload.email, - subject, - html, - } - - await sendTransanctionEmail(message) -} - -export const sendEmailForNewPost = async (payload: { - post: { - emailSubject?: string | null - slug: string - title: string - rendered: { contentHTML: string } - } - site: Site - subscribers: { id: string; email: string }[] -}) => { - if (payload.subscribers.length === 0) return - try { - const from = `${payload.site.name} ` - const subject = - payload.post.emailSubject || - `${payload.post.title} - ${payload.site.name}` - - const siteLink = getSiteLink({ subdomain: payload.site.subdomain }) - const html = ` - - View this post in browser - - -

${payload.post.title}

- -
- ${payload.post.rendered.contentHTML} -
- - -

- - Unsubscribe - -

- -

- - Published on Proselog - -

- ` - - const recipientVariables: { - [email in string]: { unsubscribeToken: string } - } = {} - - await Promise.allSettled( - payload.subscribers.map(async (sub) => { - const unsubscribeToken = await generateLoginToken({ - type: "unsubscribe", - userId: sub.id, - siteId: payload.site.id, - }) - recipientVariables[sub.email] = { - unsubscribeToken, - } - }), - ) - - const message: MailgunMessageData = { - from, - subject, - html, - // TODO: segment by every 800 subscribers - to: Object.keys(recipientVariables), - "recipient-variables": JSON.stringify(recipientVariables), - } - - await sendNewsletterEmail(message) - } catch (error) { - console.error(error) - } -} diff --git a/src/lib/trpc.ts b/src/lib/trpc.ts deleted file mode 100644 index 096014ab..00000000 --- a/src/lib/trpc.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createReactQueryHooks } from "@trpc/react" -import { withTRPC } from "@trpc/next" -import type { AppRouter } from "~/router" - -export const trpc = createReactQueryHooks() - -export const wrapTrpc = ({ ssr }: { ssr?: boolean } = {}) => { - return withTRPC({ - config() { - const url = "/api/trpc" - - return { - url, - fetch(url, init) { - return fetch(url, { - ...init, - credentials: "same-origin", - }) - }, - - queryClientConfig: { - defaultOptions: { - queries: { - // refetchOnWindowFocus: false, - }, - }, - }, - } - }, - ssr, - }) -} diff --git a/src/models/page.model.ts b/src/models/page.model.ts index 2eee6380..ec15738f 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -13,7 +13,6 @@ import { PageVisibilityEnum, Notes } from "~/lib/types" import { isUUID } from "~/lib/uuid" import { getSite } from "./site.model" import { stripHTML } from "~/lib/utils" -import { sendEmailForNewPost } from "~/lib/mailgun.server" import unidata from "~/lib/unidata" const checkPageSlug = async ({ diff --git a/src/models/site.model.ts b/src/models/site.model.ts index 9d007cb8..9a62849c 100644 --- a/src/models/site.model.ts +++ b/src/models/site.model.ts @@ -2,7 +2,6 @@ import { prismaPrimary, prismaRead } from "~/lib/db.server" import { isUUID } from "~/lib/uuid" import { MembershipRole, PageType, type Site } from "~/lib/db.server" import { Gate } from "~/lib/gate.server" -import { sendLoginEmail } from "~/lib/mailgun.server" import { SiteNavigationItem, Profile } from "~/lib/types" import { nanoid } from "nanoid" import { getMembership } from "./membership" diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 80b97cd8..97f1a94c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,9 +1,7 @@ import { Toaster } from "react-hot-toast" -import LoginModal from "~/components/common/LoginModal" import "~/css/main.css" import "~/generated/uno.css" import { StoreProvider, createStore } from "~/lib/store" -import { wrapTrpc } from "~/lib/trpc" import { getDefaultWallets, RainbowKitProvider, @@ -58,7 +56,6 @@ function MyApp({ Component, pageProps }: any) { - @@ -78,4 +75,4 @@ function MyApp({ Component, pageProps }: any) { // return { ...appProps } // } -export default wrapTrpc()(MyApp) +export default MyApp diff --git a/src/pages/_site/[site]/[page].tsx b/src/pages/_site/[site]/[page].tsx index d308be83..a647e21a 100644 --- a/src/pages/_site/[site]/[page].tsx +++ b/src/pages/_site/[site]/[page].tsx @@ -1,17 +1,14 @@ import { GetServerSideProps } from "next" import { SiteLayout } from "~/components/site/SiteLayout" import { getAuthUser } from "~/lib/auth.server" -import { trpc } from "~/lib/trpc" import { createSSGHelpers } from "@trpc/react/ssg" -import { appRouter } from "~/router" import { getTRPCContext } from "~/lib/trpc.server" import { SitePage } from "~/components/site/SitePage" import { serverSidePropsHandler } from "~/lib/server-side-props" import { getViewer } from "~/lib/viewer" -import { Viewer } from "~/lib/types" +import { Viewer, Profile, Note } from "~/lib/types" import { getUserSites, getSite } from "~/models/site.model" import { getPage } from "~/models/page.model" -import { Profile, Note } from "unidata.js" import { PageVisibilityEnum } from "~/lib/types" import { notFound } from "~/lib/server-side-props" diff --git a/src/pages/_site/[site]/archives.tsx b/src/pages/_site/[site]/archives.tsx index 44b117de..bc5f285b 100644 --- a/src/pages/_site/[site]/archives.tsx +++ b/src/pages/_site/[site]/archives.tsx @@ -1,9 +1,7 @@ import { GetServerSideProps } from "next" import { SiteLayout } from "~/components/site/SiteLayout" import { getAuthUser } from "~/lib/auth.server" -import { trpc } from "~/lib/trpc" import { createSSGHelpers } from "@trpc/react/ssg" -import { appRouter } from "~/router" import { getTRPCContext } from "~/lib/trpc.server" import { serverSidePropsHandler } from "~/lib/server-side-props" import { SiteArchives } from "~/components/site/SiteArchives" diff --git a/src/pages/_site/[site]/index.tsx b/src/pages/_site/[site]/index.tsx index 70a9c61b..7692ceae 100644 --- a/src/pages/_site/[site]/index.tsx +++ b/src/pages/_site/[site]/index.tsx @@ -1,9 +1,7 @@ import { GetServerSideProps } from "next" import { SiteHome } from "~/components/site/SiteHome" import { SiteLayout } from "~/components/site/SiteLayout" -import { trpc } from "~/lib/trpc" import { createSSGHelpers } from "@trpc/react/ssg" -import { appRouter } from "~/router" import { getTRPCContext } from "~/lib/trpc.server" import { Viewer, Profile, Notes } from "~/lib/types" import { getViewer } from "~/lib/viewer" diff --git a/src/pages/api/login-complete.ts b/src/pages/api/login-complete.ts deleted file mode 100644 index 9c4d3960..00000000 --- a/src/pages/api/login-complete.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextApiHandler } from "next" -import { z } from "zod" -import { generateCookie } from "~/lib/auth.server" -import { IS_PROD } from "~/lib/constants" -import { prismaPrimary } from "~/lib/db.server" -import { OUR_DOMAIN } from "~/lib/env" - -const pathAndSearch = (pathname: string, search: string) => { - return `${pathname}${search ? `?${search}` : ""}` -} - -const handler: NextApiHandler = async (req, res) => { - const data = z - .object({ - id: z.string(), - pathname: z.string().default("/"), - search: z.string().default(""), - host: z.string(), - }) - .parse({ - id: req.query.id, - pathname: req.query.pathname, - search: req.query.search, - host: req.headers.host, - }) - - const isCustomDomain = data.host && !data.host.endsWith(`.${OUR_DOMAIN}`) - - // Set cookie again for custom domain and subdomain.localhost (because *.localhost in cookie domain doesn't work) - if (isCustomDomain || !IS_PROD) { - const accessToken = await prismaPrimary.accessToken.findUnique({ - where: { - publicId: data.id, - }, - }) - - if ( - !accessToken || - !accessToken.publicIdExpiresAt || - accessToken.publicIdExpiresAt < new Date() - ) { - throw new Error("invalid id or id expired") - } - - await prismaPrimary.accessToken.update({ - where: { - id: accessToken.id, - }, - data: { - publicId: null, - publicIdExpiresAt: null, - }, - }) - - res.setHeader( - "set-cookie", - generateCookie({ - type: "auth", - domain: data.host, - token: accessToken.token, - }), - ) - res.redirect(pathAndSearch(data.pathname, data.search)) - return - } - - res.redirect(pathAndSearch(data.pathname, data.search)) -} - -export default handler diff --git a/src/pages/api/login.ts b/src/pages/api/login.ts deleted file mode 100644 index ec0735a9..00000000 --- a/src/pages/api/login.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { z } from "zod" -import { prismaPrimary } from "~/lib/db.server" -import { nanoid } from "nanoid" -import UAParser from "ua-parser-js" -import dayjs from "dayjs" -import { generateCookie } from "~/lib/auth.server" -import { OUR_DOMAIN } from "~/lib/env" -import { IS_PROD } from "~/lib/constants" -import { NextApiHandler } from "next" -import { getSite, subscribeToSite } from "~/models/site.model" -import { createGate } from "~/lib/gate.server" -import { decryptLoginToken } from "~/lib/token.server" -import { getSiteLink } from "~/lib/helpers" - -const handler: NextApiHandler = async (req, res) => { - const data = z - .object({ - token: z.string(), - next: z.string(), - userAgent: z.string(), - }) - .parse({ - token: req.query.token, - next: req.query.next, - userAgent: req.headers["user-agent"], - }) - - const payload = await decryptLoginToken(data.token) - - let user = await prismaPrimary.user.findUnique({ - where: - payload.type === "unsubscribe" - ? { - id: payload.userId, - } - : { - email: payload.email, - }, - include: { - memberships: true, - }, - }) - - if (!user) { - if (payload.type === "unsubscribe") { - throw new Error("User not found") - } else { - user = await prismaPrimary.user.create({ - data: { - email: payload.email, - name: payload.email.split("@")[0], - username: nanoid(7), - }, - include: { - memberships: true, - }, - }) - } - } - - if (payload.type === "subscribe") { - // Subscribe the user to the site - const gate = createGate({ user }) - await subscribeToSite(gate, { - siteId: payload.siteId, - email: true, - }) - } - - const ua = new UAParser(data.userAgent) - - const publicId = nanoid(32) - const accessToken = await prismaPrimary.accessToken.create({ - data: { - user: { - connect: { - id: user.id, - }, - }, - token: nanoid(32), - name: `Login via ${ua.getOS().name} ${ua.getBrowser().name}`, - publicId, - publicIdExpiresAt: dayjs().add(1, "minute").toDate(), - }, - }) - - if (!IS_PROD) { - console.log("login with token", accessToken.token) - } - - if (payload.type === "unsubscribe") { - const site = await getSite(payload.siteId) - data.next = `${getSiteLink({ - subdomain: site.subdomain, - })}?subscription` - } - - const nextUrl = new URL(data.next) - - // Custom domain - if (nextUrl.host !== OUR_DOMAIN && !nextUrl.host.endsWith(`.${OUR_DOMAIN}`)) { - // Check if the host belong to a site - // const existing = await prismaPrimary.domain.findUnique({ - // where: { - // domain: nextUrl.hostname, - // }, - // }) - // if (!existing) { - // throw new Error("invalid next url") - // } - throw new Error("invalid next url") - } - - nextUrl.searchParams.set("id", publicId) - nextUrl.searchParams.set("pathname", nextUrl.pathname) - nextUrl.pathname = "/api/login-complete" - console.log("redirecting to", nextUrl.href) - - res.setHeader( - "set-cookie", - generateCookie({ - type: "auth", - domain: OUR_DOMAIN, - token: accessToken.token, - }), - ) - - res.redirect(nextUrl.href) -} - -export default handler diff --git a/src/pages/api/logout.ts b/src/pages/api/logout.ts deleted file mode 100644 index 0c07f1b3..00000000 --- a/src/pages/api/logout.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextApiHandler } from "next" -import { generateCookie } from "~/lib/auth.server" - -const handler: NextApiHandler = async (req, res) => { - res.setHeader("set-cookie", generateCookie({ type: "clear" })) - const next = req.query.next as string | undefined - if (next) { - res.redirect(next) - } else { - res.redirect("/") - } -} - -export default handler diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts deleted file mode 100644 index 8acf25c5..00000000 --- a/src/pages/api/trpc/[trpc].ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as trpcNext from "@trpc/server/adapters/next" -import { getTRPCContext } from "~/lib/trpc.server" - -import { appRouter } from "~/router" - -// export API handler -export default trpcNext.createNextApiHandler({ - router: appRouter, - createContext: getTRPCContext, -}) diff --git a/src/pages/dashboard/[subdomain]/editor.tsx b/src/pages/dashboard/[subdomain]/editor.tsx index fb49555f..e6590750 100644 --- a/src/pages/dashboard/[subdomain]/editor.tsx +++ b/src/pages/dashboard/[subdomain]/editor.tsx @@ -20,7 +20,6 @@ import { getPageVisibility } from "~/lib/page-helpers" import { PageVisibilityEnum, Note } from "~/lib/types" import { FieldLabel } from "~/components/ui/FieldLabel" import { Button } from "~/components/ui/Button" -import { EmailPostModal } from "~/components/common/EmailPostModal" import { useStore } from "~/lib/store" import { getPage, createOrUpdatePage } from "~/models/page.model" @@ -286,7 +285,6 @@ export default function SubdomainEditor() { - {pageId && } ) } diff --git a/src/pages/dashboard/[subdomain]/settings/general.tsx b/src/pages/dashboard/[subdomain]/settings/general.tsx index 0d7d71c9..e8b0b917 100644 --- a/src/pages/dashboard/[subdomain]/settings/general.tsx +++ b/src/pages/dashboard/[subdomain]/settings/general.tsx @@ -6,7 +6,6 @@ import toast from "react-hot-toast" import { SettingsLayout } from "~/components/dashboard/SettingsLayout" import { useRouter } from "next/router" import { DashboardLayout } from "~/components/dashboard/DashboardLayout" -import { trpc } from "~/lib/trpc" import { useForm } from "react-hook-form" import { getSite, updateSite } from "~/models/site.model" import { Profile } from "unidata.js" @@ -64,7 +63,7 @@ export default function SiteSettingsGeneralPage() {
diff --git a/src/pages/dashboard/[subdomain]/settings/navigation.tsx b/src/pages/dashboard/[subdomain]/settings/navigation.tsx index 1e3b223d..bb7c6389 100644 --- a/src/pages/dashboard/[subdomain]/settings/navigation.tsx +++ b/src/pages/dashboard/[subdomain]/settings/navigation.tsx @@ -5,7 +5,6 @@ import toast from "react-hot-toast" import { SettingsLayout } from "~/components/dashboard/SettingsLayout" import { useRouter } from "next/router" import { DashboardLayout } from "~/components/dashboard/DashboardLayout" -import { trpc } from "~/lib/trpc" import { SiteNavigationItem, Profile } from "~/lib/types" import { nanoid } from "nanoid" import { ReactSortable } from "react-sortablejs" diff --git a/src/pages/dashboard/new-site.tsx b/src/pages/dashboard/new-site.tsx index f740919e..7d5d9d1b 100644 --- a/src/pages/dashboard/new-site.tsx +++ b/src/pages/dashboard/new-site.tsx @@ -7,7 +7,6 @@ import { SEOHead } from "~/components/common/SEOHead" import { Button } from "~/components/ui/Button" import { Input } from "~/components/ui/Input" import { APP_NAME, OUR_DOMAIN } from "~/lib/env" -import { trpc } from "~/lib/trpc" import { useAccount } from "wagmi" import { createSite } from "~/models/site.model" diff --git a/src/router/auth.ts b/src/router/auth.ts deleted file mode 100644 index 0c216cfb..00000000 --- a/src/router/auth.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { z } from "zod" -import { sendLoginEmail } from "~/lib/mailgun.server" -import { createRouter } from "~/lib/trpc.server" -import { getViewer } from "~/lib/viewer" - -export const authRouter = createRouter() - .query("viewer", { - output: z - .object({ - id: z.string(), - email: z.string(), - name: z.string(), - username: z.string(), - avatar: z.string().nullish(), - bio: z.string().nullish(), - }) - .nullable(), - async resolve({ ctx }) { - return getViewer(ctx.user) - }, - }) - .mutation("requestLoginLink", { - input: z.object({ - email: z.string().nonempty("email is required"), - url: z.string(), - }), - output: z.boolean(), - async resolve({ input }) { - await sendLoginEmail({ - url: input.url, - email: input.email, - }).catch(console.error) - - return true - }, - }) diff --git a/src/router/index.ts b/src/router/index.ts deleted file mode 100644 index e01d97f3..00000000 --- a/src/router/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as trpc from "@trpc/server" -import { z, ZodError } from "zod" -import { isNotFoundError } from "~/lib/server-side-props" -import { TRPCContext } from "~/lib/trpc.server" -import { getSite } from "~/models/site.model" -import { authRouter } from "./auth" -import { membershipRouter } from "./membership" -import { pageRouter } from "./page" -import { siteRouter } from "./site" -import { userRouter } from "./user" - -export const appRouter = trpc - .router() - .query("site", { - input: z.object({ - site: z.string(), - }), - output: z.object({ - id: z.string(), - name: z.string(), - description: z.string().nullable(), - icon: z.string().nullable(), - subdomain: z.string(), - navigation: z - .array( - z.object({ - id: z.string(), - label: z.string(), - url: z.string(), - }) - ) - .nullable(), - }), - async resolve({ input }) { - const site = await getSite(input.site) - return site - }, - }) - .merge("auth.", authRouter) - .merge("site.", siteRouter) - .merge("user.", userRouter) - .merge("membership.", membershipRouter) - .merge("page.", pageRouter) - .formatError(({ error, shape }) => { - const isZodError = error.cause instanceof ZodError - return { - ...shape, - message: isZodError - ? error.cause.issues.map((i) => i.message).join(", ") - : error.message, - data: { - ...shape.data, - notFound: isNotFoundError(error.cause), - }, - } - }) - -// export type definition of API -export type AppRouter = typeof appRouter diff --git a/src/router/membership.ts b/src/router/membership.ts deleted file mode 100644 index f4ae0905..00000000 --- a/src/router/membership.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z } from "zod" -import { createRouter } from "~/lib/trpc.server" -import { updateMembership } from "~/models/membership" - -export const membershipRouter = createRouter().mutation("updateMembership", { - input: z.object({ - id: z.string(), - lastSwitchedTo: z - .string() - .transform((v) => new Date()) - .optional(), - }), - async resolve({ input, ctx }) { - const { id, ...payload } = input - if ( - !ctx.gate.allows({ type: "can-update-membership", membership: { id } }) - ) { - throw ctx.gate.permissionError() - } - - await updateMembership(id, payload) - }, -}) diff --git a/src/router/page.ts b/src/router/page.ts deleted file mode 100644 index 40cbcea0..00000000 --- a/src/router/page.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod" -import { createRouter } from "~/lib/trpc.server" -import { createOrUpdatePage, deletePage } from "~/models/page.model" - -export const pageRouter = createRouter() - .query("authors", { - resolve() {}, - }) - .mutation("createOrUpdate", { - input: z.object({ - siteId: z.string(), - pageId: z.string().optional(), - title: z.string().optional(), - content: z.string().optional(), - published: z.boolean().optional(), - publishedAt: z.string().optional(), - excerpt: z.string().optional(), - isPost: z.boolean().optional(), - slug: z.string().optional(), - emailSubject: z.string().optional(), - }), - output: z.object({ - id: z.string(), - }), - async resolve({ input, ctx }) { - const { page } = await createOrUpdatePage(ctx.gate, input) - return page - }, - }) - .mutation("delete", { - input: z.object({ - pageId: z.string(), - }), - async resolve({ ctx, input }) { - await deletePage(ctx.gate, { id: input.pageId }) - }, - }) diff --git a/src/router/site.ts b/src/router/site.ts deleted file mode 100644 index c5e6f849..00000000 --- a/src/router/site.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { z } from "zod" -import { createRouter } from "~/lib/trpc.server" -import { PageVisibilityEnum } from "~/lib/types" -import { - getSite, - getSubscription, - updateSite, - createSite, - subscribeToSite, - unsubscribeFromSite, -} from "~/models/site.model" -import { - getPage, - getPagesBySite, - scheduleEmailForPost, -} from "~/models/page.model" - -export const siteRouter = createRouter() - .query("subscription", { - input: z.object({ - site: z.string(), - }), - output: z - .object({ - email: z.boolean().optional(), - }) - .nullable(), - async resolve({ input, ctx }) { - const user = ctx.gate.getUser() - if (!user) return null - const site = await getSite(input.site) - const subscription = await getSubscription({ - siteId: site.id, - userId: user.id, - }) - return subscription ? subscription.config : null - }, - }) - .query("pages", { - input: z.object({ - site: z.string(), - type: z.enum(["post", "page"]).default("post"), - visibility: z - .enum([ - PageVisibilityEnum.All, - PageVisibilityEnum.Published, - PageVisibilityEnum.Draft, - PageVisibilityEnum.Scheduled, - ]) - .nullish(), - take: z.number().optional(), - cursor: z.string().optional(), - includeContent: z.boolean().optional(), - includeExcerpt: z.boolean().optional(), - }), - output: z.object({ - list: z.array( - z.object({ - id: z.string(), - title: z.string(), - body: z.object({ - content: z.string(), - }), - date_published: z.date().transform((v) => v.toISOString()), - summary: z.object({ - content: z.string(), - }), - }), - ), - total: z.number(), - hasMore: z.boolean(), - }), - async resolve({ input, ctx }) { - const result = await getPagesBySite(input) - return result - }, - }) - .query("page", { - input: z.object({ - site: z.string(), - page: z.string(), - render: z.boolean(), - includeAuthors: z.boolean().optional(), - }), - output: z.object({ - id: z.string(), - title: z.string(), - content: z.string(), - excerpt: z.string().nullable(), - published: z.boolean(), - publishedAt: z.date().transform((v) => v.toISOString()), - slug: z.string(), - type: z.enum(["PAGE", "POST"]), - rendered: z - .object({ - excerpt: z.string(), - contentHTML: z.string(), - }) - .nullable(), - authors: z - .array( - z.object({ - id: z.string(), - name: z.string(), - avatar: z.string().nullable(), - }), - ) - .optional(), - emailSubject: z.string().nullish(), - emailStatus: z.string().nullish(), - siteId: z.string(), - }), - async resolve({ input, ctx }) { - const page = await getPage(ctx.gate, input) - return page - }, - }) - .mutation("updateSite", { - input: z.object({ - site: z.string(), - name: z.string().optional(), - description: z.string().optional(), - icon: z.string().nullish(), - subdomain: z.string().optional(), - navigation: z - .array( - z.object({ - id: z.string(), - label: z.string(), - url: z - .string() - .regex( - /^(https?:\/\/|\/)/, - "URL must start with / or http:// or https://", - ), - }), - ) - .optional(), - }), - output: z.object({ - site: z.object({ - id: z.string(), - name: z.string(), - subdomain: z.string(), - }), - subdomainUpdated: z.boolean(), - }), - async resolve({ ctx, input }) { - const { site, subdomainUpdated } = await updateSite(ctx.gate, input) - return { - site, - subdomainUpdated, - } - }, - }) - - .mutation("create", { - input: z.object({ - name: z.string(), - subdomain: z.string().min(3).max(26), - }), - output: z.object({ - id: z.string(), - subdomain: z.string(), - }), - async resolve({ input, ctx }) { - const { site } = await createSite(ctx.gate, input) - return site - }, - }) - .mutation("subscribe", { - input: z.object({ - email: z.boolean().optional(), - siteId: z.string(), - newUser: z - .object({ - email: z.string(), - url: z.string(), - }) - .optional(), - }), - async resolve({ input, ctx }) { - await subscribeToSite(ctx.gate, input) - }, - }) - .mutation("unsubscribe", { - input: z.object({ - siteId: z.string(), - }), - async resolve({ input, ctx }) { - await unsubscribeFromSite(ctx.gate, input) - }, - }) - .mutation("scheduleEmailForPost", { - input: z.object({ - pageId: z.string(), - emailSubject: z.string().optional(), - }), - async resolve({ ctx, input }) { - await scheduleEmailForPost(ctx.gate, { - pageId: input.pageId, - emailSubject: input.emailSubject, - }) - }, - }) diff --git a/src/router/user.ts b/src/router/user.ts deleted file mode 100644 index cdc949b3..00000000 --- a/src/router/user.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MembershipRole } from "@prisma/client" -import { encrypt, getDerivedKey } from "@proselog/jwt" -import { z } from "zod" -import { IS_PROD } from "~/lib/constants" -import { ENCRYPT_SECRET } from "~/lib/env.server" -import { createRouter } from "~/lib/trpc.server" -import { getMemberships } from "~/models/membership" -import { userModel } from "~/models/user.model" - -export const userRouter = createRouter() - .query("getSignedJwt", { - output: z.string(), - async resolve({ ctx }) { - const user = ctx.gate.getUser(true) - const key = await getDerivedKey(ENCRYPT_SECRET) - const token = await encrypt( - { prefix: IS_PROD ? `${user.id}/` : `dev/${user.id}/` }, - key, - { - expiresIn: "1h", - } - ) - return token - }, - }) - .query("getSubscriptions", { - input: z.object({ - canManage: z.boolean().optional(), - }), - output: z.array( - z.object({ - id: z.string(), - role: z.enum([ - MembershipRole.ADMIN, - MembershipRole.OWNER, - MembershipRole.SUBSCRIBER, - ]), - site: z.object({ - id: z.string(), - name: z.string(), - subdomain: z.string(), - icon: z.string().nullable(), - }), - }) - ), - async resolve({ ctx, input }) { - const user = ctx.gate.getUser(true) - return getMemberships({ userId: user.id, ...input }) - }, - }) - .mutation("updateProfile", { - input: z.object({ - username: z.string().optional(), - name: z.string().optional(), - email: z.string().optional(), - bio: z.string().optional(), - avatar: z.string().optional(), - }), - async resolve({ ctx, input }) { - await userModel.updateProfile(ctx.gate, input) - }, - })