diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index cc12d8b49..b4687bdd0 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -11,6 +11,7 @@ use OCA\Circles\AppInfo\Application; use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\PermissionService; use OCA\Circles\Service\TeamFolderPolicy; use OCA\Text\Event\LoadEditor; use OCP\AppFramework\Controller; @@ -22,6 +23,7 @@ use OCP\AppFramework\Services\IInitialState; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; +use OCP\IUserSession; use OCP\Teams\ITeamManager; use OCP\Util; @@ -36,6 +38,8 @@ public function __construct( private ITeamManager $teamManager, private TeamFolderPolicy $teamFolderPolicy, private IEventDispatcher $eventDispatcher, + private IUserSession $userSession, + private PermissionService $permissionService, ) { parent::__construct(Application::APP_ID, $request); } @@ -56,6 +60,12 @@ public function index(): TemplateResponse|NotFoundResponse { 'teamFolderProvisioningEnabled', $this->teamFolderPolicy->isTeamFolderProvisioningEnabled(), ); + $this->initialState->provideInitialState( + 'canCreateTeam', + $this->permissionService->canUserCreateCircle( + $this->userSession->getUser()?->getUID() ?? '' + ), + ); Util::addScript(Application::APP_ID, 'teams-main'); Util::addStyle(Application::APP_ID, 'teams-main'); diff --git a/lib/Dashboard/TeamDashboardWidget.php b/lib/Dashboard/TeamDashboardWidget.php index e9857fbc2..c1fa16981 100644 --- a/lib/Dashboard/TeamDashboardWidget.php +++ b/lib/Dashboard/TeamDashboardWidget.php @@ -9,12 +9,15 @@ use OCA\Circles\AppInfo\Application; use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\PermissionService; +use OCP\AppFramework\Services\IInitialState; use OCP\Dashboard\IButtonWidget; use OCP\Dashboard\IConditionalWidget; use OCP\Dashboard\IIconWidget; use OCP\Dashboard\Model\WidgetButton; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\IUserSession; use OCP\Util; class TeamDashboardWidget implements IIconWidget, IButtonWidget, IConditionalWidget { @@ -22,6 +25,9 @@ public function __construct( private readonly IURLGenerator $urlGenerator, private readonly IL10N $l10n, private readonly ConfigService $configService, + private readonly IInitialState $initialState, + private readonly IUserSession $userSession, + private readonly PermissionService $permissionService, ) { } @@ -66,21 +72,32 @@ public function getUrl(): ?string { public function load(): void { Util::addScript(Application::APP_ID, 'teams-dashboard'); Util::addStyle(Application::APP_ID, 'teams-dashboard'); + $this->initialState->provideInitialState( + 'canCreateTeam', + $this->permissionService->canUserCreateCircle( + $this->userSession->getUser()?->getUID() ?? '' + ), + ); } public function getWidgetButtons(string $userId): array { - return [ + $buttons = [ new WidgetButton( WidgetButton::TYPE_MORE, $this->getTeamPage(), $this->l10n->t('Show all teams') ), - new WidgetButton( + ]; + + if ($this->permissionService->canUserCreateCircle($userId)) { + $buttons[] = new WidgetButton( WidgetButton::TYPE_SETUP, $this->getTeamPage(), $this->l10n->t('Create a new team') - ), - ]; + ); + } + + return $buttons; } public function getIconUrl(): string { diff --git a/lib/Service/PermissionService.php b/lib/Service/PermissionService.php index c36b06e1e..65af4e995 100644 --- a/lib/Service/PermissionService.php +++ b/lib/Service/PermissionService.php @@ -9,6 +9,7 @@ namespace OCA\Circles\Service; +use Exception; use OCA\Circles\Db\MemberRequest; use OCA\Circles\Db\MembershipRequest; use OCA\Circles\Exceptions\InitiatorNotFoundException; @@ -19,6 +20,7 @@ use OCA\Circles\Exceptions\MembershipNotFoundException; use OCA\Circles\Exceptions\RequestBuilderException; use OCA\Circles\Model\Circle; +use OCA\Circles\Model\FederatedUser; use OCA\Circles\Model\Helpers\MemberHelper; use OCA\Circles\Model\Member; use OCP\IGroupManager; @@ -51,6 +53,27 @@ public function confirmCircleCreation(): void { } } + /** + * Non-throwing variant of confirmCircleCreation() for a given local user, + * used to decide whether team creation is offered in the frontend. + */ + public function canUserCreateCircle(string $userId): bool { + $singleId = $this->configService->getAppValue( + ConfigService::LIMIT_CIRCLE_CREATION + ); + if ($singleId === '') { + return true; + } + + try { + $federatedUser = $this->federatedUserService + ->getLocalFederatedUser($userId); + return $this->isLinkedTo($federatedUser, $singleId); + } catch (Exception) { + return false; + } + } + /** * @param string $config * @@ -66,10 +89,23 @@ private function confirm(string $config): void { $this->federatedUserService->mustHaveCurrentUser(); $federatedUser = $this->federatedUserService->getCurrentUser(); + if (!$this->isLinkedTo($federatedUser, $singleId)) { + throw new InsufficientPermissionException(); + } + } + + /** + * @throws RequestBuilderException + */ + private function isLinkedTo( + FederatedUser $federatedUser, + string $singleId, + ): bool { try { $federatedUser->getLink($singleId); + return true; } catch (MembershipNotFoundException) { - throw new InsufficientPermissionException(); + return false; } } diff --git a/src/teams/App.spec.ts b/src/teams/App.spec.ts new file mode 100644 index 000000000..b878559df --- /dev/null +++ b/src/teams/App.spec.ts @@ -0,0 +1,73 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { shallowMount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import App from './App.vue' +import { useTeamsStore } from './store.ts' + +type LoadState = (app: string, key: string, fallback?: unknown) => unknown + +const loadState = vi.hoisted(() => vi.fn()) + +vi.mock('@nextcloud/initial-state', () => ({ loadState })) +vi.mock('./api.ts', () => ({ fetchTeams: vi.fn(async () => []) })) +vi.mock('./components/TeamNavigation.vue', () => ({ + default: { name: 'TeamNavigation', template: '