Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2c362c3
feat: initiate remote share notifications
grnd-alt Oct 6, 2025
9a37762
wip: show federated shares in navigation
grnd-alt Oct 14, 2025
4e485fd
read-only federated boards
grnd-alt Oct 21, 2025
972c797
feat: federated card creation
grnd-alt Oct 30, 2025
1633bd8
feat: map external boardIds back to internal ones for frontend
grnd-alt Oct 30, 2025
e398765
fix: remote card creation
grnd-alt Oct 30, 2025
701ec35
feat: create stacks on remote share
grnd-alt Nov 3, 2025
d70fe58
feat: delete stacks on remote shares
grnd-alt Nov 3, 2025
62eb097
add create to ocs board controller
grnd-alt Nov 12, 2025
51876e9
feat: ocs endpoint for addAcl
grnd-alt Nov 15, 2025
2f7ec3d
feat: register deck resource type
grnd-alt Jan 5, 2026
aa63d05
feat: introduce config value for federation
grnd-alt Feb 2, 2026
7b0f590
feat: feature flag for federation
grnd-alt Feb 9, 2026
4de5898
update acl on federated shares
grnd-alt Feb 10, 2026
fd87c52
chore: apply cs:fix
grnd-alt Feb 10, 2026
a0228f1
feat: resolve federated owners
grnd-alt Feb 10, 2026
6d055fa
fix(federation): check permission on local copy of boards
grnd-alt Feb 10, 2026
e6a7bad
chore: rename new controllers
grnd-alt Feb 16, 2026
bc53969
fix: respect admin config values for federation
grnd-alt Feb 16, 2026
0e15c13
fix: do not add default permissions to federated shares
grnd-alt Feb 16, 2026
f91394b
chore: improve ci
grnd-alt Feb 16, 2026
1923cc8
chore: add reuse compliance headers
grnd-alt Feb 17, 2026
1940a2e
feat: update cards on federated boards
grnd-alt Feb 18, 2026
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
12 changes: 12 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@
['name' => 'board_api#preflighted_cors', 'url' => '/api/v{apiVersion}/{path}','verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
],
'ocs' => [
['name' => 'board_ocs#index', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'GET'],
['name' => 'board_ocs#read', 'url' => '/api/v{apiVersion}/board/{boardId}', 'verb' => 'GET'],
['name' => 'board_ocs#stacks', 'url' => '/api/v{apiVersion}/stacks/{boardId}', 'verb' => 'GET'],
['name' => 'board_ocs#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'],
['name' => 'board_ocs#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],

['name' => 'card_ocs#create', 'url' => '/api/v{apiVersion}/cards', 'verb' => 'POST'],
['name' => 'card_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}', 'verb' => 'PUT'],

['name' => 'stack_ocs#create', 'url' => '/api/v{apiVersion}/stacks', 'verb' => 'POST'],
['name' => 'stack_ocs#delete', 'url' => '/api/v{apiVersion}/stacks/{stackId}/{boardId}', 'verb' => 'DELETE', 'defaults' => ['boardId' => null]],

['name' => 'Config#get', 'url' => '/api/v{apiVersion}/config', 'verb' => 'GET'],
['name' => 'Config#setValue', 'url' => '/api/v{apiVersion}/config/{key}', 'verb' => 'POST'],

Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/boardFeatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('Board', function() {

cy.intercept({
method: 'POST',
url: '/index.php/apps/deck/boards',
url: '/ocs/v2.php/apps/deck/api/v1.0/boards',
}).as('createBoardRequest')

// Click "Add board"
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/cardFeatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('Card', function () {
it('Create card from overview', function () {
cy.visit(`/apps/deck/#/`)
const newCardTitle = 'Test create from overview'
cy.intercept({ method: 'POST', url: '**/apps/deck/cards' }).as('save')
cy.intercept({ method: 'POST', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards' }).as('save')
cy.intercept({ method: 'GET', url: '**/apps/deck/boards/*' }).as('getBoard')

cy.get('.button-vue[aria-label*="Add card"]')
Expand Down Expand Up @@ -194,7 +194,7 @@ describe('Card', function () {

it('Shows the modal with the editor', () => {
cy.get('.card:contains("Hello world")').should('be.visible').click()
cy.intercept({ method: 'PUT', url: '**/apps/deck/cards/*' }).as('save')
cy.intercept({ method: 'PUT', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards/*' }).as('save')
cy.get('.modal__card').should('be.visible')
cy.get('.app-sidebar-header__mainname').contains('Hello world')
cy.get('.modal__card .ProseMirror h1').contains('Hello world').should('be.visible')
Expand All @@ -213,7 +213,7 @@ describe('Card', function () {

it('Smart picker', () => {
const newCardTitle = 'Test smart picker'
cy.intercept({ method: 'POST', url: '**/apps/deck/cards' }).as('save')
cy.intercept({ method: 'POST', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards' }).as('save')
cy.intercept({ method: 'GET', url: '**/apps/deck/boards/*' }).as('getBoard')
cy.get('.card:contains("Hello world")').should('be.visible').click()
cy.get('.modal__card').should('be.visible')
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/deckDashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('Deck dashboard', function() {
}).then((board) => {
cy.visit(`/apps/deck/#/board/${board.id}`)

cy.intercept({ method: 'PUT', url: '**/apps/deck/cards/**' }).as('updateCard')
cy.intercept({ method: 'PUT', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards/**' }).as('updateCard')

const newCardTitle = 'Hello world'
cy.get(`.card:contains("${newCardTitle}")`).should('be.visible').click()
Expand Down
7 changes: 6 additions & 1 deletion lib/Activity/ActivityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject)
];
if ($changes['before'] !== $changes['after']) {
try {
$event = $this->createEvent($objectType, $entity, $subjectComplete, $changes);
$event = $this->createEvent($objectType, $entity, $subjectComplete, $changes, $this->userId);
if ($event !== null) {
$events[] = $event;
}
Expand Down Expand Up @@ -300,6 +300,11 @@ public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject)
* @throws \Exception
*/
private function createEvent($objectType, $entity, $subject, $additionalParams = [], $author = null) {
// @TODO implement actual activities for federated users
// this case only happens for federated activities if the author is not provided
if ($author === null && $this->userId === null) {
return;
}
try {
$object = $this->findObjectForEntity($objectType, $entity);
} catch (DoesNotExistException $e) {
Expand Down
21 changes: 21 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use OCA\Deck\Event\CardUpdatedEvent;
use OCA\Deck\Event\SessionClosedEvent;
use OCA\Deck\Event\SessionCreatedEvent;
use OCA\Deck\Federation\DeckFederationProvider;
use OCA\Deck\Listeners\AclCreatedRemovedListener;
use OCA\Deck\Listeners\BeforeTemplateRenderedListener;
use OCA\Deck\Listeners\CommentEventListener;
Expand All @@ -35,8 +36,10 @@
use OCA\Deck\Listeners\ParticipantCleanupListener;
use OCA\Deck\Listeners\ResourceAdditionalScriptsListener;
use OCA\Deck\Listeners\ResourceListener;
use OCA\Deck\Listeners\ResourceTypeRegisterListener;
use OCA\Deck\Middleware\DefaultBoardMiddleware;
use OCA\Deck\Middleware\ExceptionMiddleware;
use OCA\Deck\Middleware\FederationMiddleware;
use OCA\Deck\Notification\Notifier;
use OCA\Deck\Reference\BoardReferenceProvider;
use OCA\Deck\Reference\CardReferenceProvider;
Expand All @@ -60,9 +63,13 @@
use OCP\Comments\CommentsEntityEvent;
use OCP\Comments\CommentsEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudFederationProvider;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\OCM\Events\ResourceTypeRegisterEvent;
use OCP\Server;
use OCP\Share\IManager;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
Expand Down Expand Up @@ -102,6 +109,7 @@ public function boot(IBootContext $context): void {
$context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) {
$listener->register($eventDispatcher);
});
$context->injectFn([$this, 'registerCloudFederationProviderManager']);
}

public function register(IRegistrationContext $context): void {
Expand All @@ -110,6 +118,7 @@ public function register(IRegistrationContext $context): void {
}

$context->registerCapability(Capabilities::class);
$context->registerMiddleWare(FederationMiddleware::class);
$context->registerMiddleWare(ExceptionMiddleware::class);
$context->registerMiddleWare(DefaultBoardMiddleware::class);

Expand All @@ -134,6 +143,7 @@ public function register(IRegistrationContext $context): void {
$context->registerReferenceProvider(CommentReferenceProvider::class);

$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerEventListener(ResourceTypeRegisterEvent::class, ResourceTypeRegisterListener::class);

// Event listening to emit UserShareAccessUpdatedEvent for files_sharing
$context->registerEventListener(AclCreatedEvent::class, AclCreatedRemovedListener::class);
Expand Down Expand Up @@ -194,4 +204,15 @@ protected function registerCollaborationResources(IProviderManager $resourceMana
$resourceManager->registerResourceProvider(ResourceProvider::class);
$resourceManager->registerResourceProvider(ResourceProviderCard::class);
}

public function registerCloudFederationProviderManager(
IConfig $config,
ICloudFederationProviderManager $manager,
): void {
$manager->addCloudFederationProvider(
DeckFederationProvider::PROVIDER_ID,
'Deck Federation',
static fn (): ICloudFederationProvider => Server::get(DeckFederationProvider::class),
);
}
}
4 changes: 3 additions & 1 deletion lib/Controller/BoardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCA\Deck\Db\Board;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\ExternalBoardService;
use OCA\Deck\Service\Importer\BoardImportService;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\ApiController;
Expand All @@ -25,6 +26,7 @@ public function __construct(
$appName,
IRequest $request,
private BoardService $boardService,
private ExternalBoardService $externalBoardService,
private PermissionService $permissionService,
private BoardImportService $boardImportService,
private IL10N $l10n,
Expand Down Expand Up @@ -83,7 +85,7 @@ public function getUserPermissions(int $boardId): array {
* @param $participant
*/
#[NoAdminRequired]
public function addAcl(int $boardId, int $type, $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage): Acl {
public function addAcl(int $boardId, int $type, $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage, ?string $remote = null): Acl {
return $this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage);
}

Expand Down
86 changes: 86 additions & 0 deletions lib/Controller/BoardOcsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Controller;

use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\ExternalBoardService;
use OCA\Deck\Service\StackService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\RequestHeader;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

class BoardOcsController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private BoardService $boardService,
private ExternalBoardService $externalBoardService,
private LoggerInterface $logger,
private StackService $stackService,
private $userId,
) {
parent::__construct($appName, $request);
}

#[NoAdminRequired]
public function index(): DataResponse {
$internalBoards = $this->boardService->findAll();
return new DataResponse($internalBoards);
}

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function read(int $boardId): DataResponse {
// Board on this instance -> get it from database
$localBoard = $this->boardService->find($boardId, true, true);
if ($localBoard->getExternalId() !== null) {
return $this->externalBoardService->getExternalBoardFromRemote($localBoard);
}
// Board on other instance -> get it from other instance
return new DataResponse($localBoard);
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function create(string $title, string $color): DataResponse {
return new DataResponse($this->boardService->create($title, $this->userId, $color));
}

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function stacks(int $boardId): DataResponse {
$localBoard = $this->boardService->find($boardId, true, true);
// Board on other instance -> get it from other instance
if ($localBoard->getExternalId() !== null) {
return $this->externalBoardService->getExternalStacksFromRemote($localBoard);
} else {
return new DataResponse($this->stackService->findAll($boardId));
}
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function addAcl(int $boardId, int $type, string $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage, ?string $remote = null): DataResponse {
return new DataResponse($this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage));
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function updateAcl(int $id, bool $permissionEdit, bool $permissionShare, bool $permissionManage): DataResponse {
return new DataResponse($this->boardService->updateAcl($id, $permissionEdit, $permissionShare, $permissionManage));
}
}
112 changes: 112 additions & 0 deletions lib/Controller/CardOcsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\Controller;

use OCA\Deck\Model\OptionalNullableValue;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\CardService;
use OCA\Deck\Service\ExternalBoardService;
use OCA\Deck\Service\StackService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\RequestHeader;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;

class CardOcsController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private CardService $cardService,
private StackService $stackService,
private BoardService $boardService,
private ExternalBoardService $externalBoardService,
private ?string $userId,
) {
parent::__construct($appName, $request);
}

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, ?array $labels = [], ?array $users = []) {
if ($boardId) {
$board = $this->boardService->find($boardId, false);
if ($board->getExternalId()) {
$card = $this->externalBoardService->createCardOnRemote($board, $title, $stackId, $type, $order, $description, $duedate, $users);
return new DataResponse($card);
}
}

if (!$owner) {
$owner = $this->userId;
}
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate);

// foreach ($labels as $label) {
// $this->assignLabel($card->getId(), $label);
// }

// foreach ($users as $user) {
// $this->assignmentService->assignUser($card->getId(), $user['id'], $user['type']);
// }

return new DataResponse($card);
}

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, array|string|null $owner = null, $archived = null, int $boardId): DataResponse {
$done = array_key_exists('done', $this->request->getParams())
? new OptionalNullableValue($this->request->getParam('done', null))
: null;
if (!$owner) {
$owner = $this->userId;
} else {
if (!is_string($owner)) {
$owner = $owner['uid'];
}
}

$localBoard = $this->boardService->find($boardId, false);
if ($localBoard->getExternalId()) {
return new DataResponse($this->externalBoardService->updateCardOnRemote(
$localBoard,
$id,
$title,
$stackId,
$type,
$owner,
$description,
$order,
$duedate,
$deletedAt,
$archived,
$done
));
}

return new DataResponse($this->cardService->update($id,
$title,
$stackId,
$type,
$owner,
$description,
$order,
$duedate,
$deletedAt,
$archived,
$done
));
}
}
Loading
Loading