fix´: crossbell renovation - remove useless files and fix build errors

This commit is contained in:
DIYgod 2022-06-30 20:47:40 +01:00
parent 7d3415c57a
commit 20001c1873
No known key found for this signature in database
GPG Key ID: D159328F47A80DCA
34 changed files with 19 additions and 1226 deletions

View File

@ -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 (
<Modal title="Email Post" open={open} setOpen={setOpen}>
{
<form onSubmit={onSubmit}>
<div className="p-5">
<Input
label="Email subject"
id="subject"
help="Defaults to post title"
isBlock
{...register("subject")}
/>
</div>
<div className="p-5 border-t">
<Button type="submit" isLoading={scheduleEmailForPost.isLoading}>
<span className="i-mdi:email-send text-xl mr-1"></span>
<span>Send Email</span>
</Button>
</div>
</form>
}
</Modal>
)
}

View File

@ -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 (
<Modal
title={`Continue with Email`}
open={loginModalOpened}
setOpen={setLoginModalOpened}
>
<div className="p-5">
{data && (
<div className="mb-5">
We just emailed you with a link to log in, please check your inbox
and spam folder in case you can{`'`}t find it.
</div>
)}
{error && <div className="mb-5 text-red-500">{error.message}</div>}
<form className="space-y-5" onSubmit={handleSubmit}>
<div>
<label
className="block mb-1 font-medium text-zinc-600"
htmlFor="email"
>
Email
</label>
<input
id="email"
type="email"
required
className="input is-block"
{...form.register("email")}
/>
</div>
<div>
<Button
type="submit"
isBlock
isLoading={requestLoginLinkStatus === "loading"}
>
Continue
</Button>
</div>
<div className="text-xs text-zinc-400">
By clicking Continue, you agree to our{" "}
<UniLink
href={`https://${DOCS_DOMAIN}/terms.html`}
className="underline"
>
Terms of Service
</UniLink>{" "}
and{" "}
<UniLink
href={`https://${DOCS_DOMAIN}/privacy.html`}
className="underline"
>
Privacy Policy
</UniLink>
.
</div>
</form>
</div>
</Modal>
)
}
export default LoginModal

View File

@ -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 (
<Modal
title={subscription ? `Manage your subscription` : `Become a subscriber`}
open={open}
setOpen={(open) => {
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 ? (
<div className="p-5 space-y-3">
<p>
We have sent you an email with a link to confirm your subscription.
</p>
<p>Please check your inbox (and spam folder).</p>
</div>
) : (
<form className="p-5" onSubmit={handleSubscribe}>
{!isLoggedIn && (
<div className="mb-5">
<Input
label="Email"
type="email"
id="email"
isBlock
required
{...subscribeForm.register("newUserEmail", {})}
/>
</div>
)}
<div>
<label className="select-none flex items-center space-x-1">
<input type="checkbox" {...subscribeForm.register("email")} />
<span>Receive updates via Email</span>
</label>
</div>
<div className="mt-5 space-x-3 flex items-center">
<Button type="submit" isLoading={subscribe.isLoading}>
<span>{subscription ? "Update" : "Subscribe"}</span>
</Button>
{subscription && (
<Button
type="button"
variant="secondary"
isLoading={unsubscribe.isLoading}
onClick={() => unsubscribe.mutate({ siteId })}
>
Unsubscribe
</Button>
)}
</div>
</form>
)}
</Modal>
)
}

View File

@ -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<ReactAvatarEditor | null>(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<HTMLInputElement>(null)

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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}` },

View File

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

View File

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

View File

@ -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<UploadFile>(
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}`
}

View File

@ -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
export const GITHUB_LINK = process.env.NEXT_PUBLIC_GITHUB_LINK

View File

@ -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 = `
<p>Hello,</p>
<p>We received a request to sign in to ${APP_NAME} using this email address, please click the link below to sign in:</p>
<a href="${loginLink}">Sign in to ${APP_NAME}</a>
<p>This link will expire in 10 minutes. If you did not request this link, you can safely ignore this email.</p>
<p>Thanks,</p>
<p>Your Proselog Team</p>
`
if (payload.toSubscribeSiteId) {
const site = await getSite(payload.toSubscribeSiteId)
subject = `Confirm your subscription to ${site.name}`
html = `
<p>Please confirm your subscription to "${site.name}" by clicking the link below:</p>
<a href="${loginLink}">confirm</a>
<p>This link will expire in 7 days. If you did not request this link, you can safely ignore this email.</p>`
}
const message: MailgunMessageData = {
from: `${APP_NAME} <hi@proselog.com>`,
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} <updates@${payload.site.subdomain}.proselog.com>`
const subject =
payload.post.emailSubject ||
`${payload.post.title} - ${payload.site.name}`
const siteLink = getSiteLink({ subdomain: payload.site.subdomain })
const html = `
<a href="${siteLink}/${
payload.post.slug
}" style="display:block;background:#eee;padding:5px 8px;border-radius:4px;text-decoration:none;">
View this post in browser
</a>
<h1 style="font-size:2.4em">${payload.post.title}</h1>
<div>
${payload.post.rendered.contentHTML}
</div>
<p style="margin-top:3em;">
<a
href="${`${SITE_URL}/api/login?next=/ignore&token=%recipient.unsubscribeToken%`}"
style="text-decoration:none;">
Unsubscribe
</a>
</p>
<p>
<a
href="${SITE_URL}"
style="text-decoration:none;">
Published on Proselog
</a>
</p>
`
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)
}
}

View File

@ -1,32 +0,0 @@
import { createReactQueryHooks } from "@trpc/react"
import { withTRPC } from "@trpc/next"
import type { AppRouter } from "~/router"
export const trpc = createReactQueryHooks<AppRouter>()
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,
})
}

View File

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

View File

@ -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"

View File

@ -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) {
<RainbowKitProvider chains={chains}>
<StoreProvider createStore={createStore}>
<Component {...pageProps} />
<LoginModal />
<Toaster />
</StoreProvider>
</RainbowKitProvider>
@ -78,4 +75,4 @@ function MyApp({ Component, pageProps }: any) {
// return { ...appProps }
// }
export default wrapTrpc()(MyApp)
export default MyApp

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
</div>
</DashboardMain>
</DashboardLayout>
{pageId && <EmailPostModal pageId={pageId} />}
</>
)
}

View File

@ -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() {
<div>
<label className="form-label">Icon</label>
<AvatarForm
site={site.username}
site={site.username!}
name={site.name || site.username || ""}
filename={site.avatars?.[0] || undefined}
/>

View File

@ -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"

View File

@ -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"

View File

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

View File

@ -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<TRPCContext>()
.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

View File

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

View File

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

View File

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

View File

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