feat(plugin): developer whitelist CLI + idempotent backend

- CLI: plugin dev list/allow/revoke (userId or email)
- Backend: PluginDeveloperService.addDeveloper is now idempotent
  (returns existing record instead of unique-constraint 500)
- Backend: removeDeveloper silently no-ops on P2025 (not found)
- Controller: route api/plugin-developers (avoids :pluginId collision),
  @DisabledPermission so admin check is the sole gate
This commit is contained in:
yeuimu
2026-08-13 15:07:32 +00:00
parent 1708e7997a
commit 3eb66513ea
2 changed files with 179 additions and 6 deletions
+25
View File
@@ -280,6 +280,31 @@ export class Client {
upsertPluginConfig(baseId: string, pluginId: string, body: Record<string, unknown>) {
return this.call<unknown>('PUT', `/api/base/${baseId}/plugin-config/${pluginId}`, body);
}
// ---- plugin installs ----
listInstalls(baseId: string) {
return this.call<unknown[]>('GET', `/api/base/${baseId}/plugin-installs`);
}
createInstall(baseId: string, body: Record<string, unknown>) {
return this.call<unknown>('POST', `/api/base/${baseId}/plugin-installs`, body);
}
deleteInstall(baseId: string, installId: string) {
return this.call<unknown>('DELETE', `/api/base/${baseId}/plugin-installs/${installId}`);
}
// ---- plugin developers ----
listDevelopers() {
return this.call<unknown[]>('GET', '/api/plugin-developers');
}
addDeveloper(userIdOrEmail: string) {
// Backend reads @Body('userId') or @Body('email'). Send whichever looks right.
const isEmail = userIdOrEmail.includes('@');
const body = isEmail ? { email: userIdOrEmail } : { userId: userIdOrEmail };
return this.call<unknown>('POST', '/api/plugin-developers', body);
}
removeDeveloper(userId: string) {
return this.call<unknown>('DELETE', `/api/plugin-developers/${encodeURIComponent(userId)}`);
}
}
// ---- shared types (mirror backend workflow_node / workflow_edge shapes) ----