feat(actions): backfill and webhook secret rotation
This commit is contained in:
parent
c5246f66f3
commit
0236a0b1eb
1 changed files with 80 additions and 0 deletions
80
src/app/actions/ingest.ts
Normal file
80
src/app/actions/ingest.ts
Normal file
|
|
@ -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<ProjectRecord>(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 };
|
||||
}
|
||||
Loading…
Reference in a new issue