import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { badRequest } from "~/server/errors"; import { limitWrite } from "~/server/limits"; import { assertNotArchived, requireRepoAccess } from "~/server/repos"; import { listRefs } from "~/server/git/refs"; import { customDomainsEnabled, customDomainsTarget } from "~/server/sites/domains"; import { labelUrl, sitesDomain } from "~/server/sites/config"; import { listSiteDeployments, loadSiteForRepo, rollbackSite } from "~/server/sites/deploy"; import { deploySiteRef } from "~/server/sites/api"; import { siteUsageToday } from "~/server/sites/usage"; import { siteLabel } from "~/server/sites/naming"; import { deleteEnv, disableSite, listEnv, refreshSiteDomain, saveAccess, saveSite, setEnv, setSiteDomain, } from "~/server/sites/site"; import { repoRef } from "~/lib/validation"; import type { Viewer } from "~/lib/types"; const refName = z.string().min(1).max(255); async function adminSite(owner: string, name: string, viewer: Viewer | null, write = false) { const { repo } = await requireRepoAccess(owner, name, requireViewer(viewer), "admin"); if (write) assertNotArchived(repo); return { repo, site: await loadSiteForRepo(repo.id) }; } async function requireSite(owner: string, name: string, viewer: Viewer | null, write = true) { const { repo, site } = await adminSite(owner, name, viewer, write); if (!site) throw badRequest("Enable the site first"); await limitWrite(requireViewer(viewer)); return { repo, site }; } export const getSite = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo, site } = await adminSite(data.owner, data.repo, context.viewer); const refs = await listRefs(repo); const label = site?.label ?? siteLabel(repo.owner_name, repo.name, sitesDomain()); const deployments = site ? await listSiteDeployments(site.id) : []; return { enabled: sitesDomain() !== null, public: repo.visibility === "public", customDomainsEnabled: customDomainsEnabled(), customDomainsTarget: customDomainsTarget(), defaultBranch: repo.default_branch, branches: refs.branches.slice(0, 200).map((branch) => branch.name), url: labelUrl(label), site: site ? { source: site.source === "workflow" ? ("workflow" as const) : ("branch" as const), branch: site.branch, folder: site.folder, artifactName: site.artifact_name, spa: site.spa === 1, previews: site.previews === 1, previewEnv: site.preview_env === 1, commitPreviews: site.commit_previews === 1, access: site.access, audience: site.audience, hasPassword: site.password_hash !== null, oidcIssuer: site.oidc_issuer, oidcClientId: site.oidc_client_id, oidcAllowed: site.oidc_allowed, customDomain: site.custom_domain, domainStatus: site.domain_status, activeDeploymentId: site.active_deployment_id, } : null, env: site ? await listEnv(site.id) : [], usage: site ? await siteUsageToday(site.id) : null, deployments: deployments.map((row) => ({ id: row.id, environment: row.environment, branch: row.branch, kind: row.kind, status: row.status, ref: row.ref, sha: row.sha, source: row.source, fileCount: row.file_count, totalBytes: row.total_bytes, error: row.error, createdAt: row.created_at, url: labelUrl(row.label), })), }; }); export const saveSiteSettings = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ source: z.enum(["branch", "workflow"]), branch: refName, folder: z.string().max(200), artifactName: z.string().regex(/^[A-Za-z0-9._-]{1,100}$/), spa: z.boolean(), previews: z.boolean(), previewEnv: z.boolean().default(false), commitPreviews: z.boolean().default(false), }), ) .handler(async ({ data, context }) => { const { repo } = await adminSite(data.owner, data.repo, context.viewer, true); await limitWrite(requireViewer(context.viewer)); await saveSite(repo, data); return { ok: true }; }); export const saveSiteAccess = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ access: z.enum(["public", "password", "gitfor", "oidc"]), audience: z.enum(["readers", "members"]), password: z.string().max(200).nullable(), oidc: z .object({ issuer: z.string().url().max(300), clientId: z.string().min(1).max(300), clientSecret: z.string().max(1000).nullable(), allowed: z.string().max(2000), }) .nullable(), }), ) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const { repo, site } = await requireSite(data.owner, data.repo, context.viewer); await saveAccess(repo, site, data); return { ok: true }; }); export const setSiteVariable = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ name: z.string().min(1).max(64), value: z.string().max(4096), secret: z.boolean(), preview: z.boolean().default(false), }), ) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const { repo, site } = await requireSite(data.owner, data.repo, context.viewer); await setEnv(repo, site, data.name, data.value, data.secret, data.preview); return { ok: true }; }); export const deleteSiteVariable = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ name: z.string().min(1).max(64) })) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const { repo, site } = await requireSite(data.owner, data.repo, context.viewer, false); await deleteEnv(repo, site, data.name); return { ok: true }; }); export const deploySite = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireSite(data.owner, data.repo, viewer); const result = await deploySiteRef(repo, { actorId: viewer.id }); return { id: result.deploymentId }; }); export const rollbackSiteDeployment = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ deploymentId: z.string().min(1) })) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const { site } = await requireSite(data.owner, data.repo, context.viewer); await rollbackSite(site, data.deploymentId); return { ok: true }; }); export const setSiteCustomDomain = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ domain: z.string().max(253).nullable() })) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const { site } = await requireSite(data.owner, data.repo, context.viewer); await setSiteDomain(site, data.domain); return { ok: true }; }); export const checkSiteCustomDomain = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const { site } = await requireSite(data.owner, data.repo, context.viewer); return refreshSiteDomain(site); }); export const disableSiteHosting = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const { site } = await requireSite(data.owner, data.repo, context.viewer, false); await disableSite(site); return { ok: true }; });