Merge pull request #3876 from alangecker/sessions
basic notify_push usage with session handling
This commit is contained in:
@@ -40,6 +40,7 @@
|
|||||||
<job>OCA\Deck\Cron\DeleteCron</job>
|
<job>OCA\Deck\Cron\DeleteCron</job>
|
||||||
<job>OCA\Deck\Cron\ScheduledNotifications</job>
|
<job>OCA\Deck\Cron\ScheduledNotifications</job>
|
||||||
<job>OCA\Deck\Cron\CardDescriptionActivity</job>
|
<job>OCA\Deck\Cron\CardDescriptionActivity</job>
|
||||||
|
<job>OCA\Deck\Cron\SessionsCleanup</job>
|
||||||
</background-jobs>
|
</background-jobs>
|
||||||
<repair-steps>
|
<repair-steps>
|
||||||
<live-migration>
|
<live-migration>
|
||||||
|
|||||||
@@ -150,5 +150,10 @@ return [
|
|||||||
['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],
|
['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],
|
||||||
|
|
||||||
['name' => 'search#search', 'url' => '/api/v{apiVersion}/search', 'verb' => 'GET'],
|
['name' => 'search#search', 'url' => '/api/v{apiVersion}/search', 'verb' => 'GET'],
|
||||||
|
|
||||||
|
// sessions
|
||||||
|
['name' => 'Session#create', 'url' => '/api/v{apiVersion}/session/create', 'verb' => 'PUT'],
|
||||||
|
['name' => 'Session#sync', 'url' => '/api/v{apiVersion}/session/sync', 'verb' => 'POST'],
|
||||||
|
['name' => 'Session#close', 'url' => '/api/v{apiVersion}/session/close', 'verb' => 'POST'],
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|||||||
107
docs/API.md
107
docs/API.md
@@ -1394,3 +1394,110 @@ A bad request response is returned if invalid input values are provided. The res
|
|||||||
A not found response might be returned if:
|
A not found response might be returned if:
|
||||||
- The card for the given cardId could not be found
|
- The card for the given cardId could not be found
|
||||||
- The comment could not be found
|
- The comment could not be found
|
||||||
|
|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
### PUT /session/create - creates a new session
|
||||||
|
|
||||||
|
#### Request parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ------- | ---------------------------------------------------- |
|
||||||
|
| boardId | Integer | The id of the opened board |
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X PUT 'https://admin:admin@nextcloud/ocs/v2.php/apps/deck/api/v1.0/session/create' \
|
||||||
|
-H 'Accept: application/json' -H 'OCS-APIRequest: true' \
|
||||||
|
-H 'Content-Type: application/json;charset=utf-8' \
|
||||||
|
--data '{"boardId":1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
##### 200 Success
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "ok",
|
||||||
|
"statuscode": 200,
|
||||||
|
"message": "OK"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"token": "+zcJHf4rC6dobVSbuNa3delkCSfTW8OvYWTyLFvSpIv80FjtgLIj0ARlxspsazNQ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### POST /session/sync - notifies the server, that the session is still open
|
||||||
|
|
||||||
|
#### Request body
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ------- | ---------------------------------------------------- |
|
||||||
|
| boardId | Integer | The id of the opened board |
|
||||||
|
| token | String | The session token from the /sessions/create response |
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST 'https://admin:admin@nextcloud/ocs/v2.php/apps/deck/api/v1.0/session/create' \
|
||||||
|
-H 'Accept: application/json' -H 'OCS-APIRequest: true' \
|
||||||
|
-H 'Content-Type: application/json;charset=utf-8' \
|
||||||
|
--data '{"boardId":1, "token":"X3DyyoFslArF0t0NBZXzZXzcy8feoX/OEytSNXZtPg9TpUgO5wrkJ38IW3T/FfpV"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
##### 200 Success
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "ok",
|
||||||
|
"statuscode": 200,
|
||||||
|
"message": "OK"
|
||||||
|
},
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 404 Not Found
|
||||||
|
the provided token is invalid or expired
|
||||||
|
|
||||||
|
|
||||||
|
### POST /session/close - closes the session
|
||||||
|
|
||||||
|
#### Request body
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ------- | ---------------------------------------------------- |
|
||||||
|
| boardId | Integer | The id of the opened board |
|
||||||
|
| token | String | The session token from the /sessions/create response |
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST 'https://admin:admin@nextcloud/ocs/v2.php/apps/deck/api/v1.0/session/close' \
|
||||||
|
-H 'Accept: application/json' -H 'OCS-APIRequest: true' \
|
||||||
|
-H 'Content-Type: application/json;charset=utf-8' \
|
||||||
|
--data '{"boardId":1, "token":"X3DyyoFslArF0t0NBZXzZXzcy8feoX/OEytSNXZtPg9TpUgO5wrkJ38IW3T/FfpV"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
##### 200 Success
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "ok",
|
||||||
|
"statuscode": 200,
|
||||||
|
"message": "OK"
|
||||||
|
},
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -40,10 +40,13 @@ use OCA\Deck\Event\AclUpdatedEvent;
|
|||||||
use OCA\Deck\Event\CardCreatedEvent;
|
use OCA\Deck\Event\CardCreatedEvent;
|
||||||
use OCA\Deck\Event\CardDeletedEvent;
|
use OCA\Deck\Event\CardDeletedEvent;
|
||||||
use OCA\Deck\Event\CardUpdatedEvent;
|
use OCA\Deck\Event\CardUpdatedEvent;
|
||||||
|
use OCA\Deck\Event\SessionClosedEvent;
|
||||||
|
use OCA\Deck\Event\SessionCreatedEvent;
|
||||||
use OCA\Deck\Listeners\BeforeTemplateRenderedListener;
|
use OCA\Deck\Listeners\BeforeTemplateRenderedListener;
|
||||||
use OCA\Deck\Listeners\ParticipantCleanupListener;
|
use OCA\Deck\Listeners\ParticipantCleanupListener;
|
||||||
use OCA\Deck\Listeners\FullTextSearchEventListener;
|
use OCA\Deck\Listeners\FullTextSearchEventListener;
|
||||||
use OCA\Deck\Listeners\ResourceListener;
|
use OCA\Deck\Listeners\ResourceListener;
|
||||||
|
use OCA\Deck\Listeners\LiveUpdateListener;
|
||||||
use OCA\Deck\Middleware\DefaultBoardMiddleware;
|
use OCA\Deck\Middleware\DefaultBoardMiddleware;
|
||||||
use OCA\Deck\Middleware\ExceptionMiddleware;
|
use OCA\Deck\Middleware\ExceptionMiddleware;
|
||||||
use OCA\Deck\Notification\Notifier;
|
use OCA\Deck\Notification\Notifier;
|
||||||
@@ -147,6 +150,10 @@ class Application extends App implements IBootstrap {
|
|||||||
$context->registerEventListener(UserDeletedEvent::class, ParticipantCleanupListener::class);
|
$context->registerEventListener(UserDeletedEvent::class, ParticipantCleanupListener::class);
|
||||||
$context->registerEventListener(GroupDeletedEvent::class, ParticipantCleanupListener::class);
|
$context->registerEventListener(GroupDeletedEvent::class, ParticipantCleanupListener::class);
|
||||||
$context->registerEventListener(CircleDestroyedEvent::class, ParticipantCleanupListener::class);
|
$context->registerEventListener(CircleDestroyedEvent::class, ParticipantCleanupListener::class);
|
||||||
|
|
||||||
|
// Event listening for realtime updates via notify_push
|
||||||
|
$context->registerEventListener(SessionCreatedEvent::class, LiveUpdateListener::class);
|
||||||
|
$context->registerEventListener(SessionClosedEvent::class, LiveUpdateListener::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerNotifications(NotificationManager $notificationManager): void {
|
public function registerNotifications(NotificationManager $notificationManager): void {
|
||||||
|
|||||||
91
lib/Controller/SessionController.php
Normal file
91
lib/Controller/SessionController.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Deck\Controller;
|
||||||
|
|
||||||
|
use OCA\Deck\Service\SessionService;
|
||||||
|
use OCA\Deck\Service\PermissionService;
|
||||||
|
use OCA\Deck\Db\BoardMapper;
|
||||||
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
|
use OCP\AppFramework\OCSController;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCA\Deck\Db\Acl;
|
||||||
|
|
||||||
|
class SessionController extends OCSController {
|
||||||
|
private SessionService $sessionService;
|
||||||
|
private PermissionService $permissionService;
|
||||||
|
private BoardMapper $boardMapper;
|
||||||
|
|
||||||
|
public function __construct($appName,
|
||||||
|
IRequest $request,
|
||||||
|
SessionService $sessionService,
|
||||||
|
PermissionService $permissionService,
|
||||||
|
BoardMapper $boardMapper
|
||||||
|
) {
|
||||||
|
parent::__construct($appName, $request);
|
||||||
|
$this->sessionService = $sessionService;
|
||||||
|
$this->permissionService = $permissionService;
|
||||||
|
$this->boardMapper = $boardMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
public function create(int $boardId): DataResponse {
|
||||||
|
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
|
||||||
|
|
||||||
|
$session = $this->sessionService->initSession($boardId);
|
||||||
|
return new DataResponse([
|
||||||
|
'token' => $session->getToken(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* notifies the server that the session is still active
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @param $boardId
|
||||||
|
*/
|
||||||
|
public function sync(int $boardId, string $token): DataResponse {
|
||||||
|
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
|
||||||
|
try {
|
||||||
|
$this->sessionService->syncSession($boardId, $token);
|
||||||
|
return new DataResponse([]);
|
||||||
|
} catch (DoesNotExistException $e) {
|
||||||
|
return new DataResponse([], 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete a session if existing
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @param $boardId
|
||||||
|
*/
|
||||||
|
public function close(int $boardId, string $token) {
|
||||||
|
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
|
||||||
|
$this->sessionService->closeSession($boardId, $token);
|
||||||
|
return new DataResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/Cron/SessionsCleanup.php
Normal file
56
lib/Cron/SessionsCleanup.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace OCA\Deck\Cron;
|
||||||
|
|
||||||
|
use OCA\Deck\Service\SessionService;
|
||||||
|
use OCP\AppFramework\Utility\ITimeFactory;
|
||||||
|
use OCP\BackgroundJob\TimedJob;
|
||||||
|
use OCP\ILogger;
|
||||||
|
|
||||||
|
class SessionsCleanup extends TimedJob {
|
||||||
|
private $sessionService;
|
||||||
|
private $documentService;
|
||||||
|
private $logger;
|
||||||
|
private $imageService;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(ITimeFactory $time,
|
||||||
|
SessionService $sessionService,
|
||||||
|
ILogger $logger) {
|
||||||
|
parent::__construct($time);
|
||||||
|
$this->sessionService = $sessionService;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->setInterval(SessionService::SESSION_VALID_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run($argument) {
|
||||||
|
$this->logger->debug('Run cleanup job for deck sessions');
|
||||||
|
|
||||||
|
$removedSessions = $this->sessionService->removeInactiveSessions();
|
||||||
|
$this->logger->debug('Removed ' . $removedSessions . ' inactive sessions');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ class Board extends RelationalEntity {
|
|||||||
protected $users = [];
|
protected $users = [];
|
||||||
protected $shared;
|
protected $shared;
|
||||||
protected $stacks = [];
|
protected $stacks = [];
|
||||||
|
protected $activeSessions = [];
|
||||||
protected $deletedAt = 0;
|
protected $deletedAt = 0;
|
||||||
protected $lastModified = 0;
|
protected $lastModified = 0;
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ class Board extends RelationalEntity {
|
|||||||
$this->addRelation('acl');
|
$this->addRelation('acl');
|
||||||
$this->addRelation('shared');
|
$this->addRelation('shared');
|
||||||
$this->addRelation('users');
|
$this->addRelation('users');
|
||||||
|
$this->addRelation('activeSessions');
|
||||||
$this->addRelation('permissions');
|
$this->addRelation('permissions');
|
||||||
$this->addRelation('stacks');
|
$this->addRelation('stacks');
|
||||||
$this->addRelation('settings');
|
$this->addRelation('settings');
|
||||||
|
|||||||
51
lib/Db/Session.php
Normal file
51
lib/Db/Session.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Deck\Db;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Db\Entity;
|
||||||
|
|
||||||
|
class Session extends Entity implements \JsonSerializable {
|
||||||
|
public $id;
|
||||||
|
protected $userId;
|
||||||
|
protected $token;
|
||||||
|
protected $lastContact;
|
||||||
|
protected $boardId;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->addType('id', 'integer');
|
||||||
|
$this->addType('boardId', 'integer');
|
||||||
|
$this->addType('lastContact', 'integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jsonSerialize(): array {
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'userId' => $this->userId,
|
||||||
|
'token' => $this->token,
|
||||||
|
'lastContact' => $this->lastContact,
|
||||||
|
'boardId' => $this->boardId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
74
lib/Db/SessionMapper.php
Normal file
74
lib/Db/SessionMapper.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Deck\Db;
|
||||||
|
|
||||||
|
use OCA\Deck\Service\SessionService;
|
||||||
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
|
use OCP\AppFramework\Db\QBMapper;
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
|
/** @template-extends QBMapper<Session> */
|
||||||
|
class SessionMapper extends QBMapper {
|
||||||
|
public function __construct(IDBConnection $db) {
|
||||||
|
parent::__construct($db, 'deck_sessions', Session::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int $boardId, string $userId, string $token): Session {
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$result = $qb->select('*')
|
||||||
|
->from($this->getTableName())
|
||||||
|
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)))
|
||||||
|
->andWhere($qb->expr()->gt('last_contact', $qb->createNamedParameter(time() - SessionService::SESSION_VALID_TIME)))
|
||||||
|
->executeQuery();
|
||||||
|
|
||||||
|
$data = $result->fetch();
|
||||||
|
$result->closeCursor();
|
||||||
|
if ($data === false) {
|
||||||
|
throw new DoesNotExistException('Session is invalid');
|
||||||
|
}
|
||||||
|
$session = Session::fromRow($data);
|
||||||
|
if ($session->getUserId() != $userId || $session->getBoardId() != $boardId) {
|
||||||
|
throw new DoesNotExistException('Session is invalid');
|
||||||
|
}
|
||||||
|
return $session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAllActive($boardId) {
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('id', 'board_id', 'last_contact', 'user_id', 'token')
|
||||||
|
->from($this->getTableName())
|
||||||
|
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId)))
|
||||||
|
->andWhere($qb->expr()->gt('last_contact', $qb->createNamedParameter(time() - SessionService::SESSION_VALID_TIME)))
|
||||||
|
->executeQuery();
|
||||||
|
|
||||||
|
return $this->findEntities($qb);
|
||||||
|
}
|
||||||
|
public function deleteInactive(): int {
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->delete($this->getTableName())
|
||||||
|
->where($qb->expr()->lt('last_contact', $qb->createNamedParameter(time() - SessionService::SESSION_VALID_TIME)));
|
||||||
|
return $qb->executeStatement();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/Event/SessionClosedEvent.php
Normal file
48
lib/Event/SessionClosedEvent.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace OCA\Deck\Event;
|
||||||
|
|
||||||
|
use OCP\EventDispatcher\Event;
|
||||||
|
|
||||||
|
class SessionClosedEvent extends Event {
|
||||||
|
private $boardId;
|
||||||
|
private $userId;
|
||||||
|
|
||||||
|
public function __construct(int $boardId, string $userId) {
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->boardId = $boardId;
|
||||||
|
$this->userId = $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBoardId(): int {
|
||||||
|
return $this->boardId;
|
||||||
|
}
|
||||||
|
public function getUserId(): string {
|
||||||
|
return $this->userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/Event/SessionCreatedEvent.php
Normal file
49
lib/Event/SessionCreatedEvent.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace OCA\Deck\Event;
|
||||||
|
|
||||||
|
use OCP\EventDispatcher\Event;
|
||||||
|
|
||||||
|
class SessionCreatedEvent extends Event {
|
||||||
|
private $boardId;
|
||||||
|
private $userId;
|
||||||
|
|
||||||
|
public function __construct(int $boardId, string $userId) {
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->boardId = $boardId;
|
||||||
|
$this->userId = $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBoardId(): int {
|
||||||
|
return $this->boardId;
|
||||||
|
}
|
||||||
|
public function getUserId(): string {
|
||||||
|
return $this->userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/Listeners/LiveUpdateListener.php
Normal file
87
lib/Listeners/LiveUpdateListener.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace OCA\Deck\Listeners;
|
||||||
|
|
||||||
|
use OCA\Deck\NotifyPushEvents;
|
||||||
|
use OCA\Deck\Event\SessionClosedEvent;
|
||||||
|
use OCA\Deck\Event\SessionCreatedEvent;
|
||||||
|
use OCA\Deck\Service\SessionService;
|
||||||
|
use OCA\NotifyPush\Queue\IQueue;
|
||||||
|
use OCP\EventDispatcher\Event;
|
||||||
|
use OCP\EventDispatcher\IEventListener;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/** @template-implements IEventListener<Event|SessionCreatedEvent|SessionClosedEvent> */
|
||||||
|
class LiveUpdateListener implements IEventListener {
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
private SessionService $sessionService;
|
||||||
|
private IRequest $request;
|
||||||
|
private $queue;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
ContainerInterface $container,
|
||||||
|
IRequest $request,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
SessionService $sessionService
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
$this->queue = $container->get(IQueue::class);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// most likely notify_push is not installed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->sessionService = $sessionService;
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(Event $event): void {
|
||||||
|
if (!$this->queue) {
|
||||||
|
// notify_push is not active
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// the web frontend is adding the Session-ID as a header on every request
|
||||||
|
// TODO: verify the token! this currently allows to spoof a token from someone
|
||||||
|
// else, preventing this person from getting any live updates
|
||||||
|
$causingSessionToken = $this->request->getHeader('x-nc-deck-session');
|
||||||
|
if (
|
||||||
|
$event instanceof SessionCreatedEvent ||
|
||||||
|
$event instanceof SessionClosedEvent
|
||||||
|
) {
|
||||||
|
$this->sessionService->notifyAllSessions($this->queue, $event->getBoardId(), NotifyPushEvents::DeckBoardUpdate, [
|
||||||
|
'id' => $event->getBoardId()
|
||||||
|
], $causingSessionToken);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Error when handling live update event', ['exception' => $e]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
lib/Migration/Version10900Date202206151724222.php
Normal file
67
lib/Migration/Version10900Date202206151724222.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace OCA\Deck\Migration;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use OCP\DB\ISchemaWrapper;
|
||||||
|
use OCP\DB\Types;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
|
use OCP\Migration\SimpleMigrationStep;
|
||||||
|
|
||||||
|
class Version10900Date202206151724222 extends SimpleMigrationStep {
|
||||||
|
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||||
|
$schema = $schemaClosure();
|
||||||
|
|
||||||
|
if (!$schema->hasTable('deck_sessions')) {
|
||||||
|
$table = $schema->createTable('deck_sessions');
|
||||||
|
$table->addColumn('id', Types::INTEGER, [
|
||||||
|
'autoincrement' => true,
|
||||||
|
'notnull' => true,
|
||||||
|
'unsigned' => true,
|
||||||
|
]);
|
||||||
|
$table->addColumn('user_id', Types::STRING, [
|
||||||
|
'notnull' => false,
|
||||||
|
'length' => 64,
|
||||||
|
]);
|
||||||
|
$table->addColumn('board_id', Types::INTEGER, [
|
||||||
|
'notnull' => false,
|
||||||
|
]);
|
||||||
|
$table->addColumn('token', Types::STRING, [
|
||||||
|
'notnull' => true,
|
||||||
|
'length' => 64,
|
||||||
|
]);
|
||||||
|
$table->addColumn('last_contact', Types::INTEGER, [
|
||||||
|
'notnull' => true,
|
||||||
|
'length' => 20,
|
||||||
|
'unsigned' => true,
|
||||||
|
]);
|
||||||
|
$table->setPrimaryKey(['id']);
|
||||||
|
$table->addIndex(['board_id'], 'deck_session_board_id_idx');
|
||||||
|
$table->addIndex(['token'], 'deck_session_token_idx');
|
||||||
|
$table->addIndex(['last_contact'], 'deck_session_last_contact_idx');
|
||||||
|
}
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/NotifyPushEvents.php
Normal file
29
lib/NotifyPushEvents.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Deck;
|
||||||
|
|
||||||
|
class NotifyPushEvents {
|
||||||
|
public const DeckBoardUpdate = 'deck_board_update';
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ use OCA\Deck\Db\CardMapper;
|
|||||||
use OCA\Deck\Db\ChangeHelper;
|
use OCA\Deck\Db\ChangeHelper;
|
||||||
use OCA\Deck\Db\IPermissionMapper;
|
use OCA\Deck\Db\IPermissionMapper;
|
||||||
use OCA\Deck\Db\Label;
|
use OCA\Deck\Db\Label;
|
||||||
|
use OCA\Deck\Db\Session;
|
||||||
|
use OCA\Deck\Db\SessionMapper;
|
||||||
use OCA\Deck\Db\Stack;
|
use OCA\Deck\Db\Stack;
|
||||||
use OCA\Deck\Db\StackMapper;
|
use OCA\Deck\Db\StackMapper;
|
||||||
use OCA\Deck\Event\AclCreatedEvent;
|
use OCA\Deck\Event\AclCreatedEvent;
|
||||||
@@ -81,6 +83,7 @@ class BoardService {
|
|||||||
private IURLGenerator $urlGenerator;
|
private IURLGenerator $urlGenerator;
|
||||||
private IDBConnection $connection;
|
private IDBConnection $connection;
|
||||||
private BoardServiceValidator $boardServiceValidator;
|
private BoardServiceValidator $boardServiceValidator;
|
||||||
|
private SessionMapper $sessionMapper;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
BoardMapper $boardMapper,
|
BoardMapper $boardMapper,
|
||||||
@@ -101,6 +104,7 @@ class BoardService {
|
|||||||
IURLGenerator $urlGenerator,
|
IURLGenerator $urlGenerator,
|
||||||
IDBConnection $connection,
|
IDBConnection $connection,
|
||||||
BoardServiceValidator $boardServiceValidator,
|
BoardServiceValidator $boardServiceValidator,
|
||||||
|
SessionMapper $sessionMapper,
|
||||||
?string $userId
|
?string $userId
|
||||||
) {
|
) {
|
||||||
$this->boardMapper = $boardMapper;
|
$this->boardMapper = $boardMapper;
|
||||||
@@ -122,6 +126,7 @@ class BoardService {
|
|||||||
$this->urlGenerator = $urlGenerator;
|
$this->urlGenerator = $urlGenerator;
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
$this->boardServiceValidator = $boardServiceValidator;
|
$this->boardServiceValidator = $boardServiceValidator;
|
||||||
|
$this->sessionMapper = $sessionMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,6 +219,7 @@ class BoardService {
|
|||||||
]);
|
]);
|
||||||
$this->enrichWithUsers($board);
|
$this->enrichWithUsers($board);
|
||||||
$this->enrichWithBoardSettings($board);
|
$this->enrichWithBoardSettings($board);
|
||||||
|
$this->enrichWithActiveSessions($board);
|
||||||
$this->boardsCache[$board->getId()] = $board;
|
$this->boardsCache[$board->getId()] = $board;
|
||||||
return $board;
|
return $board;
|
||||||
}
|
}
|
||||||
@@ -448,6 +454,18 @@ class BoardService {
|
|||||||
$board->setSettings($settings);
|
$board->setSettings($settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function enrichWithActiveSessions(Board $board) {
|
||||||
|
$sessions = $this->sessionMapper->findAllActive($board->getId());
|
||||||
|
|
||||||
|
$board->setActiveSessions(array_values(
|
||||||
|
array_unique(
|
||||||
|
array_map(function (Session $session) {
|
||||||
|
return $session->getUserId();
|
||||||
|
}, $sessions)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $boardId
|
* @param $boardId
|
||||||
* @param $type
|
* @param $type
|
||||||
|
|||||||
127
lib/Service/SessionService.php
Normal file
127
lib/Service/SessionService.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Deck\Service;
|
||||||
|
|
||||||
|
use OCA\Deck\Db\Session;
|
||||||
|
use OCA\Deck\Db\SessionMapper;
|
||||||
|
use OCA\Deck\Event\SessionCreatedEvent;
|
||||||
|
use OCA\Deck\Event\SessionClosedEvent;
|
||||||
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
|
use OCP\AppFramework\Utility\ITimeFactory;
|
||||||
|
use OCP\EventDispatcher\IEventDispatcher;
|
||||||
|
use OCA\NotifyPush\Queue\IQueue;
|
||||||
|
use OCP\Security\ISecureRandom;
|
||||||
|
|
||||||
|
class SessionService {
|
||||||
|
public const SESSION_VALID_TIME = 92;
|
||||||
|
|
||||||
|
private SessionMapper $sessionMapper;
|
||||||
|
private ITimeFactory $timeFactory;
|
||||||
|
private $userId;
|
||||||
|
private IEventDispatcher $eventDispatcher;
|
||||||
|
private ISecureRandom $secureRandom;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
SessionMapper $sessionMapper,
|
||||||
|
ISecureRandom $secureRandom,
|
||||||
|
ITimeFactory $timeFactory,
|
||||||
|
$userId,
|
||||||
|
IEventDispatcher $eventDispatcher
|
||||||
|
) {
|
||||||
|
$this->sessionMapper = $sessionMapper;
|
||||||
|
$this->secureRandom = $secureRandom;
|
||||||
|
$this->timeFactory = $timeFactory;
|
||||||
|
$this->userId = $userId;
|
||||||
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initSession(int $boardId): Session {
|
||||||
|
$session = new Session();
|
||||||
|
$session->setBoardId($boardId);
|
||||||
|
$session->setUserId($this->userId);
|
||||||
|
$session->setToken($this->secureRandom->generate(32));
|
||||||
|
$session->setLastContact($this->timeFactory->getTime());
|
||||||
|
|
||||||
|
$session = $this->sessionMapper->insert($session);
|
||||||
|
$this->eventDispatcher->dispatchTyped(new SessionCreatedEvent($boardId, $this->userId));
|
||||||
|
return $session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws DoesNotExistException
|
||||||
|
*/
|
||||||
|
public function syncSession(int $boardId, string $token) {
|
||||||
|
$session = $this->sessionMapper->find($boardId, $this->userId, $token);
|
||||||
|
$session->setLastContact($this->timeFactory->getTime());
|
||||||
|
$this->sessionMapper->update($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeSession(int $boardId, string $token): void {
|
||||||
|
try {
|
||||||
|
$session = $this->sessionMapper->find($boardId, $this->userId, $token);
|
||||||
|
$this->sessionMapper->delete($session);
|
||||||
|
} catch (DoesNotExistException $e) {
|
||||||
|
}
|
||||||
|
$this->eventDispatcher->dispatchTyped(new SessionClosedEvent($boardId, $this->userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeInactiveSessions(): int {
|
||||||
|
return $this->sessionMapper->deleteInactive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notifyAllSessions(IQueue $queue, int $boardId, $event, $body, $causingSessionToken = null) {
|
||||||
|
$activeSessions = $this->sessionMapper->findAllActive($boardId);
|
||||||
|
$userIds = [];
|
||||||
|
foreach ($activeSessions as $session) {
|
||||||
|
if ($causingSessionToken && $session->getToken() === $causingSessionToken) {
|
||||||
|
// Limitation:
|
||||||
|
// If the same user has a second session active, the session $causingSessionToken
|
||||||
|
// still gets notified due to the current fact, that notify_push only supports
|
||||||
|
// to specify users, not individual sessions
|
||||||
|
// https://github.com/nextcloud/notify_push/issues/195
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't notify the same user multiple times
|
||||||
|
if (!in_array($session->getUserId(), $userIds)) {
|
||||||
|
$userIds[] = $session->getUserId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($causingSessionToken) {
|
||||||
|
// we only send a slice of the session token to everyone
|
||||||
|
// since knowledge of the full token allows everyone
|
||||||
|
// to close the session maliciously
|
||||||
|
$body['_causingSessionToken'] = substr($causingSessionToken, 0, 12);
|
||||||
|
}
|
||||||
|
foreach ($userIds as $userId) {
|
||||||
|
$queue->push('notify_custom', [
|
||||||
|
'user' => $userId,
|
||||||
|
'message' => $event,
|
||||||
|
'body' => $body
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
package-lock.json
generated
155
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"@nextcloud/initial-state": "^2.0.0",
|
"@nextcloud/initial-state": "^2.0.0",
|
||||||
"@nextcloud/l10n": "^1.6.0",
|
"@nextcloud/l10n": "^1.6.0",
|
||||||
"@nextcloud/moment": "^1.2.1",
|
"@nextcloud/moment": "^1.2.1",
|
||||||
|
"@nextcloud/notify_push": "^1.1.3",
|
||||||
"@nextcloud/router": "^2.0.1",
|
"@nextcloud/router": "^2.0.1",
|
||||||
"@nextcloud/vue": "^7.3.0",
|
"@nextcloud/vue": "^7.3.0",
|
||||||
"@nextcloud/vue-dashboard": "^2.0.1",
|
"@nextcloud/vue-dashboard": "^2.0.1",
|
||||||
@@ -3279,6 +3280,90 @@
|
|||||||
"url": "https://opencollective.com/core-js"
|
"url": "https://opencollective.com/core-js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-dHu3mz2tcqFl43DBxRhbLRY5J8gGi/mwg9PgHbEtK9qDOZ4EFUUXDteWe+B4TCghDGh+xKS6U7oDC5txtF+JaQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@nextcloud/axios": "^1.11.0",
|
||||||
|
"@nextcloud/capabilities": "^1.0.4",
|
||||||
|
"@nextcloud/event-bus": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push/node_modules/@nextcloud/auth": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-GfwRM9W7hat4psNdAt74UHEV+drEXQ53klCVp6JpON66ZLPeK5eJ1LQuiQDkpUxZpqNeaumXjiB98h5cug/uQw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@nextcloud/event-bus": "^1.1.3",
|
||||||
|
"@nextcloud/typings": "^0.2.2",
|
||||||
|
"core-js": "^3.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push/node_modules/@nextcloud/auth/node_modules/@nextcloud/event-bus": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-+U5MnCvfnNWvf0lvdqJg8F+Nm8wN+s9ayuBjtiEQxTAcootv7lOnlMgfreqF3l2T0Wet2uZh4JbFVUWf8l3w7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/semver": "^7.3.5",
|
||||||
|
"core-js": "^3.11.2",
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push/node_modules/@nextcloud/axios": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-NyaiSC2GX2CPaH/MUGGMTTTza/TW9ZqWNGWq6LJ+pLER8nqZ9BQkwJ5kXUYGo+i3cka68PO+9WhcDv4fSABpuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@nextcloud/auth": "^1.3.0",
|
||||||
|
"axios": "^0.27.1",
|
||||||
|
"core-js": "^3.6.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0",
|
||||||
|
"npm": "^7.0.0 || ^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push/node_modules/core-js": {
|
||||||
|
"version": "3.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
|
||||||
|
"integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push/node_modules/lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push/node_modules/semver": {
|
||||||
|
"version": "7.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
|
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^6.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nextcloud/notify_push/node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
},
|
||||||
"node_modules/@nextcloud/router": {
|
"node_modules/@nextcloud/router": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.0.1.tgz",
|
||||||
@@ -20968,6 +21053,76 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@nextcloud/notify_push": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-dHu3mz2tcqFl43DBxRhbLRY5J8gGi/mwg9PgHbEtK9qDOZ4EFUUXDteWe+B4TCghDGh+xKS6U7oDC5txtF+JaQ==",
|
||||||
|
"requires": {
|
||||||
|
"@nextcloud/axios": "^1.11.0",
|
||||||
|
"@nextcloud/capabilities": "^1.0.4",
|
||||||
|
"@nextcloud/event-bus": "^3.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nextcloud/auth": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-GfwRM9W7hat4psNdAt74UHEV+drEXQ53klCVp6JpON66ZLPeK5eJ1LQuiQDkpUxZpqNeaumXjiB98h5cug/uQw==",
|
||||||
|
"requires": {
|
||||||
|
"@nextcloud/event-bus": "^1.1.3",
|
||||||
|
"@nextcloud/typings": "^0.2.2",
|
||||||
|
"core-js": "^3.6.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nextcloud/event-bus": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-+U5MnCvfnNWvf0lvdqJg8F+Nm8wN+s9ayuBjtiEQxTAcootv7lOnlMgfreqF3l2T0Wet2uZh4JbFVUWf8l3w7g==",
|
||||||
|
"requires": {
|
||||||
|
"@types/semver": "^7.3.5",
|
||||||
|
"core-js": "^3.11.2",
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@nextcloud/axios": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-NyaiSC2GX2CPaH/MUGGMTTTza/TW9ZqWNGWq6LJ+pLER8nqZ9BQkwJ5kXUYGo+i3cka68PO+9WhcDv4fSABpuQ==",
|
||||||
|
"requires": {
|
||||||
|
"@nextcloud/auth": "^1.3.0",
|
||||||
|
"axios": "^0.27.1",
|
||||||
|
"core-js": "^3.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"core-js": {
|
||||||
|
"version": "3.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
|
||||||
|
"integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw=="
|
||||||
|
},
|
||||||
|
"lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"requires": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "7.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
|
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||||
|
"requires": {
|
||||||
|
"lru-cache": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nextcloud/router": {
|
"@nextcloud/router": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.0.1.tgz",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"@nextcloud/initial-state": "^2.0.0",
|
"@nextcloud/initial-state": "^2.0.0",
|
||||||
"@nextcloud/l10n": "^1.6.0",
|
"@nextcloud/l10n": "^1.6.0",
|
||||||
"@nextcloud/moment": "^1.2.1",
|
"@nextcloud/moment": "^1.2.1",
|
||||||
|
"@nextcloud/notify_push": "^1.1.3",
|
||||||
"@nextcloud/router": "^2.0.1",
|
"@nextcloud/router": "^2.0.1",
|
||||||
"@nextcloud/vue": "^7.3.0",
|
"@nextcloud/vue": "^7.3.0",
|
||||||
"@nextcloud/vue-dashboard": "^2.0.1",
|
"@nextcloud/vue-dashboard": "^2.0.1",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="board-actions">
|
<div class="board-actions">
|
||||||
|
<SessionList v-if="isNotifyPushEnabled && presentUsers.length"
|
||||||
|
:sessions="presentUsers" />
|
||||||
<div v-if="searchQuery || true" class="deck-search">
|
<div v-if="searchQuery || true" class="deck-search">
|
||||||
<input type="search"
|
<input type="search"
|
||||||
class="icon-search"
|
class="icon-search"
|
||||||
@@ -224,6 +226,8 @@ import FilterIcon from 'vue-material-design-icons/Filter.vue'
|
|||||||
import FilterOffIcon from 'vue-material-design-icons/FilterOff.vue'
|
import FilterOffIcon from 'vue-material-design-icons/FilterOff.vue'
|
||||||
import ArrowCollapseVerticalIcon from 'vue-material-design-icons/ArrowCollapseVertical.vue'
|
import ArrowCollapseVerticalIcon from 'vue-material-design-icons/ArrowCollapseVertical.vue'
|
||||||
import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertical.vue'
|
import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertical.vue'
|
||||||
|
import SessionList from './SessionList.vue'
|
||||||
|
import { isNotifyPushEnabled } from '../sessions.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Controls',
|
name: 'Controls',
|
||||||
@@ -239,6 +243,7 @@ export default {
|
|||||||
FilterOffIcon,
|
FilterOffIcon,
|
||||||
ArrowCollapseVerticalIcon,
|
ArrowCollapseVerticalIcon,
|
||||||
ArrowExpandVerticalIcon,
|
ArrowExpandVerticalIcon,
|
||||||
|
SessionList,
|
||||||
},
|
},
|
||||||
mixins: [labelStyle],
|
mixins: [labelStyle],
|
||||||
props: {
|
props: {
|
||||||
@@ -263,6 +268,7 @@ export default {
|
|||||||
filter: { tags: [], users: [], due: '', unassigned: false },
|
filter: { tags: [], users: [], due: '', unassigned: false },
|
||||||
showAddCardModal: false,
|
showAddCardModal: false,
|
||||||
defaultPageTitle: false,
|
defaultPageTitle: false,
|
||||||
|
isNotifyPushEnabled: isNotifyPushEnabled(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -286,6 +292,11 @@ export default {
|
|||||||
labelsSorted() {
|
labelsSorted() {
|
||||||
return [...this.board.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
return [...this.board.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||||
},
|
},
|
||||||
|
presentUsers() {
|
||||||
|
if (!this.board) return []
|
||||||
|
// get user object including displayname from the list of all users with acces
|
||||||
|
return this.board.users.filter((user) => this.board.activeSessions.includes(user.uid))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
board(current, previous) {
|
board(current, previous) {
|
||||||
|
|||||||
112
src/components/SessionList.vue
Normal file
112
src/components/SessionList.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<!--
|
||||||
|
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-tooltip.bottom="t('text', 'Currently present people')"
|
||||||
|
class="avatar-list">
|
||||||
|
<div v-for="session in sessionsVisible"
|
||||||
|
:key="session.uid"
|
||||||
|
class="avatar-wrapper"
|
||||||
|
:style="sessionAvatarStyle">
|
||||||
|
<NcAvatar :user="session.uid"
|
||||||
|
:display-name="session.displayname"
|
||||||
|
:disable-menu="true"
|
||||||
|
:show-user-status="false"
|
||||||
|
:disable-tooltip="true"
|
||||||
|
:size="size" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { NcAvatar, Tooltip } from '@nextcloud/vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SessionList',
|
||||||
|
components: {
|
||||||
|
NcAvatar,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip: Tooltip,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
sessions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => { return [] },
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: () => 32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sessionsVisible() {
|
||||||
|
if (!this.sessions) return []
|
||||||
|
return this.sessions.slice(0, 5)
|
||||||
|
},
|
||||||
|
sessionAvatarStyle() {
|
||||||
|
return {
|
||||||
|
'--size': this.size + 'px',
|
||||||
|
'--font-size': this.size / 2 + 'px',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
.avatar-list {
|
||||||
|
min-height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--color-main-background);
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
background-color: #b9b9b9;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
text-align: center;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: var(--size);
|
||||||
|
font-size: var(--font-size);
|
||||||
|
font-weight: normal;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: content-box !important;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -81,6 +81,7 @@ import Stack from './Stack.vue'
|
|||||||
import { NcEmptyContent } from '@nextcloud/vue'
|
import { NcEmptyContent } from '@nextcloud/vue'
|
||||||
import GlobalSearchResults from '../search/GlobalSearchResults.vue'
|
import GlobalSearchResults from '../search/GlobalSearchResults.vue'
|
||||||
import { showError } from '../../helpers/errors.js'
|
import { showError } from '../../helpers/errors.js'
|
||||||
|
import { createSession } from '../../sessions.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Board',
|
name: 'Board',
|
||||||
@@ -128,14 +129,26 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
id: 'fetchData',
|
id(newValue, oldValue) {
|
||||||
|
if (this.session) {
|
||||||
|
// close old session
|
||||||
|
this.session.close()
|
||||||
|
}
|
||||||
|
this.session = createSession(newValue)
|
||||||
|
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
showArchived() {
|
showArchived() {
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.session = createSession(this.id)
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.session.close()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { subscribe } from '@nextcloud/event-bus'
|
|||||||
import { Tooltip } from '@nextcloud/vue'
|
import { Tooltip } from '@nextcloud/vue'
|
||||||
import ClickOutside from 'vue-click-outside'
|
import ClickOutside from 'vue-click-outside'
|
||||||
import './models/index.js'
|
import './models/index.js'
|
||||||
|
import './sessions.js'
|
||||||
|
|
||||||
// the server snap.js conflicts with vertical scrolling so we disable it
|
// the server snap.js conflicts with vertical scrolling so we disable it
|
||||||
document.body.setAttribute('data-snap-ignore', 'true')
|
document.body.setAttribute('data-snap-ignore', 'true')
|
||||||
|
|||||||
56
src/services/SessionApi.js
Normal file
56
src/services/SessionApi.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import { generateOcsUrl } from '@nextcloud/router'
|
||||||
|
|
||||||
|
export class SessionApi {
|
||||||
|
|
||||||
|
url(url) {
|
||||||
|
return generateOcsUrl(`apps/deck/api/v1.0${url}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSession(boardId) {
|
||||||
|
return (await axios.put(this.url('/session/create'), { boardId })).data.ocs.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncSession(boardId, token) {
|
||||||
|
return await axios.post(this.url('/session/sync'), { boardId, token })
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeSession(boardId, token) {
|
||||||
|
return await axios.post(this.url('/session/close'), { boardId, token })
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeSessionViaBeacon(boardId, token) {
|
||||||
|
const body = {
|
||||||
|
boardId,
|
||||||
|
token,
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
type: 'application/json',
|
||||||
|
}
|
||||||
|
const blob = new Blob([JSON.stringify(body)], headers)
|
||||||
|
navigator.sendBeacon(this.url('/session/close'), blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionApi = new SessionApi()
|
||||||
137
src/sessions.js
Normal file
137
src/sessions.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { listen } from '@nextcloud/notify_push'
|
||||||
|
import { sessionApi } from './services/SessionApi.js'
|
||||||
|
import store from './store/main.js'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
|
||||||
|
const SESSION_INTERVAL = 90 // in seconds
|
||||||
|
|
||||||
|
let hasPush = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used to verify, whether an event is originated by ourselves
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
function isOurSessionToken(token) {
|
||||||
|
if (axios.defaults.headers['x-nc-deck-session']
|
||||||
|
&& axios.defaults.headers['x-nc-deck-session'].startsWith(token)) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPush = listen('deck_board_update', (name, body) => {
|
||||||
|
// ignore update events which we have triggered ourselves
|
||||||
|
if (isOurSessionToken(body._causingSessionToken)) return
|
||||||
|
|
||||||
|
// only handle update events for the currently open board
|
||||||
|
const currentBoardId = store.state.currentBoard?.id
|
||||||
|
if (body.id !== currentBoardId) return
|
||||||
|
|
||||||
|
store.dispatch('refreshBoard', currentBoardId)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is the notify_push app active and can
|
||||||
|
* provide us with real time updates?
|
||||||
|
*/
|
||||||
|
export function isNotifyPushEnabled() {
|
||||||
|
return hasPush
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param boardId
|
||||||
|
*/
|
||||||
|
export function createSession(boardId) {
|
||||||
|
|
||||||
|
if (!isNotifyPushEnabled()) {
|
||||||
|
// return a dummy object
|
||||||
|
return {
|
||||||
|
async close() {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let's try to make createSession() synchronous, so that
|
||||||
|
// the component doesn't need to bother about the asynchronousness
|
||||||
|
let tokenPromise
|
||||||
|
let token
|
||||||
|
const create = () => {
|
||||||
|
tokenPromise = sessionApi.createSession(boardId).then(res => res.token)
|
||||||
|
tokenPromise.then((t) => {
|
||||||
|
token = t
|
||||||
|
axios.defaults.headers['x-nc-deck-session'] = t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
create()
|
||||||
|
|
||||||
|
const ensureSession = async () => {
|
||||||
|
if (!tokenPromise) {
|
||||||
|
create()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await sessionApi.syncSession(boardId, await tokenPromise)
|
||||||
|
} catch (err) {
|
||||||
|
// session probably expired, let's
|
||||||
|
// create a fresh session
|
||||||
|
create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// periodically notify the server that we are still here
|
||||||
|
let interval = setInterval(ensureSession, SESSION_INTERVAL * 1000)
|
||||||
|
|
||||||
|
// close session when tab gets hidden/inactive
|
||||||
|
const visibilitychangeListener = () => {
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
sessionApi.closeSessionViaBeacon(boardId, token)
|
||||||
|
tokenPromise = null
|
||||||
|
token = null
|
||||||
|
delete axios.defaults.headers['x-nc-deck-session']
|
||||||
|
|
||||||
|
// stop session refresh interval
|
||||||
|
clearInterval(interval)
|
||||||
|
} else {
|
||||||
|
// tab is back in focus or was restored from the bfcache
|
||||||
|
ensureSession()
|
||||||
|
|
||||||
|
// we must assume that the websocket connection was
|
||||||
|
// paused and we have missed updates in the meantime.
|
||||||
|
store.dispatch('refreshBoard', store.state.currentBoard?.id)
|
||||||
|
|
||||||
|
// restart session refresh interval
|
||||||
|
interval = setInterval(ensureSession, SESSION_INTERVAL * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', visibilitychangeListener)
|
||||||
|
|
||||||
|
return {
|
||||||
|
async close() {
|
||||||
|
clearInterval(interval)
|
||||||
|
document.removeEventListener('visibilitychange', visibilitychangeListener)
|
||||||
|
await sessionApi.closeSession(boardId, await tokenPromise)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,3 +11,4 @@ default:
|
|||||||
- CommentContext
|
- CommentContext
|
||||||
- AttachmentContext
|
- AttachmentContext
|
||||||
- SearchContext
|
- SearchContext
|
||||||
|
- SessionContext
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ Feature: acl
|
|||||||
|
|
||||||
Scenario: Reshare a board
|
Scenario: Reshare a board
|
||||||
Given Logging in using web as "user0"
|
Given Logging in using web as "user0"
|
||||||
And creates a board named "Reshared board" with color "ff0000"
|
And creates a board named "Shared board" with color "ff0000"
|
||||||
And shares the board with user "user1"
|
And shares the board with user "user1"
|
||||||
| permissionEdit | 0 |
|
| permissionEdit | 0 |
|
||||||
| permissionShare | 1 |
|
| permissionShare | 1 |
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class BoardContext implements Context {
|
|||||||
return $this->card;
|
return $this->card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLastUsedBoard() {
|
||||||
|
return $this->board;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given /^creates a board with example content$/
|
* @Given /^creates a board with example content$/
|
||||||
*/
|
*/
|
||||||
@@ -57,7 +61,21 @@ class BoardContext implements Context {
|
|||||||
* @When /^fetches the board named "([^"]*)"$/
|
* @When /^fetches the board named "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
public function fetchesTheBoardNamed($boardName) {
|
public function fetchesTheBoardNamed($boardName) {
|
||||||
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $this->board['id'], []);
|
$id = null;
|
||||||
|
if (!$this->board || $boardName != $this->board['title']) {
|
||||||
|
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards', []);
|
||||||
|
$boards = json_decode((string)$this->getResponse()->getBody(), true);
|
||||||
|
foreach (array_reverse($boards) as $board) {
|
||||||
|
if ($board['title'] == $boardName) {
|
||||||
|
$id = $board['id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert::assertNotNull($id, "Could not find board named ".$boardName);
|
||||||
|
} else {
|
||||||
|
$id = $this->board['id'];
|
||||||
|
}
|
||||||
|
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $id, []);
|
||||||
$this->getResponse()->getBody()->seek(0);
|
$this->getResponse()->getBody()->seek(0);
|
||||||
$this->board = json_decode((string)$this->getResponse()->getBody(), true);
|
$this->board = json_decode((string)$this->getResponse()->getBody(), true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,8 @@ class ServerContext implements Context {
|
|||||||
public function getReqestToken(): string {
|
public function getReqestToken(): string {
|
||||||
return $this->requestToken;
|
return $this->requestToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCurrentUser(): string {
|
||||||
|
return $this->currentUser;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
tests/integration/features/bootstrap/SessionContext.php
Normal file
80
tests/integration/features/bootstrap/SessionContext.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Behat\Behat\Context\Context;
|
||||||
|
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||||
|
use PHPUnit\Framework\Assert;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
|
class SessionContext implements Context {
|
||||||
|
use RequestTrait;
|
||||||
|
|
||||||
|
/** @var ServerContext */
|
||||||
|
private $serverContext;
|
||||||
|
|
||||||
|
/** @var BoardContext */
|
||||||
|
private $boardContext;
|
||||||
|
|
||||||
|
private $tokens = [];
|
||||||
|
|
||||||
|
/** @BeforeScenario */
|
||||||
|
public function gatherContexts(BeforeScenarioScope $scope) {
|
||||||
|
$environment = $scope->getEnvironment();
|
||||||
|
|
||||||
|
$this->serverContext = $environment->getContext('ServerContext');
|
||||||
|
$this->boardContext = $environment->getContext('BoardContext');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Given user opens the board named :name
|
||||||
|
*/
|
||||||
|
public function opensTheBoardNamed($name) {
|
||||||
|
$this->boardContext->fetchesTheBoardNamed($name);
|
||||||
|
|
||||||
|
$board = $this->boardContext->getLastUsedBoard();
|
||||||
|
$this->requestContext->sendOCSRequest('PUT', '/apps/deck/api/v1.0/session/create', [
|
||||||
|
'boardId' => $board['id'],
|
||||||
|
]);
|
||||||
|
$res = json_decode((string)$this->getResponse()->getBody(), true);
|
||||||
|
Assert::assertArrayHasKey('token', $res['ocs']['data'], "session creation did not respond with a token");
|
||||||
|
|
||||||
|
// store token
|
||||||
|
$user = $this->serverContext->getCurrentUser();
|
||||||
|
$this->token[$user] = $res['ocs']['data']['token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then the response should have a list of active sessions with the length :length
|
||||||
|
*/
|
||||||
|
public function theResponseShouldHaveActiveSessions($length) {
|
||||||
|
$board = $this->boardContext->getLastUsedBoard();
|
||||||
|
Assert::assertEquals($length, count($board['activeSessions']), "unexpected count of active sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then the user :user should be in the list of active sessions
|
||||||
|
*/
|
||||||
|
public function theUserShouldBeInTheListOfActiveSessions($user) {
|
||||||
|
$board = $this->boardContext->getLastUsedBoard();
|
||||||
|
Assert::assertContains($user, $board['activeSessions'], "user is not found in the list of active sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When user closes the board named :name
|
||||||
|
*/
|
||||||
|
public function closingTheBoardNamed($name) {
|
||||||
|
$board = $this->boardContext->getLastUsedBoard();
|
||||||
|
if (!$board || $board['title'] != $name) {
|
||||||
|
$this->boardContext->fetchesTheBoardNamed($name);
|
||||||
|
$board = $this->boardContext->getLastUsedBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->serverContext->getCurrentUser();
|
||||||
|
$token = $this->token[$user];
|
||||||
|
Assert::assertNotEmpty($token, "no token for the user found");
|
||||||
|
$this->requestContext->sendOCSRequest('POST', '/apps/deck/api/v1.0/session/close', [
|
||||||
|
'boardId' => $board['id'],
|
||||||
|
'token' => $token
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
tests/integration/features/sessions.feature
Normal file
33
tests/integration/features/sessions.feature
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
Feature: Sessions
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given user "admin" exists
|
||||||
|
And user "user0" exists
|
||||||
|
And user "user1" exists
|
||||||
|
Given acting as user "user0"
|
||||||
|
And creates a board named "Shared board" with color "fafafa"
|
||||||
|
And shares the board with user "user1"
|
||||||
|
|
||||||
|
|
||||||
|
Scenario: Open a board with multiple users
|
||||||
|
Given acting as user "user0"
|
||||||
|
And user opens the board named "Shared board"
|
||||||
|
When fetches the board named "Shared board"
|
||||||
|
Then the response should have a status code "200"
|
||||||
|
And the response should have a list of active sessions with the length 1
|
||||||
|
And the user "user0" should be in the list of active sessions
|
||||||
|
|
||||||
|
Given acting as user "user1"
|
||||||
|
And user opens the board named "Shared board"
|
||||||
|
When fetches the board named "Shared board"
|
||||||
|
Then the response should have a status code "200"
|
||||||
|
And the response should have a list of active sessions with the length 2
|
||||||
|
And the user "user0" should be in the list of active sessions
|
||||||
|
And the user "user1" should be in the list of active sessions
|
||||||
|
|
||||||
|
When user closes the board named "Shared board"
|
||||||
|
And fetches the board named "Shared board"
|
||||||
|
Then the response should have a status code "200"
|
||||||
|
And the response should have a list of active sessions with the length 1
|
||||||
|
And the user "user0" should be in the list of active sessions
|
||||||
|
|
||||||
@@ -661,3 +661,16 @@ namespace OC\Files\Storage\Wrapper{
|
|||||||
public function getQuota() {}
|
public function getQuota() {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace OCA\NotifyPush\Queue {
|
||||||
|
|
||||||
|
interface IQueue {
|
||||||
|
/**
|
||||||
|
* @param string $channel
|
||||||
|
* @param mixed $message
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function push(string $channel, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ class BoardTest extends TestCase {
|
|||||||
'archived' => false,
|
'archived' => false,
|
||||||
'users' => ['user1', 'user2'],
|
'users' => ['user1', 'user2'],
|
||||||
'settings' => [],
|
'settings' => [],
|
||||||
|
'activeSessions' => [],
|
||||||
'ETag' => $board->getETag(),
|
'ETag' => $board->getETag(),
|
||||||
], $board->jsonSerialize());
|
], $board->jsonSerialize());
|
||||||
}
|
}
|
||||||
@@ -55,6 +56,7 @@ class BoardTest extends TestCase {
|
|||||||
'archived' => false,
|
'archived' => false,
|
||||||
'users' => ['user1', 'user2'],
|
'users' => ['user1', 'user2'],
|
||||||
'settings' => [],
|
'settings' => [],
|
||||||
|
'activeSessions' => [],
|
||||||
'ETag' => $board->getETag(),
|
'ETag' => $board->getETag(),
|
||||||
], $board->jsonSerialize());
|
], $board->jsonSerialize());
|
||||||
}
|
}
|
||||||
@@ -76,6 +78,7 @@ class BoardTest extends TestCase {
|
|||||||
'archived' => false,
|
'archived' => false,
|
||||||
'users' => [],
|
'users' => [],
|
||||||
'settings' => [],
|
'settings' => [],
|
||||||
|
'activeSessions' => [],
|
||||||
'ETag' => $board->getETag(),
|
'ETag' => $board->getETag(),
|
||||||
], $board->jsonSerialize());
|
], $board->jsonSerialize());
|
||||||
}
|
}
|
||||||
@@ -105,6 +108,7 @@ class BoardTest extends TestCase {
|
|||||||
'shared' => 1,
|
'shared' => 1,
|
||||||
'users' => [],
|
'users' => [],
|
||||||
'settings' => [],
|
'settings' => [],
|
||||||
|
'activeSessions' => [],
|
||||||
'ETag' => $board->getETag(),
|
'ETag' => $board->getETag(),
|
||||||
], $board->jsonSerialize());
|
], $board->jsonSerialize());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ use OCA\Deck\Db\BoardMapper;
|
|||||||
use OCA\Deck\Db\CardMapper;
|
use OCA\Deck\Db\CardMapper;
|
||||||
use OCA\Deck\Db\ChangeHelper;
|
use OCA\Deck\Db\ChangeHelper;
|
||||||
use OCA\Deck\Db\LabelMapper;
|
use OCA\Deck\Db\LabelMapper;
|
||||||
|
use OCA\Deck\Db\Session;
|
||||||
|
use OCA\Deck\Db\SessionMapper;
|
||||||
use OCA\Deck\Db\StackMapper;
|
use OCA\Deck\Db\StackMapper;
|
||||||
use OCA\Deck\Event\AclCreatedEvent;
|
use OCA\Deck\Event\AclCreatedEvent;
|
||||||
use OCA\Deck\Event\AclDeletedEvent;
|
use OCA\Deck\Event\AclDeletedEvent;
|
||||||
@@ -89,6 +91,8 @@ class BoardServiceTest extends TestCase {
|
|||||||
private $connection;
|
private $connection;
|
||||||
/** @var BoardServiceValidator */
|
/** @var BoardServiceValidator */
|
||||||
private $boardServiceValidator;
|
private $boardServiceValidator;
|
||||||
|
/** @var SessionMapper */
|
||||||
|
private $sessionMapper;
|
||||||
|
|
||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@@ -110,6 +114,7 @@ class BoardServiceTest extends TestCase {
|
|||||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||||
$this->connection = $this->createMock(IDBConnection::class);
|
$this->connection = $this->createMock(IDBConnection::class);
|
||||||
$this->boardServiceValidator = $this->createMock(BoardServiceValidator::class);
|
$this->boardServiceValidator = $this->createMock(BoardServiceValidator::class);
|
||||||
|
$this->sessionMapper = $this->createMock(SessionMapper::class);
|
||||||
|
|
||||||
$this->service = new BoardService(
|
$this->service = new BoardService(
|
||||||
$this->boardMapper,
|
$this->boardMapper,
|
||||||
@@ -130,6 +135,7 @@ class BoardServiceTest extends TestCase {
|
|||||||
$this->urlGenerator,
|
$this->urlGenerator,
|
||||||
$this->connection,
|
$this->connection,
|
||||||
$this->boardServiceValidator,
|
$this->boardServiceValidator,
|
||||||
|
$this->sessionMapper,
|
||||||
$this->userId
|
$this->userId
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -172,6 +178,11 @@ class BoardServiceTest extends TestCase {
|
|||||||
->willReturn([
|
->willReturn([
|
||||||
'admin' => 'admin',
|
'admin' => 'admin',
|
||||||
]);
|
]);
|
||||||
|
$session = $this->createMock(Session::class);
|
||||||
|
$this->sessionMapper->expects($this->once())
|
||||||
|
->method('findAllActive')
|
||||||
|
->with(1)
|
||||||
|
->willReturn([$session]);
|
||||||
$this->assertEquals($b1, $this->service->find(1));
|
$this->assertEquals($b1, $this->service->find(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +235,9 @@ class BoardServiceTest extends TestCase {
|
|||||||
->willReturn([
|
->willReturn([
|
||||||
'admin' => 'admin',
|
'admin' => 'admin',
|
||||||
]);
|
]);
|
||||||
|
$this->sessionMapper->expects($this->once())
|
||||||
|
->method('findAllActive')
|
||||||
|
->willReturn([]);
|
||||||
$b = $this->service->update(123, 'MyNewNameBoard', 'ffffff', false);
|
$b = $this->service->update(123, 'MyNewNameBoard', 'ffffff', false);
|
||||||
|
|
||||||
$this->assertEquals($b->getTitle(), 'MyNewNameBoard');
|
$this->assertEquals($b->getTitle(), 'MyNewNameBoard');
|
||||||
@@ -244,6 +258,10 @@ class BoardServiceTest extends TestCase {
|
|||||||
->willReturn([
|
->willReturn([
|
||||||
'admin' => 'admin',
|
'admin' => 'admin',
|
||||||
]);
|
]);
|
||||||
|
$this->sessionMapper->expects($this->once())
|
||||||
|
->method('findAllActive')
|
||||||
|
->with(null)
|
||||||
|
->willReturn([]);
|
||||||
$boardDeleted = clone $board;
|
$boardDeleted = clone $board;
|
||||||
$boardDeleted->setDeletedAt(1);
|
$boardDeleted->setDeletedAt(1);
|
||||||
$this->boardMapper->expects($this->once())
|
$this->boardMapper->expects($this->once())
|
||||||
|
|||||||
Reference in New Issue
Block a user