diff --git a/frontend/src/pages/settings/Integrations.jsx b/frontend/src/pages/settings/Integrations.jsx index 1d3d493..c8df57d 100644 --- a/frontend/src/pages/settings/Integrations.jsx +++ b/frontend/src/pages/settings/Integrations.jsx @@ -28,6 +28,8 @@ const Integrations = () => { const [host_groups, setHostGroups] = useState([]); const [loading, setLoading] = useState(true); const [show_create_modal, setShowCreateModal] = useState(false); + const [show_edit_modal, setShowEditModal] = useState(false); + const [edit_token, setEditToken] = useState(null); const [new_token, setNewToken] = useState(null); const [show_secret, setShowSecret] = useState(false); const [server_url, setServerUrl] = useState(""); @@ -40,6 +42,9 @@ const Integrations = () => { default_host_group_id: "", allowed_ip_ranges: "", expires_at: "", + scopes: { + host: [], + }, }); const [copy_success, setCopySuccess] = useState({}); @@ -54,6 +59,25 @@ const Integrations = () => { setActiveTab(tabName); }; + const toggle_scope_action = (resource, action) => { + setFormData((prev) => { + const current_scopes = prev.scopes || { [resource]: [] }; + const resource_scopes = current_scopes[resource] || []; + + const updated_scopes = resource_scopes.includes(action) + ? resource_scopes.filter((a) => a !== action) + : [...resource_scopes, action]; + + return { + ...prev, + scopes: { + ...current_scopes, + [resource]: updated_scopes, + }, + }; + }); + }; + // biome-ignore lint/correctness/useExhaustiveDependencies: Only run on mount useEffect(() => { load_tokens(); @@ -96,6 +120,14 @@ const Integrations = () => { e.preventDefault(); try { + // Determine integration type based on active tab + let integration_type = "proxmox-lxc"; + if (activeTab === "gethomepage") { + integration_type = "gethomepage"; + } else if (activeTab === "api") { + integration_type = "api"; + } + const data = { token_name: form_data.token_name, max_hosts_per_day: Number.parseInt(form_data.max_hosts_per_day, 10), @@ -103,8 +135,7 @@ const Integrations = () => { ? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim()) : [], metadata: { - integration_type: - activeTab === "gethomepage" ? "gethomepage" : "proxmox-lxc", + integration_type: integration_type, }, }; @@ -116,6 +147,11 @@ const Integrations = () => { data.expires_at = form_data.expires_at; } + // Add scopes for API credentials + if (activeTab === "api" && form_data.scopes) { + data.scopes = form_data.scopes; + } + const response = await api.post("/auto-enrollment/tokens", data); setNewToken(response.data.token); setShowCreateModal(false); @@ -128,6 +164,9 @@ const Integrations = () => { default_host_group_id: "", allowed_ip_ranges: "", expires_at: "", + scopes: { + host: [], + }, }); } catch (error) { console.error("Failed to create token:", error); @@ -168,6 +207,69 @@ const Integrations = () => { } }; + const open_edit_modal = (token) => { + setEditToken(token); + setFormData({ + token_name: token.token_name, + max_hosts_per_day: token.max_hosts_per_day || 100, + default_host_group_id: token.default_host_group_id || "", + allowed_ip_ranges: token.allowed_ip_ranges?.join(", ") || "", + expires_at: token.expires_at + ? new Date(token.expires_at).toISOString().slice(0, 16) + : "", + scopes: token.scopes || { host: [] }, + }); + setShowEditModal(true); + }; + + const update_token = async (e) => { + e.preventDefault(); + + try { + const data = { + allowed_ip_ranges: form_data.allowed_ip_ranges + ? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim()) + : [], + }; + + // Add expiration if provided + if (form_data.expires_at) { + data.expires_at = form_data.expires_at; + } + + // Add scopes for API credentials + if ( + edit_token?.metadata?.integration_type === "api" && + form_data.scopes + ) { + data.scopes = form_data.scopes; + } + + await api.patch(`/auto-enrollment/tokens/${edit_token.id}`, data); + setShowEditModal(false); + setEditToken(null); + load_tokens(); + + // Reset form + setFormData({ + token_name: "", + max_hosts_per_day: 100, + default_host_group_id: "", + allowed_ip_ranges: "", + expires_at: "", + scopes: { + host: [], + }, + }); + } catch (error) { + console.error("Failed to update token:", error); + const error_message = error.response?.data?.errors + ? error.response.data.errors.map((e) => e.msg).join(", ") + : error.response?.data?.error || "Failed to update token"; + alert(error_message); + } + }; + const copy_to_clipboard = async (text, key) => { // Check if Clipboard API is available if (navigator.clipboard && window.isSecureContext) { @@ -256,6 +358,17 @@ const Integrations = () => { > GetHomepage + + + + {/* API Credentials List */} + {loading ? ( +
+
+
+ ) : tokens.filter( + (token) => token.metadata?.integration_type === "api", + ).length === 0 ? ( +
+

No API credentials created yet.

+

+ Create a credential to enable programmatic access to + PatchMon. +

+
+ ) : ( +
+ {tokens + .filter( + (token) => token.metadata?.integration_type === "api", + ) + .map((token) => ( +
+
+
+
+

+ {token.token_name} +

+ + API + + {token.is_active ? ( + + Active + + ) : ( + + Inactive + + )} +
+
+
+ + {token.token_key} + + +
+ {token.scopes && ( +

+ Scopes:{" "} + {Object.entries(token.scopes) + .map( + ([resource, actions]) => + `${resource}: ${Array.isArray(actions) ? actions.join(", ") : actions}`, + ) + .join(" | ")} +

+ )} + {token.allowed_ip_ranges?.length > 0 && ( +

+ Allowed IPs:{" "} + {token.allowed_ip_ranges.join(", ")} +

+ )} +

Created: {format_date(token.created_at)}

+ {token.last_used_at && ( +

+ Last Used: {format_date(token.last_used_at)} +

+ )} + {token.expires_at && ( +

+ Expires: {format_date(token.expires_at)} + {new Date(token.expires_at) < + new Date() && ( + + (Expired) + + )} +

+ )} +
+
+
+ + + +
+
+
+ ))} +
+ )} + + {/* Documentation Section */} +
+

+ Using API Credentials +

+
+

+ API credentials allow you to programmatically access + PatchMon data using Basic Authentication. +

+
+

+ Example cURL Request: +

+
+ curl -u "YOUR_API_KEY:YOUR_API_SECRET" \
+   {server_url}/api/v1/api/hosts +
+
+
+

+ Query Hosts by Group: +

+
+ curl -u "YOUR_API_KEY:YOUR_API_SECRET" \
+   "{server_url} + /api/v1/api/hosts?hostgroup=Production,Development" +
+
+

+ 💡 Tip: You can filter by host group + names or UUIDs. Multiple groups can be specified as a + comma-separated list. +

+
+
+
+ )} + {/* Docker Tab */} {activeTab === "docker" && (
@@ -885,7 +1206,9 @@ const Integrations = () => {

{activeTab === "gethomepage" ? "Create GetHomepage API Key" - : "Create Auto-Enrollment Token"} + : activeTab === "api" + ? "Create API Credential" + : "Create Auto-Enrollment Token"}

+ + +
+

+ Filter by host group: +

+
+ + +
+
+ +

+ 💡 Replace "Production" with your host group name or UUID +

+ + )} + {activeTab === "proxmox" && (
@@ -1371,6 +1845,154 @@ const Integrations = () => {
)} + + {/* Edit API Credential Modal */} + {show_edit_modal && edit_token && ( +
+
+
+
+

+ Edit API Credential +

+ +
+ +
+
+ + Token Name + + +

+ Token name cannot be changed +

+
+ + {edit_token?.metadata?.integration_type === "api" && ( +
+ + Scopes + +
+
+

+ Host Permissions +

+
+ {["get", "put", "patch", "update", "delete"].map( + (action) => ( + + ), + )} +
+
+
+

+ Update the permissions for this API credential +

+
+ )} + + + + + +
+ + +
+
+
+
+
+ )} ); };