xLog/app/controllers/site.controller.ts

312 lines
7.3 KiB
TypeScript

import { prismaRead, prismaWrite } from "~/lib/db.server"
import {
checkPageSlug,
checkSubdomain,
getSite,
getSitesByUser,
} from "~/models/site.model"
import { PageVisibilityEnum } from "~/lib/types"
import { isUUID } from "~/lib/uuid"
import { MembershipRole, PageType, type Prisma } from "@prisma/client"
import { nanoid } from "nanoid"
import { z } from "zod"
import { type AuthUser } from "~/lib/auth.server"
import { createGate } from "~/lib/gate.server"
export const siteController = {
async getSites(user: AuthUser | undefined | null) {
const gate = createGate({ user, requireAuth: true })
const sites = await getSitesByUser({ userId: gate.user.id })
return sites
},
async getSite(siteIdOrSubdomainOrDomain: string) {
const site = await getSite(siteIdOrSubdomainOrDomain)
return { site }
},
async getPages(
user: AuthUser | undefined | null,
input: {
site: string
type: "post" | "page"
visibility?: PageVisibilityEnum | null
take?: number | null
cursor?: string | null
}
) {
const gate = createGate({ user })
const validated = 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().default(30),
cursor: z.string().nullish(),
})
.parse(input)
const site = await getSite(validated.site)
const visibility = validated.visibility || PageVisibilityEnum.Published
if (!gate.allows({ type: "can-list-page", visibility, siteId: site.id })) {
throw gate.permissionError()
}
const now = new Date()
const where: Prisma.PageWhereInput = {
siteId: site.id,
deletedAt: null,
type: validated.type === "post" ? "POST" : "PAGE",
}
if (validated.visibility === PageVisibilityEnum.Published) {
where.published = true
where.publishedAt = {
lte: now,
}
} else if (validated.visibility === PageVisibilityEnum.Scheduled) {
where.published = true
where.publishedAt = {
gt: now,
}
} else if (validated.visibility === PageVisibilityEnum.Draft) {
where.published = false
}
const [pages, total] = await Promise.all([
prismaRead.page.findMany({
where,
orderBy: {
createdAt: "desc",
},
take: validated.take + 1,
cursor: validated.cursor
? {
id: validated.cursor,
}
: undefined,
}),
await prismaRead.page.count({
where: {
siteId: site.id,
},
}),
])
const hasMore = pages.length > validated.take
return {
pages,
total,
hasMore,
}
},
async createOrUpdatePage(
user: AuthUser | null | undefined,
input: {
pageId?: string
siteId: string
slug?: string
title?: string
content?: string
published?: boolean
publishedAt?: Date
excerpt?: string
isPost?: boolean
}
) {
const gate = createGate({ user })
const page = input.pageId
? await prismaRead.page.findUnique({
where: {
id: input.pageId,
},
include: {
site: true,
},
})
: await prismaWrite.page.create({
data: {
title: "Untitled",
slug: `untitled-${nanoid(4)}`,
site: {
connect: {
id: input.siteId,
},
},
content: "",
excerpt: "",
},
include: {
site: true,
},
})
if (!page || page.deletedAt) {
throw new Error(`Page not found`)
}
if (!gate.allows({ type: "can-create-page", siteId: page.siteId })) {
throw gate.permissionError()
}
const slug = input.slug || page.slug
await checkPageSlug({ slug, excludePage: page.id, siteId: page.siteId })
const updated = await prismaWrite.page.update({
where: {
id: page.id,
},
data: {
title: input.title,
content: input.content,
published: input.published,
publishedAt: input.publishedAt,
excerpt: input.excerpt,
slug,
type: input.isPost ? "POST" : "PAGE",
},
})
return { page: updated }
},
async getPage(
user: AuthUser | null | undefined,
input: {
/** page slug or id, `site` is needed when `page` is a slug */
page: string
site: string
}
) {
const gate = createGate({ user })
const site = input.site ? await getSite(input.site) : null
if (input.site && !site) {
throw new Error(`site not found`)
}
const isPageUUID = isUUID(input.page)
if (!isPageUUID && !input.site) {
throw new Error("missing input site")
}
const page = isPageUUID
? await prismaRead.page.findUnique({
where: {
id: input.page,
},
})
: site
? await prismaRead.page.findFirst({
where: { siteId: site.id, slug: input.page },
})
: null
if (!page || page.deletedAt) {
throw new Response(`page ${input.page} not found`, {
status: 404,
})
}
if (!gate.allows({ type: "can-read-page", page })) {
throw gate.permissionError()
}
return page
},
async updateSite(
user: AuthUser | undefined | null,
payload: {
site: string
name?: string
description?: string
icon?: string | null
subdomain?: string
}
) {
const gate = createGate({ user, requireAuth: true })
const site = await getSite(payload.site)
if (!gate.allows({ type: "can-update-site", site })) {
throw gate.permissionError()
}
if (payload.subdomain) {
await checkSubdomain({
subdomain: payload.subdomain,
updatingSiteId: site.id,
})
}
const updated = await prismaWrite.site.update({
where: {
id: site.id,
},
data: {
name: payload.name,
subdomain: payload.subdomain,
description: payload.description,
icon: payload.icon,
},
})
return {
site: updated,
subdomainUpdated: updated.subdomain !== site.subdomain,
}
},
async createSite(
user: AuthUser | null | undefined,
payload: { name: string; subdomain: string }
) {
const gate = createGate({ user, requireAuth: true })
await checkSubdomain({ subdomain: payload.subdomain })
const site = await prismaWrite.site.create({
data: {
name: payload.name,
subdomain: payload.subdomain,
memberships: {
create: {
role: MembershipRole.OWNER,
user: {
connect: {
id: gate.user.id,
},
},
},
},
pages: {
create: {
title: "About",
slug: "about",
excerpt: "",
content: `My name is ${payload.name} and I'm a new site.`,
published: true,
publishedAt: new Date(),
type: PageType.PAGE,
},
},
},
})
return { site }
},
}