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:
co-authored by
Claude Opus 4.8
parent
5a729abd71
commit
5e435c5f77
@@ -75,6 +75,8 @@ const F = {
|
||||
autodate: (name, onCreate = false, onUpdate = false) => ({ name, type: "autodate", required: false, onCreate, onUpdate }),
|
||||
// Single-file attachment. maxSize is in bytes; mimeTypes [] means "any".
|
||||
file: (name, maxSize, mimeTypes = []) => ({ name, type: "file", required: false, maxSize, mimeTypes }),
|
||||
// Free-form JSON blob. maxSize is in bytes.
|
||||
json: (name, maxSize = 100000) => ({ name, type: "json", required: false, maxSize }),
|
||||
};
|
||||
|
||||
function renderField(def, format, idByName) {
|
||||
@@ -96,6 +98,9 @@ function renderField(def, format, idByName) {
|
||||
options.maxSize = def.maxSize;
|
||||
options.mimeTypes = def.mimeTypes || [];
|
||||
}
|
||||
if (def.type === "json") {
|
||||
options.maxSize = def.maxSize;
|
||||
}
|
||||
return { name: def.name, type: def.type, required: def.required, options };
|
||||
}
|
||||
// Modern: options flattened onto the field.
|
||||
@@ -119,6 +124,9 @@ function renderField(def, format, idByName) {
|
||||
field.maxSize = def.maxSize;
|
||||
field.mimeTypes = def.mimeTypes || [];
|
||||
}
|
||||
if (def.type === "json") {
|
||||
field.maxSize = def.maxSize;
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -380,6 +388,12 @@ const DESIRED = {
|
||||
organizations: [
|
||||
F.text("name", true),
|
||||
F.autodate("created", true, false),
|
||||
// Per-organization plugin/integration config — the middle (org admin) layer
|
||||
// of the integration cascade (API Server → org admin → user). Shape:
|
||||
// { "<plugin>": { "config": {…}, "disabled": bool } }
|
||||
// See internal/api/integrations.go. Only meaningful for plugins that expose
|
||||
// a per-user cascade (today: toyota).
|
||||
F.json("pluginSettings"),
|
||||
],
|
||||
// Custom fields layered onto the built-in "users" auth collection (which
|
||||
// already ships with email/name/avatar). Settings-panel additions:
|
||||
@@ -404,6 +418,11 @@ const DESIRED = {
|
||||
// not delete its people. (The API refuses to delete an org that still has
|
||||
// members, so this should not arise in practice.)
|
||||
F.relation("organization", "organizations", false, false),
|
||||
// Per-user plugin/integration config — the bottom (user) layer of the
|
||||
// integration cascade. Shape:
|
||||
// { "<plugin>": { "config": {…}, "enabled": bool } }
|
||||
// The `enabled` flag is the personal opt-in; see internal/api/integrations.go.
|
||||
F.json("pluginSettings"),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user