Add per-user Toyota integration with a settings cascade

Let each user run the Toyota Connected plugin under their own MyToyota
credentials and enable/disable it for themselves in the Web App, while a
superadmin (and, in an organization, an org admin) can impose settings
from above. Resolution is a cascade — top wins, and a lower level only
fills fields the levels above left blank:

  - org user:      API Server (superadmin) -> org admin -> user
  - org-less user: API Server (superadmin) -> user

The MyToyota email + password resolve together as a pair from the highest
layer that supplies an email; brand resolves on its own; enablement is
strictly per-user, gated by the global master switch and the org gate.

API Server:
  - plugins.Manager gains RawConfig / HealthCheckWith / InvokeWith so the
    cascade can read global config and probe/invoke under a per-caller
    resolved config.
  - internal/api/integrations.go resolves the cascade and serves
    GET/PUT /api/integrations/toyota, POST .../health, GET .../vehicles.
    Secrets and inherited usernames are masked before leaving the server.
  - The toyota builtin's credentials are no longer required at the global
    layer, so the master switch can be enabled without global credentials.
  - setup-pocketbase.mjs adds a pluginSettings JSON field to the users and
    organizations collections (the user and org layers of the cascade).

Web App:
  - api.js gains getToyota/saveToyota/testToyota.
  - Settings grows an Integrations section: an enable toggle, credential
    fields with locked / "inherited from" states, a brand select, an
    org-scope switch for admins, and a live test-connection button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-18 11:08:58 +02:00
co-authored by Claude Opus 4.8
parent 5a729abd71
commit 5e435c5f77
9 changed files with 910 additions and 11 deletions
+9
View File
@@ -222,6 +222,15 @@ export const api = {
deleteAvatar: () => request("/me/avatar", { method: "DELETE" }),
getAvatarBlob: () => requestBlob("/me/avatar"),
// Integrations — per-user plugin settings under the superadmin → org admin →
// user cascade. getToyota returns the resolved view (effective/own/locked per
// field, with secrets and inherited usernames masked); saveToyota writes the
// caller's editable layer (scope "user" by default, "org" for org admins);
// testToyota runs a live login probe under the resolved credentials.
getToyota: () => request("/integrations/toyota"),
saveToyota: (body) => request("/integrations/toyota", { method: "PUT", body: JSON.stringify(body) }),
testToyota: () => request("/integrations/toyota/health", { method: "POST" }),
// Settings — advanced / danger zone
exportData: () => requestBlob("/me/export"),
importData: (payload) => request("/me/import", { method: "POST", body: JSON.stringify(payload) }),