diff --git a/admin-web/src/api/client.ts b/admin-web/src/api/client.ts index fe2d523..93fff36 100644 --- a/admin-web/src/api/client.ts +++ b/admin-web/src/api/client.ts @@ -55,6 +55,15 @@ export type Policy = { full_tunnel: boolean; is_active: boolean; destinations?: string[]; + services?: Array<{ + id: string; + name: string; + domain: string; + upstream_ip: string; + proxy_ip: string; + ports: number[]; + description: string; + }>; targets?: Array<{ type: string; id: string; @@ -62,6 +71,17 @@ export type Policy = { }>; }; +export type ServiceCatalogItem = { + id: string; + name: string; + description: string; + domain: string; + upstream_ip: string; + proxy_ip: string; + ports: number[]; + is_active: boolean; +}; + export type Group = { id: string; name: string; @@ -168,6 +188,37 @@ export const api = { method: "DELETE" }), devices: () => request("/admin/devices"), + services: () => request("/admin/services"), + createService: (payload: { + name: string; + description: string; + domain: string; + upstream_ip: string; + proxy_ip: string; + ports: number[]; + is_active?: boolean; + }) => + request("/admin/services", { + method: "POST", + body: JSON.stringify(payload) + }), + updateService: (serviceId: string, payload: { + name?: string; + description?: string; + domain?: string; + upstream_ip?: string; + proxy_ip?: string; + ports?: number[]; + is_active?: boolean; + }) => + request(`/admin/services/${serviceId}`, { + method: "PATCH", + body: JSON.stringify(payload) + }), + deleteService: (serviceId: string) => + request<{ ok: boolean }>(`/admin/services/${serviceId}`, { + method: "DELETE" + }), deviceProfile: (deviceId: string) => request(`/admin/devices/${deviceId}/profile`), revokeDevice: (deviceId: string) => request<{ ok: boolean }>(`/admin/devices/${deviceId}/revoke`, { @@ -187,6 +238,7 @@ export const api = { effect: string; full_tunnel: boolean; destinations: string[]; + service_ids: string[]; targets: Array<{ type: string; id: string }>; }) => request("/admin/policies", { @@ -201,6 +253,7 @@ export const api = { full_tunnel?: boolean; is_active?: boolean; destinations?: string[]; + service_ids?: string[]; targets?: Array<{ type: string; id: string }>; }) => request(`/admin/policies/${policyId}`, { diff --git a/admin-web/src/app/App.tsx b/admin-web/src/app/App.tsx index 60e9f41..8366d4e 100644 --- a/admin-web/src/app/App.tsx +++ b/admin-web/src/app/App.tsx @@ -10,6 +10,7 @@ import { DevicesPage } from "../features/devices/DevicesPage"; import { GatewaysPage } from "../features/gateways/GatewaysPage"; import { GroupsPage } from "../features/groups/GroupsPage"; import { PoliciesPage } from "../features/policies/PoliciesPage"; +import { ServicesPage } from "../features/services/ServicesPage"; import { SettingsPage } from "../features/settings/SettingsPage"; import { UsersPage } from "../features/users/UsersPage"; @@ -44,6 +45,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/admin-web/src/components/Layout.tsx b/admin-web/src/components/Layout.tsx index 4efc400..4e81ebd 100644 --- a/admin-web/src/components/Layout.tsx +++ b/admin-web/src/components/Layout.tsx @@ -5,6 +5,7 @@ const items = [ ["Users", "/users"], ["Groups", "/groups"], ["Devices", "/devices"], + ["Services", "/services"], ["Policies", "/policies"], ["Gateways", "/gateways"], ["Audit", "/audit"], diff --git a/admin-web/src/features/policies/PoliciesPage.tsx b/admin-web/src/features/policies/PoliciesPage.tsx index 2d054ec..24da969 100644 --- a/admin-web/src/features/policies/PoliciesPage.tsx +++ b/admin-web/src/features/policies/PoliciesPage.tsx @@ -28,10 +28,15 @@ export function PoliciesPage() { queryKey: ["groups"], queryFn: api.groups }); + const servicesQuery = useQuery({ + queryKey: ["services"], + queryFn: api.services + }); const [form, setForm] = useState({ name: "", description: "", destinations: "", + serviceIds: [] as string[], targetType: "user", targetIds: [] as string[], fullTunnel: false @@ -42,6 +47,7 @@ export function PoliciesPage() { name: "", description: "", destinations: "", + serviceIds: [] as string[], fullTunnel: false, isActive: true, targetType: "user", @@ -52,17 +58,17 @@ export function PoliciesPage() { mutationFn: api.createPolicy, onSuccess: () => { setCreateOpen(false); - setForm({ name: "", description: "", destinations: "", targetType: "user", targetIds: [], fullTunnel: false }); + setForm({ name: "", description: "", destinations: "", serviceIds: [], targetType: "user", targetIds: [], fullTunnel: false }); void queryClient.invalidateQueries({ queryKey: ["policies"] }); } }); const updateMutation = useMutation({ - mutationFn: ({ policyId, payload }: { policyId: string; payload: { name: string; description: string; destinations: string[]; full_tunnel: boolean; is_active: boolean; targets: Array<{ type: string; id: string }> } }) => + mutationFn: ({ policyId, payload }: { policyId: string; payload: { name: string; description: string; destinations: string[]; service_ids: string[]; full_tunnel: boolean; is_active: boolean; targets: Array<{ type: string; id: string }> } }) => api.updatePolicy(policyId, payload), onSuccess: () => { setEditingPolicyId(null); - setEditForm({ name: "", description: "", destinations: "", fullTunnel: false, isActive: true, targetType: "user", targetIds: [] }); + setEditForm({ name: "", description: "", destinations: "", serviceIds: [], fullTunnel: false, isActive: true, targetType: "user", targetIds: [] }); void queryClient.invalidateQueries({ queryKey: ["policies"] }); } }); @@ -78,12 +84,16 @@ export function PoliciesPage() { id: policy.id, name: policy.name, targets: policy.targets?.length ? policy.targets.map((target) => `${target.type}: ${target.name ?? target.id}`).join(", ") : "No targets", - destinations: policy.destinations?.join(", ") ?? (policy.full_tunnel ? "0.0.0.0/0" : "-"), + destinations: [ + ...(policy.services?.map((service) => service.name) ?? []), + ...(policy.destinations ?? []) + ].join(", ") || (policy.full_tunnel ? "0.0.0.0/0" : "-"), mode: policy.full_tunnel ? "Full tunnel" : "Split tunnel" })) ?? []; const selectableUsers = useMemo(() => usersQuery.data ?? [], [usersQuery.data]); const selectableGroups = useMemo(() => groupsQuery.data ?? [], [groupsQuery.data]); + const selectableServices = useMemo(() => servicesQuery.data ?? [], [servicesQuery.data]); const selectableTargets = form.targetType === "group" ? selectableGroups : selectableUsers; const editableTargets = editForm.targetType === "group" ? selectableGroups : selectableUsers; @@ -99,6 +109,7 @@ export function PoliciesPage() { effect: "allow", full_tunnel: form.fullTunnel, destinations: form.fullTunnel ? ["0.0.0.0/0"] : form.destinations.split(",").map((value) => value.trim()).filter(Boolean), + service_ids: form.serviceIds, targets: form.targetIds.map((id) => ({ type: form.targetType, id })) }); } @@ -114,6 +125,7 @@ export function PoliciesPage() { name: policy.name, description: policy.description, destinations: policy.destinations?.join(", ") ?? "", + serviceIds: policy.services?.map((service) => service.id) ?? [], fullTunnel: policy.full_tunnel, isActive: policy.is_active, targetType, @@ -132,6 +144,7 @@ export function PoliciesPage() { name: editForm.name, description: editForm.description, destinations: editForm.fullTunnel ? ["0.0.0.0/0"] : editForm.destinations.split(",").map((value) => value.trim()).filter(Boolean), + service_ids: editForm.serviceIds, full_tunnel: editForm.fullTunnel, is_active: editForm.isActive, targets: editForm.targetIds.map((id) => ({ type: editForm.targetType, id })) @@ -158,6 +171,25 @@ export function PoliciesPage() { })); } + function toggleService(id: string, editing = false) { + if (editing) { + setEditForm((value) => ({ + ...value, + serviceIds: value.serviceIds.includes(id) + ? value.serviceIds.filter((item) => item !== id) + : [...value.serviceIds, id] + })); + return; + } + + setForm((value) => ({ + ...value, + serviceIds: value.serviceIds.includes(id) + ? value.serviceIds.filter((item) => item !== id) + : [...value.serviceIds, id] + })); + } + return ( setForm((value) => ({ ...value, destinations: event.target.value }))} disabled={form.fullTunnel} /> +
+

Allowed services

+
+ {selectableServices.map((service) => ( + + ))} +
+