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
@@ -127,9 +127,14 @@ func (p *Plugin) Descriptor() plugins.Descriptor {
{ID: "service-history", Method: "GET", Endpoint: epServiceHistory, Description: "Dealer service history summary for a VIN."},
},
ConfigFields: []plugins.ConfigField{
{Key: "username", Label: "MyToyota email", Type: "text", Required: true,
// Credentials are intentionally NOT required at the global (panel) layer:
// this connector runs under each user's own MyToyota account, supplied in
// the Web App's per-user integration settings. A superadmin/org admin may
// still set a shared (e.g. fleet) account here that users inherit. See the
// cascade in internal/api/integrations.go.
{Key: "username", Label: "MyToyota email", Type: "text",
Help: "The email address for your MyToyota (Toyota Connected Europe) account."},
{Key: "password", Label: "MyToyota password", Type: "password", Required: true, Secret: true,
{Key: "password", Label: "MyToyota password", Type: "password", Secret: true,
Help: "Your MyToyota account password. Stored locally, sent only to Toyota's login endpoint."},
{Key: "brand", Label: "Brand", Type: "select", Default: "T",
Help: "Vehicle brand tied to the account.",
@@ -21,17 +21,23 @@ func TestDescriptor(t *testing.T) {
if len(d.Capabilities) == 0 {
t.Fatal("expected capabilities")
}
// Required credential fields must be present.
req := map[string]bool{}
// The credential + brand fields must be advertised. They are intentionally
// NOT required at the global layer — credentials come from the per-user
// cascade — and the password field must be marked secret.
fields := map[string]plugins.ConfigField{}
for _, f := range d.ConfigFields {
if f.Required {
req[f.Key] = true
fields[f.Key] = f
}
for _, k := range []string{"username", "password", "brand"} {
if _, ok := fields[k]; !ok {
t.Errorf("config field %q should be present", k)
}
}
for _, k := range []string{"username", "password"} {
if !req[k] {
t.Errorf("config field %q should be required", k)
}
if fields["username"].Required || fields["password"].Required {
t.Error("credentials must not be required at the global layer (per-user cascade)")
}
if !fields["password"].Secret {
t.Error("password field must be marked secret")
}
}