Skip to content

Commit

Permalink
feat: Saving file
Browse files Browse the repository at this point in the history
Signed-off-by: Hoang Pham <[email protected]>
  • Loading branch information
hweihwang committed Jul 22, 2024
1 parent 1fb09e1 commit 5431d6c
Show file tree
Hide file tree
Showing 13 changed files with 495 additions and 335 deletions.
15 changes: 15 additions & 0 deletions lib/Consts/JWTConsts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Consts;

final class JWTConsts {
public const JWT_ALGORITHM = 'HS256';
public const EXPIRATION_TIME = 15 * 60;
}
92 changes: 16 additions & 76 deletions lib/Controller/JWTController.php
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Controller;

use Firebase\JWT\JWT;
use OC\User\NoUserException;
use OCA\Whiteboard\Service\ConfigService;
use OCA\Whiteboard\Service\AuthenticationService;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\FileService;
use OCA\Whiteboard\Service\JWTService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IRequest;
use OCP\IUserSession;

/**
* @psalm-suppress UndefinedClass
* @psalm-suppress MissingDependency
*/
final class JWTController extends Controller {
private const EXPIRATION_TIME = 15 * 60;

public const JWT_ALGORITHM = 'HS256';

public function __construct(
IRequest $request,
private IUserSession $userSession,
private ConfigService $configService,
private IRootFolder $rootFolder
IRequest $request,
private AuthenticationService $authService,
private FileService $fileService,
private JWTService $jwtService,
private ExceptionService $exceptionService
) {
parent::__construct('whiteboard', $request);
}
Expand All @@ -44,66 +37,13 @@ public function __construct(
* @NoAdminRequired
*/
public function getJWT(int $fileId): DataResponse {
if (!$this->userSession->isLoggedIn()) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

$user = $this->userSession->getUser();

if ($user === null) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

$userId = $user->getUID();
try {
$folder = $this->rootFolder->getUserFolder($userId);
} catch (NotPermittedException $e) {
return new DataResponse(['message' => 'Access denied'], Http::STATUS_FORBIDDEN);
} catch (NoUserException $e) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

$file = $folder->getById($fileId)[0] ?? null;

if ($file === null) {
return new DataResponse(['message' => 'File not found or access denied'], Http::STATUS_FORBIDDEN);
}

try {
$readable = $file->isReadable();
} catch (InvalidPathException|NotFoundException $e) {
return new DataResponse(['message' => 'Access denied'], Http::STATUS_FORBIDDEN);
$user = $this->authService->getAuthenticatedUser();
$file = $this->fileService->getUserFileById($user->getUID(), $fileId);
$jwt = $this->jwtService->generateJWT($user, $file, $fileId);
return new DataResponse(['token' => $jwt]);
} catch (\Exception $e) {
return $this->exceptionService->handleException($e);
}

if (!$readable) {
return new DataResponse(['message' => 'Access denied'], Http::STATUS_FORBIDDEN);
}

try {
$permissions = $file->getPermissions();
} catch (InvalidPathException $e) {
return new DataResponse(['message' => 'Access denied'], Http::STATUS_FORBIDDEN);
} catch (NotFoundException $e) {
return new DataResponse(['message' => 'File not found'], Http::STATUS_NOT_FOUND);
}

$key = $this->configService->getJwtSecretKey();
$issuedAt = time();
$expirationTime = $issuedAt + self::EXPIRATION_TIME;
$payload = [
'userid' => $userId,
'fileId' => $fileId,
'permissions' => $permissions,
'user' => [
'id' => $userId,
'name' => $user->getDisplayName()
],
'iat' => $issuedAt,
'exp' => $expirationTime
];

$jwt = JWT::encode($payload, $key, self::JWT_ALGORITHM);

return new DataResponse(['token' => $jwt]);
}
}
116 changes: 24 additions & 92 deletions lib/Controller/WhiteboardController.php
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Controller;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use JsonException;
use OC\User\NoUserException;
use OCA\Whiteboard\Service\ConfigService;
use Exception;
use OCA\Whiteboard\Service\AuthenticationService;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\FileService;
use OCA\Whiteboard\Service\WhiteboardContentService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IRequest;
use OCP\IUserManager;

/**
* @psalm-suppress UndefinedClass
Expand All @@ -32,105 +29,40 @@ final class WhiteboardController extends ApiController {
public function __construct(
$appName,
IRequest $request,
private IRootFolder $rootFolder,
private ConfigService $configService,
private IUserManager $userManager
private AuthenticationService $authService,
private FileService $fileService,
private WhiteboardContentService $contentService,
private ExceptionService $exceptionService
) {
parent::__construct($appName, $request);
}

/**
* @throws NotPermittedException
* @throws NoUserException
* @throws JsonException
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function show(int $fileId): DataResponse {
$authHeader = $this->request->getHeader('Authorization');

if (!$authHeader) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

$assignedValues = sscanf($authHeader, 'Bearer %s', $jwt);

if (!$assignedValues) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

if (!$jwt || !is_string($jwt)) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

try {
$key = $this->configService->getJwtSecretKey();
$decoded = JWT::decode($jwt, new Key($key, JWTController::JWT_ALGORITHM));
$userId = $decoded->userid;
} catch (\Exception $e) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

$userFolder = $this->rootFolder->getUserFolder($userId);
$file = $userFolder->getById($fileId)[0];

$fileContent = $file->getContent();
if ($fileContent === '') {
$fileContent = '{"elements":[],"scrollToContent":true}';
$userId = $this->authService->authenticateJWT($this->request);
$file = $this->fileService->getUserFileById($userId, $fileId);
$data = $this->contentService->getContent($file);
return new DataResponse(['data' => $data]);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
$data = json_decode($fileContent, true, 512, JSON_THROW_ON_ERROR);

return new DataResponse([
'data' => $data,
]);
}

private function verifySharedToken(string $token, int $fileId): bool {
[$roomId, $timestamp, $signature] = explode(':', $token);

if ($roomId !== (string)$fileId) {
return false;
}

$sharedSecret = $this->configService->getWhiteboardSharedSecret();
$payload = "{$roomId}:{$timestamp}";
$expectedSignature = hash_hmac('sha256', $payload, $sharedSecret);

return hash_equals($expectedSignature, $signature);
}

/**
* @throws NotPermittedException
* @throws NoUserException
* @throws JsonException
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function update(int $fileId, array $data): DataResponse {
$whiteboardAuth = $this->request->getHeader('X-Whiteboard-Auth');
$whiteboardUser = $this->request->getHeader('X-Whiteboard-User');

if (!$whiteboardAuth || !$this->verifySharedToken($whiteboardAuth, $fileId)) {
return new DataResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED);
}

$user = $this->userManager->get($whiteboardUser);

if (!$user) {
return new DataResponse(['message' => 'Invalid user'], Http::STATUS_BAD_REQUEST);
}

$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$file = $userFolder->getById($fileId)[0];

if (empty($data)) {
$data = ['elements' => [], 'scrollToContent' => true];
try {
$this->authService->authenticateSharedToken($this->request, $fileId);
$user = $this->authService->getAndSetUser($this->request);
$file = $this->fileService->getUserFileById($user->getUID(), $fileId);
$this->contentService->updateContent($file, $data);
return new DataResponse(['status' => 'success']);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}

$file->putContent(json_encode($data, JSON_THROW_ON_ERROR));

return new DataResponse(['status' => 'success']);
}
}
Loading

0 comments on commit 5431d6c

Please sign in to comment.