Add editing existing integrations, so an expired secret can be rotated
Manage integrations only supported add/toggle/delete — fixing an expired API token meant deleting and recreating the whole integration. - New GET /api/integrations/:id/config returns only the non-secret config fields (never the decrypted secret) so an edit form can pre-fill URL/tailnet/etc. fields. - New POST /api/integrations/:id/test merges the stored, decrypted config with any freshly-typed overrides and pings the real adapter — lets "Test connection" work during an edit without ever sending the current secret back to the browser. - New IntegrationEditForm component: secret fields render blank with a "leave blank to keep the current value" placeholder; submitting only sends the fields that were actually filled in, so a name/URL edit can't accidentally wipe a secret and a secret rotation can't touch anything else. Reuses the existing PATCH /:id route, which already merged partial config updates correctly. Verified end-to-end against the real dev server: confirmed via direct DB decryption that a non-secret-only edit leaves the stored secret byte-for-byte unchanged, and that a secret-only edit rotates it without touching other config; the test route was confirmed to make a real network call (got a genuine "API token invalid" from Tailscale's API against a fake key). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b9409d3095
commit
f5d3c25c89
5 files changed
+253
-5
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
type TailscaleDevicesResponse,
|
||||
} from "../api/client";
|
||||
import IntegrationForm from "../components/IntegrationForm";
|
||||
import IntegrationEditForm from "../components/IntegrationEditForm";
|
||||
|
||||
function proxmoxStatusBadge(status: string) {
|
||||
return status === "running" ? (
|
||||
@@ -135,6 +136,7 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
|
||||
const [integrations, setIntegrations] = useState<IntegrationSummary[] | null>(null);
|
||||
const [managing, setManaging] = useState(false);
|
||||
const [editingIntegration, setEditingIntegration] = useState<IntegrationSummary | null>(null);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -388,7 +390,18 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
|
||||
{managing ? (
|
||||
<>
|
||||
<IntegrationForm onCreated={loadIntegrations} onCancel={() => setManaging(false)} />
|
||||
{editingIntegration ? (
|
||||
<IntegrationEditForm
|
||||
integration={editingIntegration}
|
||||
onSaved={() => {
|
||||
setEditingIntegration(null);
|
||||
loadIntegrations();
|
||||
}}
|
||||
onCancel={() => setEditingIntegration(null)}
|
||||
/>
|
||||
) : (
|
||||
<IntegrationForm onCreated={loadIntegrations} onCancel={() => setManaging(false)} />
|
||||
)}
|
||||
<div className="card">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter card-table">
|
||||
@@ -414,9 +427,14 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button className="btn btn-sm btn-outline-danger" onClick={() => removeIntegration(i)}>
|
||||
Delete
|
||||
</button>
|
||||
<div className="btn-list flex-nowrap">
|
||||
<button className="btn btn-sm" onClick={() => setEditingIntegration(i)}>
|
||||
Edit
|
||||
</button>
|
||||
<button className="btn btn-sm btn-outline-danger" onClick={() => removeIntegration(i)}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user