Skip to content

Commit 92f3125

Browse files
committed
ex app harp
Signed-off-by: Hoang Pham <[email protected]>
1 parent d29d159 commit 92f3125

File tree

14 files changed

+3430
-1
lines changed

14 files changed

+3430
-1
lines changed

appinfo/routes.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@
2121
['name' => 'Whiteboard#show', 'url' => '{fileId}', 'verb' => 'GET'],
2222
/** @see SettingsController::update() */
2323
['name' => 'Settings#update', 'url' => 'settings', 'verb' => 'POST'],
24+
/** @see ExAppController::updateSettings() */
25+
['name' => 'ExApp#updateSettings', 'url' => 'ex_app/settings', 'verb' => 'POST'],
2426
]
2527
];

lib/AppInfo/Application.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010

1111
namespace OCA\Whiteboard\AppInfo;
1212

13+
use OCA\AppAPI\Middleware\AppAPIAuthMiddleware;
1314
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
1415
use OCA\Viewer\Event\LoadViewer;
1516
use OCA\Whiteboard\Listener\AddContentSecurityPolicyListener;
1617
use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener;
1718
use OCA\Whiteboard\Listener\LoadViewerListener;
1819
use OCA\Whiteboard\Listener\RegisterTemplateCreatorListener;
20+
use OCA\Whiteboard\Service\ExAppService;
1921
use OCA\Whiteboard\Settings\SetupCheck;
2022
use OCP\AppFramework\App;
2123
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -26,6 +28,8 @@
2628
use OCP\IL10N;
2729
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
2830
use OCP\Util;
31+
use Psr\Container\ContainerExceptionInterface;
32+
use Psr\Container\NotFoundExceptionInterface;
2933

3034
/**
3135
* @psalm-suppress UndefinedClass
@@ -47,6 +51,10 @@ public function register(IRegistrationContext $context): void {
4751
$context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class);
4852
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
4953
$context->registerSetupCheck(SetupCheck::class);
54+
55+
if (class_exists(AppAPIAuthMiddleware::class) && $this->getExAppService()->isWhiteboardWebsocketEnabled()) {
56+
$context->registerMiddleware(AppAPIAuthMiddleware::class);
57+
}
5058
}
5159

5260
#[\Override]
@@ -60,4 +68,12 @@ public function boot(IBootContext $context): void {
6068
});
6169
}
6270
}
71+
72+
/**
73+
* @throws ContainerExceptionInterface
74+
* @throws NotFoundExceptionInterface
75+
*/
76+
private function getExAppService(): ExAppService {
77+
return $this->getContainer()->get(ExAppService::class);
78+
}
6379
}

lib/Consts/ExAppConsts.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\Whiteboard\Consts;
11+
12+
final class ExAppConsts {
13+
public const APP_API_ID = 'app_api';
14+
public const WHITEBOARD_EX_APP_ID = 'nextcloud_whiteboard';
15+
public const WHITEBOARD_EX_APP_ENABLED_KEY = 'isWhiteboardExAppEnabled';
16+
}

lib/Controller/ExAppController.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Whiteboard\Controller;
11+
12+
use Exception;
13+
use OCA\AppAPI\Attribute\AppAPIAuth;
14+
use OCA\Whiteboard\Service\ConfigService;
15+
use OCA\Whiteboard\Service\ExceptionService;
16+
use OCP\AppFramework\Controller;
17+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
18+
use OCP\AppFramework\Http\Attribute\PublicPage;
19+
use OCP\AppFramework\Http\DataResponse;
20+
use OCP\IRequest;
21+
22+
/**
23+
* @psalm-suppress UndefinedClass
24+
* @psalm-suppress MissingDependency
25+
* @psalm-suppress UndefinedAttributeClass
26+
*/
27+
final class ExAppController extends Controller {
28+
public function __construct(
29+
IRequest $request,
30+
private ExceptionService $exceptionService,
31+
private ConfigService $configService,
32+
) {
33+
parent::__construct('whiteboard', $request);
34+
}
35+
36+
#[NoCSRFRequired]
37+
#[PublicPage]
38+
#[AppAPIAuth]
39+
public function updateSettings(): DataResponse {
40+
try {
41+
$serverUrl = $this->request->getParam('serverUrl');
42+
$secret = $this->request->getParam('secret');
43+
44+
if ($serverUrl !== null) {
45+
$this->configService->setCollabBackendUrl($serverUrl);
46+
}
47+
48+
if ($secret !== null) {
49+
$this->configService->setWhiteboardSharedSecret($secret);
50+
}
51+
52+
return new DataResponse([
53+
'message' => 'Settings updated',
54+
]);
55+
} catch (Exception $e) {
56+
return $this->exceptionService->handleException($e);
57+
}
58+
}
59+
}

lib/Service/ExAppService.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Whiteboard\Service;
11+
12+
use OCA\AppAPI\Service\ExAppService as AppAPIService;
13+
use OCA\Whiteboard\Consts\ExAppConsts;
14+
use OCP\App\IAppManager;
15+
use OCP\AppFramework\Services\IInitialState;
16+
use Psr\Container\ContainerInterface;
17+
use Psr\Log\LoggerInterface;
18+
use Throwable;
19+
20+
/**
21+
* @psalm-suppress UndefinedClass
22+
* @psalm-suppress MissingDependency
23+
*/
24+
final class ExAppService {
25+
private ?AppAPIService $appAPIService = null;
26+
27+
public function __construct(
28+
private IAppManager $appManager,
29+
private ContainerInterface $container,
30+
private IInitialState $initialState,
31+
private LoggerInterface $logger,
32+
) {
33+
$this->initAppAPIService();
34+
}
35+
36+
private function initAppAPIService(): void {
37+
$isAppAPIEnabled = $this->isAppAPIEnabled();
38+
39+
if (class_exists(AppAPIService::class) && $isAppAPIEnabled) {
40+
try {
41+
$this->appAPIService = $this->container->get(AppAPIService::class);
42+
} catch (Throwable $e) {
43+
$this->logger->error('exApp', [$e->getMessage()]);
44+
}
45+
}
46+
}
47+
48+
private function isAppAPIEnabled(): bool {
49+
return $this->appManager->isEnabledForUser(ExAppConsts::APP_API_ID);
50+
}
51+
52+
public function isExAppEnabled(string $appId): bool {
53+
if ($this->appAPIService === null) {
54+
return false;
55+
}
56+
57+
return $this->appAPIService->getExApp($appId)?->getEnabled() === 1;
58+
}
59+
60+
public function isWhiteboardWebsocketEnabled(): bool {
61+
return $this->isExAppEnabled(ExAppConsts::WHITEBOARD_EX_APP_ID);
62+
}
63+
64+
public function initFrontendState(): void {
65+
$this->initialState->provideInitialState(
66+
ExAppConsts::WHITEBOARD_EX_APP_ENABLED_KEY,
67+
$this->isWhiteboardWebsocketEnabled()
68+
);
69+
}
70+
}

lib/Settings/Admin.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace OCA\Whiteboard\Settings;
99

1010
use OCA\Whiteboard\Service\ConfigService;
11+
use OCA\Whiteboard\Service\ExAppService;
1112
use OCA\Whiteboard\Service\JWTService;
1213
use OCP\AppFramework\Http\ContentSecurityPolicy;
1314
use OCP\AppFramework\Http\TemplateResponse;
@@ -19,6 +20,7 @@ public function __construct(
1920
private IInitialState $initialState,
2021
private ConfigService $configService,
2122
private JWTService $jwtService,
23+
private ExAppService $exAppService,
2224
) {
2325
}
2426

@@ -44,6 +46,10 @@ public function getForm(): TemplateResponse {
4446

4547
#[\Override]
4648
public function getSection() {
49+
if ($this->exAppService->isWhiteboardWebsocketEnabled()) {
50+
return null;
51+
}
52+
4753
return 'whiteboard';
4854
}
4955

websocket_server/.dockerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
node_modules/
5+
*.pem
6+
appinfo
7+
REUSE.toml
8+
.gitignore

websocket_server/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
1+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
22
# SPDX-License-Identifier: AGPL-3.0-or-later
33

44
*.pem
5+
node_modules/

websocket_server/AppManager.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export default class AppManager {
2626
if (Config.METRICS_TOKEN) {
2727
this.app.get('/metrics', this.metricsHandler.bind(this))
2828
}
29+
30+
// Ex App
31+
this.app.get('/heartbeat', this.heartbeatHandler.bind(this))
32+
this.app.put('/enabled', express.json(), this.enabledHandler.bind(this))
2933
}
3034

3135
/**
@@ -139,6 +143,58 @@ export default class AppManager {
139143
res.send(JSON.stringify(response))
140144
}
141145

146+
// Ex App
147+
heartbeatHandler(req, res) {
148+
res.status(200).json({ status: 'ok' })
149+
}
150+
151+
async enabledHandler(req, res) {
152+
try {
153+
const authHeader = req.headers['authorization-app-api']
154+
155+
if (!authHeader) {
156+
return res
157+
.status(401)
158+
.send('Unauthorized: Missing AUTHORIZATION-APP-API header')
159+
}
160+
161+
const headers = {
162+
'EX-APP-ID': 'nextcloud_whiteboard',
163+
'EX-APP-VERSION': '0.0.1',
164+
'OCS-APIRequest': 'true',
165+
'AUTHORIZATION-APP-API': authHeader,
166+
'Content-Type': 'application/json',
167+
}
168+
169+
const response = await fetch(
170+
`${Config.NEXTCLOUD_URL}/index.php/apps/whiteboard/ex_app/settings`,
171+
{
172+
method: 'POST',
173+
headers,
174+
body: JSON.stringify({
175+
serverUrl: `${Config.NEXTCLOUD_WEBSOCKET_URL}/index.php/apps/app_api/proxy/whiteboard_websocket`,
176+
secret: Config.JWT_SECRET_KEY,
177+
}),
178+
},
179+
)
180+
181+
if (!response.ok) {
182+
const responseBody = await response.text()
183+
throw new Error(
184+
`HTTP error! status: ${response.status}, body: ${responseBody}`,
185+
)
186+
}
187+
188+
const data = await response.json()
189+
res.status(200).json(data)
190+
} catch (error) {
191+
console.error('Error updating Nextcloud config:', error)
192+
res
193+
.status(500)
194+
.send(`Failed to update Nextcloud configuration: ${error.message}`)
195+
}
196+
}
197+
142198
getApp() {
143199
return this.app
144200
}

websocket_server/Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# syntax=docker/dockerfile:latest
2+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
### Build Stage ###
6+
FROM node:23.11.1-alpine3.21 AS build
7+
8+
ENV NODE_ENV=production
9+
WORKDIR /app
10+
COPY . .
11+
12+
RUN apk add --no-cache python3 make g++ \
13+
&& npm clean-install \
14+
&& apk del python3 make g++
15+
16+
### Runtime Stage ###
17+
FROM node:23.11.1-alpine3.21
18+
19+
# Install frp for HaRP integration
20+
RUN apk add --no-cache bash frp
21+
22+
# Copy start.sh and application
23+
COPY start.sh /usr/local/bin/start.sh
24+
RUN chmod +x /usr/local/bin/start.sh
25+
COPY --from=build /app /app
26+
WORKDIR /app
27+
28+
# Entrypoint
29+
ENTRYPOINT ["/usr/local/bin/start.sh"]

0 commit comments

Comments
 (0)