diff --git a/src/app/actions/ingest.ts b/src/app/actions/ingest.ts new file mode 100644 index 0000000..9fcb0bf --- /dev/null +++ b/src/app/actions/ingest.ts @@ -0,0 +1,80 @@ +"use server"; + +import { randomBytes } from "node:crypto"; +import { revalidatePath } from "next/cache"; + +import { getAdminPb } from "@/lib/pb/admin"; +import { requireSession } from "@/lib/auth/session"; +import { ingestProject } from "@/lib/git/ingest"; +import { repoExists } from "@/lib/git/forgejo"; +import type { ProjectRecord } from "@/lib/pb/types"; + +/** See the security note at the top of src/app/actions/cards.ts. */ +async function requireOwner() { + const session = await requireSession(); + if (session.role !== "owner") throw new Error("Not found"); + return session; +} + +/** + * Generate or replace a project's webhook secret. + * + * Returned once so the owner can paste it into Forgejo. It is stored and can be + * read again by the owner's admin page — unlike a user password, this is a + * shared secret that both ends need, so there is no value in making it + * unrecoverable. It must still never reach a client manager's browser. + */ +export async function rotateWebhookSecret(input: { + projectId: string; +}): Promise<{ secret: string }> { + await requireOwner(); + + const secret = `whsec_${randomBytes(24).toString("hex")}`; + const pb = await getAdminPb(); + + await pb + .collection("projects") + .update(input.projectId, { webhook_secret: secret }); + + revalidatePath(`/admin/projects/${input.projectId}`); + return { secret }; +} + +/** + * Pull the full history for a project. + * + * `stopAfterKnownPages: Infinity` reads to the end rather than stopping at the + * first familiar page — that is the whole difference between a backfill and the + * hourly poll, and it is why both share one function. + * + * WFDASH-WEBAPP is 787 commits, so this is 16 pages at 50 per page. It is slow + * enough to want a progress indication in the UI and fast enough not to need a + * job queue. + */ +export async function backfillProject(input: { + projectId: string; +}): Promise<{ created: number; skipped: number }> { + await requireOwner(); + + const pb = await getAdminPb(); + const project = await pb + .collection("projects") + .getOne(input.projectId); + + if (!project.repo_owner || !project.repo_name) { + throw new Error("Link a repository to this project first"); + } + + if (!(await repoExists(project.repo_owner, project.repo_name))) { + throw new Error( + `Forgejo has no repository ${project.repo_owner}/${project.repo_name}, or it is not readable` + ); + } + + const result = await ingestProject(project, { + stopAfterKnownPages: Number.POSITIVE_INFINITY, + }); + + revalidatePath(`/admin/projects/${input.projectId}`); + return { created: result.created, skipped: result.skipped }; +} \ No newline at end of file