Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions apps/web/app/admin/operations/catalogue/[[...section]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { notFound } from "next/navigation";
import type { SearchParams } from "@/ui/admin/catalogue/catalogue-pages";
import {
CatalogueDiscoveryDetailPage,
CatalogueOperationsPage,
CatalogueSyncDetailPage,
} from "@/ui/admin/operations/operations-pages";

export const dynamic = "force-dynamic";

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;

export default async function Page({
params,
searchParams,
}: {
params: Promise<{ section?: string[] }>;
searchParams: SearchParams;
}) {
const { section = [] } = await params;
if (section.length === 0) {
return (
<CatalogueOperationsPage
section="syncs"
searchParams={await searchParams}
/>
);
}
if (section.length === 1 && section[0] === "discovery") {
return (
<CatalogueOperationsPage
section="discovery"
searchParams={await searchParams}
/>
);
}
if (section.length === 2 && section[0] === "syncs") {
if (!UUID.test(section[1]!)) notFound();
return <CatalogueSyncDetailPage syncId={section[1]!} />;
}
if (section.length === 2 && section[0] === "discovery") {
const checkId = Number(section[1]);
if (!Number.isInteger(checkId) || checkId < 1) notFound();
return <CatalogueDiscoveryDetailPage checkId={checkId} />;
}
notFound();
}
4 changes: 2 additions & 2 deletions apps/web/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UsersRound } from "lucide-react";
import { ImportModelCard } from "@/ui/admin/imports/import-model-card";
import { loadImportModelSetting } from "@/lib/admin/settings";
import { loadAdminUserSummary } from "@/lib/admin/users";
import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { canManageCatalogueOperations } from "@/lib/auth/viewer";
import { AppShell } from "@/ui/shell";
import { StatTile } from "@/ui/common/stat-tile";

Expand All @@ -12,7 +12,7 @@ export default async function AdminOverviewPage() {
const [users, importModel, canManageImports] = await Promise.all([
loadAdminUserSummary(),
loadImportModelSetting(),
canManageCatalogueSources(),
canManageCatalogueOperations(),
]);

return (
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/admin/catalogue-directory/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { canManageCatalogueOperations } from "@/lib/auth/viewer";
import { refreshCatalogueDirectory } from "@/lib/catalogue-import/directory";
import { isCatalogueKind } from "@/lib/catalogue/content";

Expand All @@ -20,7 +20,7 @@ function eventResponse(data: unknown, status: number) {

/** Streams directory refresh progress as server-sent events. */
export async function POST(request: Request) {
if (!(await canManageCatalogueSources())) {
if (!(await canManageCatalogueOperations())) {
return eventResponse(
{ type: "error", message: "Import permission is required." },
403,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { canManageCatalogueOperations } from "@/lib/auth/viewer";
import {
type SyncArtifactLocator,
readSyncArtifact,
} from "@/lib/catalogue-sync/artifact-store";
import { createClient } from "@/lib/supabase/server";

export const runtime = "nodejs";

/** Serves a stored sync artefact as inert text for the operations viewer. */
export async function GET(
_request: Request,
{ params }: { params: Promise<{ artifactId: string }> },
) {
if (!(await canManageCatalogueOperations())) {
return new Response("Catalogue operations permission is required.", {
status: 403,
});
}
const { artifactId } = await params;
const supabase = await createClient();
const { data, error } = await supabase
.from("catalogue_sync_artifacts")
.select("media_type,content_sha256,byte_size,storage_bucket,storage_path")
.eq("id", artifactId)
.maybeSingle();
if (error || !data)
return new Response("Artefact not found.", { status: 404 });
try {
const body = await readSyncArtifact({
artifact: {
bucket: data.storage_bucket as SyncArtifactLocator["bucket"],
path: data.storage_path,
mediaType: data.media_type,
contentSha256: data.content_sha256,
byteSize: data.byte_size,
},
});
return new Response(body, {
headers: {
"content-type": "text/plain; charset=utf-8",
"cache-control": "private, no-store",
"x-content-type-options": "nosniff",
},
});
} catch {
return new Response("The artefact could not be read.", { status: 502 });
}
}
6 changes: 3 additions & 3 deletions apps/web/app/api/admin/catalogue-syncs/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { after } from "next/server";
import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { canManageCatalogueOperations } from "@/lib/auth/viewer";
import { isCatalogueKind } from "@/lib/catalogue/content";
import { processCatalogueSyncInline } from "@/lib/catalogue-sync/sync-queue";
import { startCatalogueSync } from "@/lib/catalogue-sync/sync-service";
Expand All @@ -16,7 +16,7 @@ function json(data: unknown, status = 200) {

/** Creates one record sync. Inline processing continues after the response. */
export async function POST(request: Request) {
if (!(await canManageCatalogueSources())) {
if (!(await canManageCatalogueOperations())) {
return json({ error: "Catalogue sync permission is required." }, 403);
}
let payload: StartRequest;
Expand Down Expand Up @@ -52,7 +52,7 @@ export async function POST(request: Request) {

/** Stops an unfinished record sync. */
export async function DELETE(request: Request) {
if (!(await canManageCatalogueSources())) {
if (!(await canManageCatalogueOperations())) {
return json({ error: "Catalogue sync permission is required." }, 403);
}
let payload: { syncId?: unknown };
Expand Down
10 changes: 5 additions & 5 deletions apps/web/lib/admin/settings-actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use server";

import { revalidatePath } from "next/cache";
import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { canManageCatalogueOperations } from "@/lib/auth/viewer";
import { createClient } from "@/lib/supabase/server";
import { IMPORT_MODEL_SETTING_KEY } from "@/lib/admin/settings";
import { fetchCatalogueModel } from "@/lib/admin/model-catalogue";
Expand All @@ -20,7 +20,7 @@ function refreshImportPages() {
export async function setImportModel(
model: string,
): Promise<ImportModelActionResult> {
if (!(await canManageCatalogueSources()))
if (!(await canManageCatalogueOperations()))
return {
ok: false,
model,
Expand Down Expand Up @@ -61,7 +61,7 @@ export async function saveImportModel(
model: string,
refreshOnly = false,
): Promise<ImportModelActionResult> {
if (!(await canManageCatalogueSources()))
if (!(await canManageCatalogueOperations()))
return {
ok: false,
model,
Expand Down Expand Up @@ -103,7 +103,7 @@ export async function saveImportModel(
export async function removeImportModel(
model: string,
): Promise<ImportModelActionResult> {
if (!(await canManageCatalogueSources()))
if (!(await canManageCatalogueOperations()))
return {
ok: false,
model,
Expand Down Expand Up @@ -139,7 +139,7 @@ export async function setImportModelVisibility(
model: string,
visible: boolean,
): Promise<ImportModelActionResult> {
if (!(await canManageCatalogueSources()))
if (!(await canManageCatalogueOperations()))
return {
ok: false,
model,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/assistant/model-actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server";

import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { canManageCatalogueOperations } from "@/lib/auth/viewer";
import { loadImportModelSetting } from "@/lib/admin/settings";
import type { ImportModel } from "@/lib/admin/import-model";

Expand All @@ -9,7 +9,7 @@ export async function loadAssistantModels(): Promise<{
defaultModel: string;
error: string | null;
}> {
if (!(await canManageCatalogueSources())) {
if (!(await canManageCatalogueOperations())) {
return {
models: [],
defaultModel: "",
Expand Down
12 changes: 6 additions & 6 deletions apps/web/lib/auth/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ async function currentUserHasPermission(requiredPermission: string) {
}

/** Check the permission required to run programme and calendar imports. */
export async function canManageCatalogueImports() {
return currentUserHasPermission("imports.manage");
}

/** Check the shared permission for catalogue sources and extraction models. */
export async function canManageCatalogueSources() {
/**
* The permission for ANU operations: running syncs and discovery, choosing an
* extraction model, and reading the technical record of either. Separate from
* catalogue.write, which authors and publishes content.
*/
export async function canManageCatalogueOperations() {
return currentUserHasPermission("imports.manage");
}

Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/catalogue-sync/sync-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "server-only";
import { canManageCatalogueSources, getAuthViewer } from "@/lib/auth/viewer";
import { canManageCatalogueOperations, getAuthViewer } from "@/lib/auth/viewer";
import { loadImportModelSetting } from "@/lib/admin/settings";
import { createClient } from "@/lib/supabase/server";
import { dispatchCatalogueSync } from "./sync-queue";
Expand All @@ -24,7 +24,7 @@ export async function startCatalogueSync({
requestedBy?: string;
kind: CatalogueKind;
}) {
if (!(await canManageCatalogueSources())) {
if (!(await canManageCatalogueOperations())) {
throw new CatalogueSyncStartError("Catalogue sync permission is required.");
}
const viewer = await getAuthViewer();
Expand Down
Loading
Loading