Add mTLS RBAC with path-based access control, role/cert trust model, and comprehensive tests
Implements full role-based access control for mTLS client certificates: - Database: mtls_roles, mtls_certificate_roles, mtls_access_rules tables with migration - Models: CRUD for roles, cert-role assignments, path-based access rules - Caddy config: HTTP-layer RBAC enforcement via CEL fingerprint matching in subroutes - New trust model: select individual certs or entire roles instead of CAs (derives CAs automatically) - REST API: /api/v1/mtls-roles, cert assignments, proxy-host access rules endpoints - UI: Roles management tab (card-based), cert/role trust picker, inline RBAC rule editor - Fix: dialog autoclose bug after creating proxy host (key-based remount) - Tests: 85 new tests (785 total) covering models, schema, RBAC route generation, leaf override, edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,15 @@ import { useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { PageHeader } from "@/components/ui/PageHeader";
|
||||
import { SearchField } from "@/components/ui/SearchField";
|
||||
import type { AcmeHost, CaCertificateView, CertExpiryStatus, ImportedCertView, ManagedCertView } from "./page";
|
||||
import type { AcmeHost, CaCertificateView, CertExpiryStatus, ImportedCertView, ManagedCertView, MtlsRole } from "./page";
|
||||
import type { IssuedClientCertificate } from "@/lib/models/issued-client-certificates";
|
||||
import { StatusSummaryBar } from "./components/StatusSummaryBar";
|
||||
import { AcmeTab } from "./components/AcmeTab";
|
||||
import { ImportedTab } from "./components/ImportedTab";
|
||||
import { CaTab } from "./components/CaTab";
|
||||
import { MtlsRolesTab } from "@/components/mtls-roles/MtlsRolesTab";
|
||||
|
||||
type TabId = "acme" | "imported" | "ca";
|
||||
type TabId = "acme" | "imported" | "ca" | "roles";
|
||||
|
||||
type Props = {
|
||||
acmeHosts: AcmeHost[];
|
||||
@@ -18,6 +20,8 @@ type Props = {
|
||||
managedCerts: ManagedCertView[];
|
||||
caCertificates: CaCertificateView[];
|
||||
acmePagination: { total: number; page: number; perPage: number };
|
||||
mtlsRoles: MtlsRole[];
|
||||
issuedClientCerts: IssuedClientCertificate[];
|
||||
};
|
||||
|
||||
function countExpiry(statuses: (CertExpiryStatus | null)[]) {
|
||||
@@ -36,11 +40,14 @@ export default function CertificatesClient({
|
||||
managedCerts,
|
||||
caCertificates,
|
||||
acmePagination,
|
||||
mtlsRoles,
|
||||
issuedClientCerts,
|
||||
}: Props) {
|
||||
const [activeTab, setActiveTab] = useState<TabId>("acme");
|
||||
const [searchAcme, setSearchAcme] = useState("");
|
||||
const [searchImported, setSearchImported] = useState("");
|
||||
const [searchCa, setSearchCa] = useState("");
|
||||
const [searchRoles, setSearchRoles] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
||||
|
||||
const allStatuses: (CertExpiryStatus | null)[] = [
|
||||
@@ -48,8 +55,8 @@ export default function CertificatesClient({
|
||||
];
|
||||
const { expired, expiringSoon, healthy } = countExpiry(allStatuses);
|
||||
|
||||
const search = activeTab === "acme" ? searchAcme : activeTab === "imported" ? searchImported : searchCa;
|
||||
const setSearch = activeTab === "acme" ? setSearchAcme : activeTab === "imported" ? setSearchImported : setSearchCa;
|
||||
const search = activeTab === "acme" ? searchAcme : activeTab === "imported" ? searchImported : activeTab === "roles" ? searchRoles : searchCa;
|
||||
const setSearch = activeTab === "acme" ? setSearchAcme : activeTab === "imported" ? setSearchImported : activeTab === "roles" ? setSearchRoles : setSearchCa;
|
||||
|
||||
function handleTabChange(value: string) {
|
||||
setActiveTab(value as TabId);
|
||||
@@ -94,6 +101,12 @@ export default function CertificatesClient({
|
||||
{caCertificates.length}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="roles" className="gap-1.5">
|
||||
Roles
|
||||
<span className="rounded-full bg-muted px-1.5 py-0 text-xs font-bold tabular-nums">
|
||||
{mtlsRoles.length}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<SearchField
|
||||
@@ -134,6 +147,13 @@ export default function CertificatesClient({
|
||||
statusFilter={statusFilter}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="roles" className="mt-4">
|
||||
<MtlsRolesTab
|
||||
roles={mtlsRoles}
|
||||
issuedCerts={issuedClientCerts}
|
||||
search={searchRoles}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,9 +6,11 @@ import { requireAdmin } from '@/src/lib/auth';
|
||||
import CertificatesClient from './CertificatesClient';
|
||||
import { listCaCertificates, type CaCertificate } from '@/src/lib/models/ca-certificates';
|
||||
import { listIssuedClientCertificates, type IssuedClientCertificate } from '@/src/lib/models/issued-client-certificates';
|
||||
import { listMtlsRoles, type MtlsRole } from '@/src/lib/models/mtls-roles';
|
||||
|
||||
export type { CaCertificate };
|
||||
export type { IssuedClientCertificate };
|
||||
export type { MtlsRole };
|
||||
|
||||
export type CaCertificateView = CaCertificate & {
|
||||
issuedCerts: IssuedClientCertificate[];
|
||||
@@ -83,8 +85,9 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
|
||||
const offset = (page - 1) * PER_PAGE;
|
||||
const [caCerts, issuedClientCerts] = await Promise.all([
|
||||
listCaCertificates(),
|
||||
listIssuedClientCertificates()
|
||||
listIssuedClientCertificates(),
|
||||
]);
|
||||
const mtlsRoles = await listMtlsRoles().catch(() => []);
|
||||
|
||||
const [acmeRows, acmeTotal, certRows, usageRows] = await Promise.all([
|
||||
db
|
||||
@@ -176,6 +179,8 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
|
||||
managedCerts={managedCerts}
|
||||
caCertificates={caCertificateViews}
|
||||
acmePagination={{ total: acmeTotal, page, perPage: PER_PAGE }}
|
||||
mtlsRoles={mtlsRoles}
|
||||
issuedClientCerts={issuedClientCerts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { Certificate } from "@/lib/models/certificates";
|
||||
import type { ProxyHost } from "@/lib/models/proxy-hosts";
|
||||
import type { CaCertificate } from "@/lib/models/ca-certificates";
|
||||
import type { AuthentikSettings } from "@/lib/settings";
|
||||
import type { MtlsRole } from "@/lib/models/mtls-roles";
|
||||
import type { IssuedClientCertificate } from "@/lib/models/issued-client-certificates";
|
||||
import { toggleProxyHostAction } from "./actions";
|
||||
import { PageHeader } from "@/components/ui/PageHeader";
|
||||
import { SearchField } from "@/components/ui/SearchField";
|
||||
@@ -35,13 +37,17 @@ type Props = {
|
||||
pagination: { total: number; page: number; perPage: number };
|
||||
initialSearch: string;
|
||||
initialSort?: { sortBy: string; sortDir: "asc" | "desc" };
|
||||
mtlsRoles?: MtlsRole[];
|
||||
issuedClientCerts?: IssuedClientCertificate[];
|
||||
};
|
||||
|
||||
export default function ProxyHostsClient({ hosts, certificates, accessLists, caCertificates, authentikDefaults, pagination, initialSearch, initialSort }: Props) {
|
||||
export default function ProxyHostsClient({ hosts, certificates, accessLists, caCertificates, authentikDefaults, pagination, initialSearch, initialSort, mtlsRoles, issuedClientCerts }: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [duplicateHost, setDuplicateHost] = useState<ProxyHost | null>(null);
|
||||
const [editHost, setEditHost] = useState<ProxyHost | null>(null);
|
||||
const [deleteHost, setDeleteHost] = useState<ProxyHost | null>(null);
|
||||
// Counter forces CreateHostDialog to remount on each open, resetting useFormState
|
||||
const [dialogKey, setDialogKey] = useState(0);
|
||||
const [searchTerm, setSearchTerm] = useState(initialSearch);
|
||||
|
||||
const router = useRouter();
|
||||
@@ -200,7 +206,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setEditHost(host)}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setDuplicateHost(host); setCreateOpen(true); }}>Duplicate</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setDuplicateHost(host); { setDialogKey(k => k + 1); setCreateOpen(true); }; }}>Duplicate</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
@@ -248,7 +254,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setEditHost(host)}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setDuplicateHost(host); setCreateOpen(true); }}>Duplicate</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setDuplicateHost(host); { setDialogKey(k => k + 1); setCreateOpen(true); }; }}>Duplicate</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive focus:text-destructive" onClick={() => setDeleteHost(host)}>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -264,7 +270,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
<PageHeader
|
||||
title="Proxy Hosts"
|
||||
description="Define HTTP(S) reverse proxies orchestrated by Caddy with automated certificates."
|
||||
action={{ label: "Create Host", onClick: () => setCreateOpen(true) }}
|
||||
action={{ label: "Create Host", onClick: () => { setDialogKey(k => k + 1); setCreateOpen(true); } }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -287,6 +293,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
/>
|
||||
|
||||
<CreateHostDialog
|
||||
key={dialogKey}
|
||||
open={createOpen}
|
||||
onClose={() => { setCreateOpen(false); setTimeout(() => setDuplicateHost(null), 200); }}
|
||||
initialData={duplicateHost}
|
||||
@@ -294,6 +301,8 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
accessLists={accessLists}
|
||||
authentikDefaults={authentikDefaults}
|
||||
caCertificates={caCertificates}
|
||||
mtlsRoles={mtlsRoles ?? []}
|
||||
issuedClientCerts={issuedClientCerts ?? []}
|
||||
/>
|
||||
|
||||
{editHost && (
|
||||
@@ -304,6 +313,8 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
certificates={certificates}
|
||||
accessLists={accessLists}
|
||||
caCertificates={caCertificates}
|
||||
mtlsRoles={mtlsRoles ?? []}
|
||||
issuedClientCerts={issuedClientCerts ?? []}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -394,8 +394,9 @@ function parseMtlsConfig(formData: FormData): MtlsConfig | null {
|
||||
if (!formData.has("mtls_present")) return null;
|
||||
const enabled = formData.get("mtls_enabled") === "true";
|
||||
if (!enabled) return null;
|
||||
const ids = formData.getAll("mtls_ca_cert_id").map(Number).filter(n => Number.isFinite(n) && n > 0);
|
||||
return { enabled, ca_certificate_ids: ids };
|
||||
const certIds = formData.getAll("mtls_cert_id").map(Number).filter(n => Number.isFinite(n) && n > 0);
|
||||
const roleIds = formData.getAll("mtls_role_id").map(Number).filter(n => Number.isFinite(n) && n > 0);
|
||||
return { enabled, trusted_client_cert_ids: certIds, trusted_role_ids: roleIds };
|
||||
}
|
||||
|
||||
function parseRedirectsConfig(formData: FormData): RedirectRule[] | null {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { listCertificates } from "@/src/lib/models/certificates";
|
||||
import { listCaCertificates } from "@/src/lib/models/ca-certificates";
|
||||
import { listAccessLists } from "@/src/lib/models/access-lists";
|
||||
import { getAuthentikSettings } from "@/src/lib/settings";
|
||||
import { listMtlsRoles } from "@/src/lib/models/mtls-roles";
|
||||
import { listIssuedClientCertificates } from "@/src/lib/models/issued-client-certificates";
|
||||
import { requireAdmin } from "@/src/lib/auth";
|
||||
|
||||
const PER_PAGE = 25;
|
||||
@@ -29,6 +31,11 @@ export default async function ProxyHostsPage({ searchParams }: PageProps) {
|
||||
listAccessLists(),
|
||||
getAuthentikSettings(),
|
||||
]);
|
||||
// These are safe to fail if the RBAC migration hasn't been applied yet
|
||||
const [mtlsRoles, issuedClientCerts] = await Promise.all([
|
||||
listMtlsRoles().catch(() => []),
|
||||
listIssuedClientCertificates().catch(() => []),
|
||||
]);
|
||||
|
||||
return (
|
||||
<ProxyHostsClient
|
||||
@@ -40,6 +47,8 @@ export default async function ProxyHostsPage({ searchParams }: PageProps) {
|
||||
pagination={{ total, page, perPage: PER_PAGE }}
|
||||
initialSearch={search ?? ""}
|
||||
initialSort={{ sortBy: sortBy ?? "created_at", sortDir }}
|
||||
mtlsRoles={mtlsRoles}
|
||||
issuedClientCerts={issuedClientCerts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
17
app/api/v1/client-certificates/[id]/roles/route.ts
Normal file
17
app/api/v1/client-certificates/[id]/roles/route.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAdmin, apiErrorResponse } from "@/src/lib/api-auth";
|
||||
import { getCertificateRoles } from "@/src/lib/models/mtls-roles";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
await requireApiAdmin(request);
|
||||
const { id } = await params;
|
||||
const roles = await getCertificateRoles(Number(id));
|
||||
return NextResponse.json(roles);
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
17
app/api/v1/mtls-roles/[id]/certificates/[certId]/route.ts
Normal file
17
app/api/v1/mtls-roles/[id]/certificates/[certId]/route.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAdmin, apiErrorResponse } from "@/src/lib/api-auth";
|
||||
import { removeRoleFromCertificate } from "@/src/lib/models/mtls-roles";
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; certId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const { id, certId } = await params;
|
||||
await removeRoleFromCertificate(Number(id), Number(certId), userId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
22
app/api/v1/mtls-roles/[id]/certificates/route.ts
Normal file
22
app/api/v1/mtls-roles/[id]/certificates/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAdmin, apiErrorResponse } from "@/src/lib/api-auth";
|
||||
import { assignRoleToCertificate, getMtlsRole } from "@/src/lib/models/mtls-roles";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
if (!body.certificate_id || typeof body.certificate_id !== "number") {
|
||||
return NextResponse.json({ error: "certificate_id is required" }, { status: 400 });
|
||||
}
|
||||
await assignRoleToCertificate(Number(id), body.certificate_id, userId);
|
||||
const role = await getMtlsRole(Number(id));
|
||||
return NextResponse.json(role, { status: 201 });
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
49
app/api/v1/mtls-roles/[id]/route.ts
Normal file
49
app/api/v1/mtls-roles/[id]/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAdmin, apiErrorResponse } from "@/src/lib/api-auth";
|
||||
import { getMtlsRole, updateMtlsRole, deleteMtlsRole } from "@/src/lib/models/mtls-roles";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
await requireApiAdmin(request);
|
||||
const { id } = await params;
|
||||
const role = await getMtlsRole(Number(id));
|
||||
if (!role) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(role);
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const role = await updateMtlsRole(Number(id), body, userId);
|
||||
return NextResponse.json(role);
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const { id } = await params;
|
||||
await deleteMtlsRole(Number(id), userId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
27
app/api/v1/mtls-roles/route.ts
Normal file
27
app/api/v1/mtls-roles/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAdmin, apiErrorResponse } from "@/src/lib/api-auth";
|
||||
import { listMtlsRoles, createMtlsRole } from "@/src/lib/models/mtls-roles";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
await requireApiAdmin(request);
|
||||
const roles = await listMtlsRoles();
|
||||
return NextResponse.json(roles);
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const body = await request.json();
|
||||
if (!body.name || typeof body.name !== "string" || !body.name.trim()) {
|
||||
return NextResponse.json({ error: "name is required" }, { status: 400 });
|
||||
}
|
||||
const role = await createMtlsRole(body, userId);
|
||||
return NextResponse.json(role, { status: 201 });
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAdmin, apiErrorResponse } from "@/src/lib/api-auth";
|
||||
import { getMtlsAccessRule, updateMtlsAccessRule, deleteMtlsAccessRule } from "@/src/lib/models/mtls-access-rules";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; ruleId: string }> }
|
||||
) {
|
||||
try {
|
||||
await requireApiAdmin(request);
|
||||
const { ruleId } = await params;
|
||||
const rule = await getMtlsAccessRule(Number(ruleId));
|
||||
if (!rule) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(rule);
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; ruleId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const { ruleId } = await params;
|
||||
const body = await request.json();
|
||||
const rule = await updateMtlsAccessRule(Number(ruleId), body, userId);
|
||||
return NextResponse.json(rule);
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; ruleId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const { ruleId } = await params;
|
||||
await deleteMtlsAccessRule(Number(ruleId), userId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
38
app/api/v1/proxy-hosts/[id]/mtls-access-rules/route.ts
Normal file
38
app/api/v1/proxy-hosts/[id]/mtls-access-rules/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAdmin, apiErrorResponse } from "@/src/lib/api-auth";
|
||||
import { listMtlsAccessRules, createMtlsAccessRule } from "@/src/lib/models/mtls-access-rules";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
await requireApiAdmin(request);
|
||||
const { id } = await params;
|
||||
const rules = await listMtlsAccessRules(Number(id));
|
||||
return NextResponse.json(rules);
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await requireApiAdmin(request);
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
if (!body.path_pattern || typeof body.path_pattern !== "string" || !body.path_pattern.trim()) {
|
||||
return NextResponse.json({ error: "path_pattern is required" }, { status: 400 });
|
||||
}
|
||||
const rule = await createMtlsAccessRule(
|
||||
{ ...body, proxy_host_id: Number(id) },
|
||||
userId
|
||||
);
|
||||
return NextResponse.json(rule, { status: 201 });
|
||||
} catch (error) {
|
||||
return apiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user