backend: Add callback to application to check editing key status (#525)

This commit is contained in:
Eric Doughty-Papassideris
2024-08-25 22:26:30 +02:00
parent 9b0dcc1068
commit 483b1b0688
8 changed files with 171 additions and 17 deletions
@@ -0,0 +1,48 @@
import { NextFunction, Request, Response } from 'express';
import logger from '@/lib/logger';
import onlyofficeService, { CommandError, ErrorCode } from '@/services/onlyoffice.service';
interface RequestQuery {
editing_session_key: string;
}
async function ignoreMissingKeyErrorButNoneElse(res: Response, call: () => Promise<void>): Promise<void> {
try {
await call();
} catch (e) {
if (e instanceof CommandError && e.errorCode == ErrorCode.KEY_MISSING_OR_DOC_NOT_FOUND) {
return void (await res.send({ info: 'Unknown editing_session_key' }));
}
logger.error('Running OO command for TDrive backend', e);
return void (await res.sendStatus(500));
}
}
/**
* These routes are called by Twake Drive backend, for ex. before editing or retreiving a file,
* if it has an editing_session_key still, get the status of that and force a resolution.
*/
export default class TwakeDriveBackendCallbackController {
/**
* Get status of an `editing_session_key` from OO, and return a URL to get the latest version,
* or an object with no `url` property, in which case the key is not known as forgotten by OO and should
* be considered lost after an admin alert.
*/
public async checkSessionStatus(req: Request<RequestQuery>, res: Response): Promise<void> {
//TODO: Find a way to check if the key is live (`info` uses the callback url)
await ignoreMissingKeyErrorButNoneElse(res, async () => {
await res.send({ url: await onlyofficeService.getForgotten(req.params.editing_session_key) });
});
}
/**
* Force deletion of the provided `editing_session_key` in the OO document server.
* If the key was succesfully deleted, the `done` property in the response body will be true.
*/
public async deleteSessionKey(req: Request<RequestQuery>, res: Response): Promise<void> {
await ignoreMissingKeyErrorButNoneElse(res, async () => {
await onlyofficeService.deleteForgotten(req.params.editing_session_key);
await res.send({ done: true });
});
}
}
@@ -34,6 +34,21 @@ export default async (req: Request<{}, {}, {}, RequestQuery>, res: Response, nex
return res.status(401).send({ message: 'invalid token' });
}
const authHeaderToken = req.header('authorization');
try {
if (authHeaderToken) {
const fromTwakeDriveToken = jwt.verify(authHeaderToken, CREDENTIALS_SECRET) as Record<string, string>;
// The following constant comes from tdrive/backend/node/src/services/applications-api/index.ts
// This is in the case when Twake Drive backend makes requests from the connector,
// but not on the behalf of a specific user, eg. when checking stale only office keys
if (fromTwakeDriveToken['type'] != 'tdriveToApplication') throw new Error(`Bad type in token ${JSON.stringify(fromTwakeDriveToken)}`);
return next();
}
} catch (e) {
logger.error(`Invalid token in Authorization header from Twake Drive backend`, e);
return res.status(401).json({ message: 'invalid token' });
}
const user = await userService.getCurrentUser(token);
if (!user || !user.id) {
@@ -0,0 +1,21 @@
import TwakeDriveBackendCallbackController from '@/controllers/backend-callbacks.controller';
import { Routes } from '@/interfaces/routes.interface';
import authMiddleware from '@/middlewares/auth.middleware';
import { Router } from 'express';
/**
* These routes are called by Twake Drive backend, for ex. before editing or retreiving a file,
* if it has an editing_session_key still, get the status of that and force a resolution.
*/
export default class TwakeDriveBackendCallbackRoutes implements Routes {
public readonly router = Router();
public readonly path = '/tdriveApi/1';
private readonly controller: TwakeDriveBackendCallbackController;
constructor() {
this.controller = new TwakeDriveBackendCallbackController();
// Why post ? to garantee it is never cached and always ran
this.router.post('/session/:editing_session_key/check', authMiddleware, this.controller.checkSessionStatus);
this.router.delete('/session/:editing_session_key', authMiddleware, this.controller.deleteSessionKey);
}
}
@@ -3,13 +3,14 @@ import { Routes } from '@/interfaces/routes.interface';
import authMiddleware from '@/middlewares/auth.middleware';
import requirementsMiddleware from '@/middlewares/requirements.middleware';
import { Router } from 'express';
import { SERVER_PREFIX } from '@config';
/**
* When the user previews or edits a file in Twake Drive, their browser is sent to these routes
* which return a webpage that instantiates the client side JS Only Office component.
*/
class IndexRoute implements Routes {
public path = '/';
public path = SERVER_PREFIX;
public router = Router();
public indexController: IndexController;
@@ -19,8 +20,8 @@ class IndexRoute implements Routes {
}
private initRoutes = () => {
this.router.get(this.path, requirementsMiddleware, authMiddleware, this.indexController.index);
this.router.get(this.path + 'editor', requirementsMiddleware, authMiddleware, this.indexController.editor);
this.router.get('/', requirementsMiddleware, authMiddleware, this.indexController.index);
this.router.get('/editor', requirementsMiddleware, authMiddleware, this.indexController.editor);
};
}
@@ -2,9 +2,10 @@ import OnlyOfficeController from '@/controllers/onlyoffice.controller';
import { Routes } from '@/interfaces/routes.interface';
import requirementsMiddleware from '@/middlewares/requirements.middleware';
import { Router } from 'express';
import { SERVER_PREFIX } from '@config';
class OnlyOfficeRoute implements Routes {
public path = '/';
public path = SERVER_PREFIX;
public router = Router();
public onlyOfficeController: OnlyOfficeController;
@@ -14,8 +15,8 @@ class OnlyOfficeRoute implements Routes {
}
private initRoutes = () => {
this.router.get(`${this.path}:mode/read`, requirementsMiddleware, this.onlyOfficeController.read);
this.router.post(`${this.path}:mode/callback`, requirementsMiddleware, this.onlyOfficeController.ooCallback);
this.router.get(`:mode/read`, requirementsMiddleware, this.onlyOfficeController.read);
this.router.post(`:mode/callback`, requirementsMiddleware, this.onlyOfficeController.ooCallback);
};
}
@@ -1,7 +1,8 @@
import App from '@/app';
import IndexRoute from './routes/index.route';
import OnlyOfficeRoute from './routes/onlyoffice.route';
import TwakeDriveBackendCallbacksRoutes from './routes/backend-callbacks.route';
const app = new App([new IndexRoute(), new OnlyOfficeRoute()]);
const app = new App([new IndexRoute(), new OnlyOfficeRoute(), new TwakeDriveBackendCallbacksRoutes()]);
app.listen();
@@ -62,6 +62,15 @@ export namespace Callback {
}
}
/** For error responses from the {@see CommandService} */
export class CommandError extends Error {
constructor(public readonly errorCode: ErrorCode, req: any, res: any) {
super(
`OnlyOffice command service error ${ErrorCodeFromValue(errorCode)} (${errorCode}): Requested ${JSON.stringify(req)} got ${JSON.stringify(res)}`,
);
}
}
/**
* Helpers to define the protocol of the OnlyOffice editor service command API
* @see https://api.onlyoffice.com/editors/command/
@@ -77,16 +86,6 @@ namespace CommandService {
error: Exclude<ErrorCode, ErrorCode.SUCCESS>;
}
export class CommandError extends Error {
constructor(errorCode: ErrorCode, req: any, res: any) {
super(
`OnlyOffice command service error ${ErrorCodeFromValue(errorCode)} (${errorCode}): Requested ${JSON.stringify(req)} got ${JSON.stringify(
res,
)}`,
);
}
}
abstract class BaseRequest<TSuccessResponse extends SuccessResponse> {
constructor(public readonly c: string) {}