Skip to content

Commit

Permalink
Whiteboard exapp
Browse files Browse the repository at this point in the history
Signed-off-by: Hoang Pham <[email protected]>
  • Loading branch information
hweihwang committed Oct 15, 2024
1 parent 94ea4be commit a543800
Show file tree
Hide file tree
Showing 13 changed files with 1,783 additions and 5 deletions.
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace OCA\Whiteboard\AppInfo;

use OCA\AppAPI\Middleware\AppAPIAuthMiddleware;
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Viewer\Event\LoadViewer;
use OCA\Whiteboard\Listener\AddContentSecurityPolicyListener;
Expand Down Expand Up @@ -44,6 +45,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(LoadViewer::class, LoadViewerListener::class);
$context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerMiddleware(AppAPIAuthMiddleware::class);
}

public function boot(IBootContext $context): void {
Expand Down
6 changes: 6 additions & 0 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
namespace OCA\Whiteboard\Controller;

use Exception;
use OCA\AppAPI\Attribute\AppAPIAuth;
use OCA\Whiteboard\Service\ConfigService;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\JWTService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;

Expand All @@ -31,6 +34,9 @@ public function __construct(
parent::__construct('whiteboard', $request);
}

#[AppAPIAuth]

Check failure on line 37 in lib/Controller/SettingsController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedAttributeClass

lib/Controller/SettingsController.php:37:4: UndefinedAttributeClass: Attribute class OCA\AppAPI\Attribute\AppAPIAuth does not exist (see https://psalm.dev/241)
#[PublicPage]
#[NoCSRFRequired]
public function update(): DataResponse {
try {
$serverUrl = $this->request->getParam('serverUrl');
Expand Down
2 changes: 1 addition & 1 deletion src/collaboration/Portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class Portal {
auth: {
token,
},
transports: ['websocket'],
transports: ['polling'],
timeout: 10000,
}).connect()

Expand Down
3 changes: 3 additions & 0 deletions websocket_server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
*.pem
appinfo
1 change: 1 addition & 0 deletions websocket_server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# SPDX-License-Identifier: AGPL-3.0-or-later

*.pem
node_modules/
2 changes: 1 addition & 1 deletion websocket_server/ApiService.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dotenv.config()
export default class ApiService {

constructor(tokenGenerator) {
this.NEXTCLOUD_URL = process.env.NEXTCLOUD_URL
this.NEXTCLOUD_URL = 'http://nextcloud'
this.IS_DEV = Utils.parseBooleanFromEnv(process.env.IS_DEV)
this.agent = this.IS_DEV ? new https.Agent({ rejectUnauthorized: false }) : null
this.tokenGenerator = tokenGenerator
Expand Down
71 changes: 69 additions & 2 deletions websocket_server/AppManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,30 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/* eslint-disable no-console */

import dotenv from 'dotenv'
import express from 'express'
import PrometheusDataManager from './PrometheusDataManager.js'

import fetch from 'node-fetch'
import getOrCreateJwtSecretKey from './JwtSecretManager.js'
dotenv.config()

export default class AppManager {

constructor(storageManager) {
this.app = express()
this.storageManager = storageManager
this.metricsManager = new PrometheusDataManager(storageManager)
this.METRICS_TOKEN = process.env.METRICS_TOKEN
this.JWT_SECRET_KEY = getOrCreateJwtSecretKey()
this.setupRoutes()
}

setupRoutes() {
this.app.get('/', this.homeHandler.bind(this))
this.app.get('/metrics', this.metricsHandler.bind(this))
this.app.get('/heartbeat', this.heartbeatHandler.bind(this))
this.app.put('/enabled', express.json(), this.enabledHandler.bind(this))
}

homeHandler(req, res) {
Expand All @@ -39,6 +44,68 @@ export default class AppManager {
res.end(metrics)
}

heartbeatHandler(req, res) {
res.status(200).json({ status: 'ok' })
}

async enabledHandler(req, res) {
try {
const authHeader = req.headers['authorization-app-api']

console.log('authHeader', authHeader)

if (!authHeader) {
return res
.status(401)
.send('Unauthorized: Missing AUTHORIZATION-APP-API header')
}

const [userId, appSecret] = Buffer.from(authHeader, 'base64')
.toString()
.split(':')
if (appSecret !== process.env.APP_SECRET) {
return res.status(401).send('Unauthorized: Invalid APP_SECRET')
}

const headers = {
'EX-APP-ID': process.env.APP_ID,
'EX-APP-VERSION': process.env.APP_VERSION,
'OCS-APIRequest': 'true',
'AUTHORIZATION-APP-API': Buffer.from(
`${userId}:${process.env.APP_SECRET}`,
).toString('base64'),
'Content-Type': 'application/json',
}

const response = await fetch(
'http://nextcloud/index.php/apps/whiteboard/settings',
{
method: 'POST',
headers,
body: JSON.stringify({
serverUrl: `${process.env.NEXTCLOUD_URL}/apps/app_api/proxy/whiteboard_websocket`,
secret: this.JWT_SECRET_KEY,
}),
},
)

if (!response.ok) {
const responseBody = await response.text()
throw new Error(
`HTTP error! status: ${response.status}, body: ${responseBody}`,
)
}

const data = await response.json()
res.status(200).json(data)
} catch (error) {
console.error('Error updating Nextcloud config:', error)
res
.status(500)
.send(`Failed to update Nextcloud configuration: ${error.message}`)
}
}

getApp() {
return this.app
}
Expand Down
26 changes: 26 additions & 0 deletions websocket_server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM node:22.9.0-alpine3.20 AS build
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
ARG NODE_ENV=production
COPY ./ /app
WORKDIR /app
RUN apk upgrade --no-cache -a && \
apk add --no-cache ca-certificates && \
npm install --global clean-modules && \
npm clean-install && \
clean-modules --yes && \
npm cache clean --force

FROM node:22.9.0-alpine3.20
COPY --from=build --chown=nobody:nobody /app /app
WORKDIR /app
RUN apk upgrade --no-cache -a && \
apk add --no-cache ca-certificates tzdata netcat-openbsd curl

ENV PORT=23000
ENV TLS=false
ENV STORAGE_STRATEGY=lru

USER nobody
EXPOSE 23000
ENTRYPOINT ["npm", "run", "start"]
HEALTHCHECK CMD nc -z 127.0.0.1 23000 || exit 1
22 changes: 22 additions & 0 deletions websocket_server/JwtSecretManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/* eslint-disable no-console */

import crypto from 'crypto'
import dotenv from 'dotenv'

dotenv.config()

export default function getOrCreateJwtSecretKey() {
if (!process.env.JWT_SECRET_KEY) {
const newSecret = crypto.randomBytes(32).toString('hex')
process.env.JWT_SECRET_KEY = newSecret
console.log('Generated new JWT_SECRET_KEY:', newSecret)
} else {
console.log('Using existing JWT_SECRET_KEY from environment')
}
return process.env.JWT_SECRET_KEY
}
3 changes: 2 additions & 1 deletion websocket_server/SharedTokenGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import crypto from 'crypto'
import dotenv from 'dotenv'
import getOrCreateJwtSecretKey from './JwtSecretManager.js'

dotenv.config()

export default class SharedTokenGenerator {

constructor() {
this.SHARED_SECRET = process.env.JWT_SECRET_KEY
this.SHARED_SECRET = getOrCreateJwtSecretKey()
}

handle(roomId) {
Expand Down
32 changes: 32 additions & 0 deletions websocket_server/appinfo/info.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0"?>
<info>
<id>whiteboard_websocket</id>
<name>Whiteboard WebSocket Server</name>
<summary>WebSocket server for Whiteboard app</summary>
<description>A WebSocket server implementation as an external app for Nextcloud Whiteboard</description>
<version>0.0.1</version>
<licence>AGPL</licence>
<author>Nextcloud GmbH</author>
<namespace>WhiteboardWebSocket</namespace>
<category>tools</category>
<bugs>https://your-github-repo/issues</bugs>
<dependencies>
<nextcloud min-version="27" max-version="30"/>
</dependencies>
<external-app>
<docker-install>
<registry>docker.io</registry>
<image>hweihwang/whiteboard_websocket</image>
<image-tag>latest</image-tag>
</docker-install>
<routes>
<route>
<url>.*</url>
<verb>GET,POST,PUT,DELETE,OPTIONS</verb>
<access_level>PUBLIC</access_level>
<headers_to_exclude>[]</headers_to_exclude>
<bruteforce_protection>[401, 500]</bruteforce_protection>
</route>
</routes>
</external-app>
</info>
Loading

0 comments on commit a543800

Please sign in to comment.