Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Vrij en open source onder de EUPL-licentie.

<settings>
<admin>OCA\OpenRegister\Settings\OpenRegisterAdmin</admin>
<admin>OCA\OpenRegister\Settings\IntegrationsAdminSettings</admin>
<admin-section>OCA\OpenRegister\Sections\OpenRegisterAdmin</admin-section>
</settings>

Expand Down
7 changes: 7 additions & 0 deletions lib/AppInfo/Application.php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] Double-registration: settings class registered in both info.xml and Application.php Bootstrap

IntegrationsAdminSettings is declared in appinfo/info.xml as an <admin> settings entry AND again registered as a service via $context->registerService() in Application.php (lines 24-35). Nextcloud's Bootstrap registers ISettings classes automatically from info.xml; manually calling registerService() for the same class will cause it to be instantiated a second time, potentially showing the admin page twice in the sidebar and doubling any side-effects. Remove the registerService() block — info.xml registration is sufficient and idiomatic.

Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,13 @@ function (ContainerInterface $container) {
}
);

// IntegrationsAdminSettings is declared in info.xml <admin> and
// resolved by Nextcloud's container — IntegrationRegistry +
// ExternalIntegrationRouter are already registered above and the
// remaining constructor deps (IAppManager / IURLGenerator /
// IL10N) are framework services NC autowires. No explicit
// registerService needed, mirroring OpenRegisterAdmin.

// IntegrationsCapability — surfaces the registry through the
// Nextcloud OCS capabilities endpoint, role-redacted per AD-17.
$context->registerService(
Expand Down
227 changes: 227 additions & 0 deletions lib/Settings/IntegrationsAdminSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<?php

/**
* IntegrationsAdminSettings — admin settings page that surfaces the
* pluggable integration registry.
*
* Renders a table of every registered IntegrationProvider with:
* - id / label / group
* - storage strategy (magic-column / link-table / external / query-time)
* - required NC app + isEnabled() result
* - OpenConnector source (for external providers) + auth status
* - "Test connection" action (for external providers)
* - "Configure" deep-link into OpenConnector's credential UI
*
* Per AD-15: OpenRegister hosts the unified admin surface; the
* actual credential flows live in OpenConnector. This page never
* touches credentials directly — it links out to the right
* OpenConnector source page.
*
* @category Settings
* @package OCA\OpenRegister\Settings
*
* @author Conduction Development Team <info@conduction.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* SPDX-License-Identifier: EUPL-1.2
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
*
* @link https://conduction.nl
*
* @spec openspec/changes/pluggable-integration-registry/tasks.md#task-23
*/

declare(strict_types=1);

namespace OCA\OpenRegister\Settings;

use OCA\OpenRegister\Service\Integration\ExternalIntegrationRouter;
use OCA\OpenRegister\Service\Integration\IntegrationProvider;
use OCA\OpenRegister\Service\Integration\IntegrationRegistry;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\ISettings;

/**
* Admin settings: integration registry overview + auth status.
*/
class IntegrationsAdminSettings implements ISettings
{

/**
* Constructor.
*
* @param IntegrationRegistry $registry Integration registry.
* @param ExternalIntegrationRouter $router External router (for probe()).
* @param IAppManager $appManager NC app manager.
* @param IURLGenerator $urlGenerator URL generator (Configure deep-link).
* @param IL10N $l10n Localisation.
*
* @return void
*/
public function __construct(
private IntegrationRegistry $registry,
private ExternalIntegrationRouter $router,
private IAppManager $appManager,
private IURLGenerator $urlGenerator,
private IL10N $l10n,

Check failure on line 70 in lib/Settings/IntegrationsAdminSettings.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Property OCA\OpenRegister\Settings\IntegrationsAdminSettings::$l10n is never read, only written.
) {
}//end __construct()

/**
* @inheritDoc
*/
public function getForm(): TemplateResponse
{
return new TemplateResponse(
appName: 'openregister',
templateName: 'settings/integrations-admin',
params: ['rows' => $this->buildRows()],
renderAs: 'admin'
);
}//end getForm()

/**
* @inheritDoc
*/
public function getSection(): string
{
return 'openregister';
}//end getSection()

/**
* @inheritDoc
*/
public function getPriority(): int
{
// Below OpenRegisterAdmin (which is the default landing
// surface) but above any future sections. 50 leaves room on
// both sides.
return 50;
}//end getPriority()

/**
* Build the array of integration descriptors the template renders.
*
* Each row contains everything the table needs:
* id, label, group, storage, requiredApp, enabled, status,
* authStatus, message, openConnectorSource, configureUrl.
*
* @return array<int,array<string,mixed>>
*/
private function buildRows(): array
{
$rows = [];
foreach ($this->registry->list() as $provider) {
$rows[] = $this->describe($provider);
}

return $rows;
}//end buildRows()

/**
* Describe a single provider as a renderable row.
*
* @param IntegrationProvider $provider Provider.
*
* @return array<string,mixed>
*/
private function describe(IntegrationProvider $provider): array
{
$requiredApp = $provider->getRequiredApp();
$isExternal = ($provider->getStorageStrategy() === 'external');
$health = $this->probeHealth($provider);
$openConnSource = $provider->getOpenConnectorSource();
$configureUrl = ($isExternal === true && $openConnSource !== null)
? $this->buildOpenConnectorConfigureUrl($openConnSource)
: null;

return [
'id' => $provider->getId(),
'label' => $provider->getLabel(),
'group' => $provider->getGroup() ?? '',
'storage' => $provider->getStorageStrategy(),
'requiredApp' => $requiredApp,
'requiredAppOk' => ($requiredApp === null || $this->appManager->isInstalled($requiredApp)),
'enabled' => $provider->isEnabled(),
'isExternal' => $isExternal,
'openConnectorSource' => $openConnSource,
'authStatus' => $health['authStatus'],
'status' => $health['status'],
'message' => $health['message'],
'configureUrl' => $configureUrl,
'testConnectionUrl' => ($isExternal === true)
? $this->urlGenerator->linkToOCSRouteAbsolute(
'openregister.integrations.show',
['id' => $provider->getId()]
)
: null,
];
}//end describe()

/**
* Resolve the provider's health descriptor.
*
* External providers go through the router's `probe()` so failure
* modes match what runtime callers will see. Native providers use
* their own `health()` method (typically a static "ok" shape).
*
* @param IntegrationProvider $provider Provider.
*
* @return array{status: string, authStatus: string, message: ?string}
*/
private function probeHealth(IntegrationProvider $provider): array
{
if ($provider->getStorageStrategy() === 'external') {
return $this->router->probe($provider);
}

try {
return $provider->health();
} catch (\Throwable $e) {
return [
'status' => 'unavailable',
'authStatus' => 'unknown',
'message' => 'Provider health check threw',
];
}
}//end probeHealth()

/**
* Build the deep-link into OpenConnector's source-edit screen.
*
* OpenConnector exposes its sources at
* `/apps/openconnector/sources/{sourceId}`; this helper builds
* the absolute URL. When OpenConnector isn't installed we fall
* back to the OpenConnector landing app entry so admins land on
* an install/enable banner instead of a 404.
*
* @param string $sourceId OpenConnector source id declared by
* the provider.
*
* @return string Absolute URL.
*/
private function buildOpenConnectorConfigureUrl(string $sourceId): string
{
if ($this->appManager->isInstalled('openconnector') === false) {
return $this->urlGenerator->getAbsoluteURL('/index.php/settings/apps/integration/openconnector');
}

try {
return $this->urlGenerator->linkToRouteAbsolute(
'openconnector.sources.show',
['id' => $sourceId]
);
} catch (\Throwable $e) {
// OpenConnector's route names have varied across versions —
// fall back to the source-edit URL by convention.
return $this->urlGenerator->getAbsoluteURL(
sprintf('/index.php/apps/openconnector/sources/%s', rawurlencode($sourceId))
);
}
}//end buildOpenConnectorConfigureUrl()

}//end class
6 changes: 3 additions & 3 deletions openspec/changes/pluggable-integration-registry/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
{"id": "4.3", "section": "Backend — Routes, Controller, Capabilities", "title": "Add /api/integrations + sub-resource routes to appinfo/routes.php", "description": "Mounts both IntegrationsController and ObjectIntegrationsController endpoints.", "status": "done", "files_likely_affected": ["appinfo/routes.php"]},
{"id": "4.4", "section": "Backend — Routes, Controller, Capabilities", "title": "Add lib/Capabilities/IntegrationsCapability.php", "description": "Surfaces the registry through /ocs/v2.php/cloud/capabilities, role-redacted per AD-17. Spec said 'Update lib/Service/CapabilitiesService.php'; OR's capability pattern uses one ICapability class per concern (see UrnCapability) — new file in lib/Capabilities/ + $context->registerCapability(). Same end shape, idiomatic structure.", "status": "done", "files_likely_affected": ["lib/Capabilities/IntegrationsCapability.php"]},
{"id": "4.5", "section": "Backend — Routes, Controller, Capabilities", "title": "Register IntegrationsCapability via $context->registerCapability()", "description": "info.xml doesn't carry capability declarations in OR — registration happens at runtime via IRegistrationContext::registerCapability(), mirroring the existing UrnCapability pattern. No appinfo/info.xml change needed.", "status": "done", "files_likely_affected": ["lib/AppInfo/Application.php"]},
{"id": "5.1", "section": "Backend — Admin UI for auth", "title": "Create lib/Settings/IntegrationsAdminSection.php", "description": "Admin section listing integrations + auth status + Configure buttons.", "status": "pending", "files_likely_affected": ["lib/Settings/IntegrationsAdminSection.php"]},
{"id": "5.2", "section": "Backend — Admin UI for auth", "title": "Wire admin section to OpenConnector credential management for storage:external providers", "description": "Configure buttons deep-link into OpenConnector.", "status": "pending", "files_likely_affected": ["lib/Settings/IntegrationsAdminSection.php"]},
{"id": "5.3", "section": "Backend — Admin UI for auth", "title": "Per-integration Test connection action in admin UI", "description": "Calls provider's auth-test method, displays result.", "status": "pending", "files_likely_affected": ["lib/Settings/IntegrationsAdminSection.php"]},
{"id": "5.1", "section": "Backend — Admin UI for auth", "title": "Create admin settings page + template", "description": "lib/Settings/IntegrationsAdminSettings.php + templates/settings/integrations-admin.php. Lists every IntegrationProvider with id / label / group / storage / requiredApp / status / authStatus / OpenConnectorSource. Spec called for an 'AdminSection'; OR's pattern is one ISettings page per IIconSection, so this lands as a second <admin> entry under the existing Sections\\OpenRegisterAdmin parent section.", "status": "done", "files_likely_affected": ["lib/Settings/IntegrationsAdminSettings.php", "templates/settings/integrations-admin.php"]},
{"id": "5.2", "section": "Backend — Admin UI for auth", "title": "Wire admin section to OpenConnector credential management", "description": "buildOpenConnectorConfigureUrl() produces a deep-link to OpenConnector's source-edit screen (with graceful fallback to the install page when OpenConnector isn't enabled). External-provider rows render a Configure button.", "status": "done", "files_likely_affected": ["lib/Settings/IntegrationsAdminSettings.php"]},
{"id": "5.3", "section": "Backend — Admin UI for auth", "title": "Per-integration Test connection action", "description": "External providers' rows include a Test connection link pointing at the OCS route /ocs/v2.php/apps/openregister/api/integrations/{id} which returns the role-redacted descriptor (including authStatus).", "status": "done", "files_likely_affected": ["lib/Settings/IntegrationsAdminSettings.php", "templates/settings/integrations-admin.php"]},
{"id": "6.1", "section": "Frontend — Registry & Composable", "title": "Create src/integrations/registry.js — window.OCA.OpenRegister.integrations", "description": "register, unregister, list, get, onChange, listByGroup; collision policy per AD-11; queue stub for late-loaded apps.", "status": "pending", "files_likely_affected": ["src/integrations/registry.js"]},
{"id": "6.2", "section": "Frontend — Registry & Composable", "title": "Create src/composables/useIntegrationRegistry.js — reactive registry consumer", "description": "Vue composable returning reactive list + selection helpers.", "status": "pending", "files_likely_affected": ["src/composables/useIntegrationRegistry.js"]},
{"id": "6.3", "section": "Frontend — Registry & Composable", "title": "Add integrations export to src/index.js", "description": "Public barrel export.", "status": "pending", "files_likely_affected": ["src/index.js"]},
Expand Down
6 changes: 3 additions & 3 deletions openspec/changes/pluggable-integration-registry/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@

## Backend — Admin UI for auth

- [ ] Create `lib/Settings/IntegrationsAdminSection.php` — admin section listing integrations + auth status + Configure buttons
- [ ] Wire admin section to OpenConnector credential management for `storage: external` providers
- [ ] Per-integration "Test connection" action in admin UI
- [x] Create admin settings page — `lib/Settings/IntegrationsAdminSettings.php` + server-rendered template at `templates/settings/integrations-admin.php`. Lists every IntegrationProvider with id / label / group / storage / requiredApp / status / authStatus / OpenConnectorSource. _Spec called for an "AdminSection" but OR's pattern is one `ISettings` page per `IIconSection`; the existing `Sections\OpenRegisterAdmin` already provides the parent section, so this lands as a second `<admin>` entry under it. Same end shape, idiomatic structure._
- [x] Wire admin section to OpenConnector credential management — `buildOpenConnectorConfigureUrl()` produces a deep-link to OpenConnector's source-edit screen (with graceful fallback to the install page when OpenConnector isn't enabled). External-provider rows render a "Configure" button pointing there.
- [x] Per-integration "Test connection" action — external providers' rows include a "Test connection" link pointing at the OCS route `/ocs/v2.php/apps/openregister/api/integrations/{id}` which returns the role-redacted descriptor (including `authStatus`).

## Frontend — Registry & Composable (`@conduction/nextcloud-vue`)

Expand Down
Loading
Loading