From dcb6fa1c362635ce1eaf017142edd86ade1a2c2f Mon Sep 17 00:00:00 2001 From: Montassar Ghanmy Date: Tue, 27 Feb 2024 10:35:01 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=96=A5=EF=B8=8F=20Onlyoffice=20connector?= =?UTF-8?q?=20moved=20into=20the=20TwakeDrive=20repository=20=20(#366)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../publish-onlyoffice-connector.yml | 54 +++++++ .../onlyoffice-connector/.editorconfig | 9 ++ .../onlyoffice-connector/.eslintignore | 2 + .../connectors/onlyoffice-connector/.eslintrc | 33 ++++ .../onlyoffice-connector/.gitignore | 106 ++++++++++++ .../onlyoffice-connector/.prettierrc | 8 + tdrive/connectors/onlyoffice-connector/.swcrc | 36 +++++ .../onlyoffice-connector/assets/empty.docx | Bin 0 -> 10230 bytes .../onlyoffice-connector/assets/empty.pptx | Bin 0 -> 8704 bytes .../onlyoffice-connector/assets/empty.xlsx | Bin 0 -> 5933 bytes .../onlyoffice-connector/assets/logo.png | Bin 0 -> 28156 bytes .../onlyoffice-connector/package.json | 56 +++++++ .../onlyoffice-connector/src/app.ts | 74 +++++++++ .../onlyoffice-connector/src/config/index.ts | 14 ++ .../src/controllers/index.controller.ts | 126 +++++++++++++++ .../src/controllers/onlyoffice.controller.ts | 128 +++++++++++++++ .../src/interfaces/api.interface.ts | 31 ++++ .../src/interfaces/drive.interface.ts | 22 +++ .../src/interfaces/editor.interface.ts | 42 +++++ .../src/interfaces/file.interface.ts | 23 +++ .../src/interfaces/routes.interface.ts | 16 ++ .../src/interfaces/user.interface.ts | 14 ++ .../src/middlewares/auth.middleware.ts | 70 ++++++++ .../src/middlewares/error.middleware.ts | 15 ++ .../middlewares/requirements.middleware.ts | 16 ++ .../src/routes/index.route.ts | 23 +++ .../src/routes/onlyoffice.route.ts | 22 +++ .../onlyoffice-connector/src/server.ts | 7 + .../src/services/api.service.ts | 152 ++++++++++++++++++ .../src/services/drive.service.ts | 49 ++++++ .../src/services/editor.service.ts | 91 +++++++++++ .../src/services/file.service.ts | 82 ++++++++++ .../src/services/logger.service.ts | 5 + .../src/services/user.service.ts | 22 +++ .../src/types/express.d.ts | 9 ++ .../onlyoffice-connector/src/views/index.eta | 92 +++++++++++ .../onlyoffice-connector/tsconfig.json | 36 +++++ .../onlyoffice/local.json | 57 +++++++ tdrive/docker-compose.dev.mongo.yml | 7 +- tdrive/docker/onlyoffice-connector/Dockerfile | 11 ++ 40 files changed, 1557 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-onlyoffice-connector.yml create mode 100644 tdrive/connectors/onlyoffice-connector/.editorconfig create mode 100644 tdrive/connectors/onlyoffice-connector/.eslintignore create mode 100644 tdrive/connectors/onlyoffice-connector/.eslintrc create mode 100644 tdrive/connectors/onlyoffice-connector/.gitignore create mode 100644 tdrive/connectors/onlyoffice-connector/.prettierrc create mode 100644 tdrive/connectors/onlyoffice-connector/.swcrc create mode 100644 tdrive/connectors/onlyoffice-connector/assets/empty.docx create mode 100644 tdrive/connectors/onlyoffice-connector/assets/empty.pptx create mode 100644 tdrive/connectors/onlyoffice-connector/assets/empty.xlsx create mode 100644 tdrive/connectors/onlyoffice-connector/assets/logo.png create mode 100644 tdrive/connectors/onlyoffice-connector/package.json create mode 100644 tdrive/connectors/onlyoffice-connector/src/app.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/config/index.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/controllers/index.controller.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/interfaces/api.interface.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/interfaces/file.interface.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/interfaces/routes.interface.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/interfaces/user.interface.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/middlewares/auth.middleware.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/middlewares/error.middleware.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/middlewares/requirements.middleware.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/routes/index.route.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/server.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/services/api.service.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/services/file.service.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/services/logger.service.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/services/user.service.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/types/express.d.ts create mode 100644 tdrive/connectors/onlyoffice-connector/src/views/index.eta create mode 100644 tdrive/connectors/onlyoffice-connector/tsconfig.json create mode 100644 tdrive/default-configuration/onlyoffice/local.json create mode 100644 tdrive/docker/onlyoffice-connector/Dockerfile diff --git a/.github/workflows/publish-onlyoffice-connector.yml b/.github/workflows/publish-onlyoffice-connector.yml new file mode 100644 index 00000000..4432627d --- /dev/null +++ b/.github/workflows/publish-onlyoffice-connector.yml @@ -0,0 +1,54 @@ +name: publish-onlyoffice-connector + +# Deploy onlyoffice-connector only if push on "main" +on: + push: + branches: [main] + paths: + - "tdrive/docker/**" + - "tdrive/onlyoffice-connector/**" + - ".github/workflows/**" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [16.x] + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies and build + run: | + npm ci + npm run build --if-present + npm run lint + working-directory: ./server + env: + CI: true + + publish: + runs-on: ubuntu-20.04 + needs: build + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + with: + name: tdrive/onlyoffice-connector + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + workdir: . + registry: docker-registry.linagora.com + context: . + target: production + buildoptions: "-t docker-registry.linagora.com/tdrive/onlyoffice-connector" + tags: "latest" diff --git a/tdrive/connectors/onlyoffice-connector/.editorconfig b/tdrive/connectors/onlyoffice-connector/.editorconfig new file mode 100644 index 00000000..c6c8b362 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/tdrive/connectors/onlyoffice-connector/.eslintignore b/tdrive/connectors/onlyoffice-connector/.eslintignore new file mode 100644 index 00000000..f06235c4 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/.eslintignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/tdrive/connectors/onlyoffice-connector/.eslintrc b/tdrive/connectors/onlyoffice-connector/.eslintrc new file mode 100644 index 00000000..7d57747f --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/.eslintrc @@ -0,0 +1,33 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "prettier", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "env": { + "es6": true, + "node": true + }, + "rules": { + "no-var": "error", + "semi": "error", + "indent": ["error", 2, { "SwitchCase": 1 }], + "no-multi-spaces": "error", + "space-in-parens": "error", + "no-multiple-empty-lines": "error", + "prefer-const": "error", + "@typescript-eslint/explicit-member-accessibility": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/no-parameter-properties": 0, + "@typescript-eslint/interface-name-prefix": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-var-requires": "off" + } +} diff --git a/tdrive/connectors/onlyoffice-connector/.gitignore b/tdrive/connectors/onlyoffice-connector/.gitignore new file mode 100644 index 00000000..acf5ec7b --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/.gitignore @@ -0,0 +1,106 @@ +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +# .env +# .env.test +.env.development.local +.env* + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Package-lock json file +package-lock.json +yarn.lock \ No newline at end of file diff --git a/tdrive/connectors/onlyoffice-connector/.prettierrc b/tdrive/connectors/onlyoffice-connector/.prettierrc new file mode 100644 index 00000000..1f673681 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 150, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "arrowParens": "avoid" +} diff --git a/tdrive/connectors/onlyoffice-connector/.swcrc b/tdrive/connectors/onlyoffice-connector/.swcrc new file mode 100644 index 00000000..7b203885 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/.swcrc @@ -0,0 +1,36 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": false, + "dynamicImport": true, + "decorators": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "target": "es2017", + "externalHelpers": false, + "keepClassNames": true, + "loose": false, + "minify": { + "compress": false, + "mangle": false + }, + "baseUrl": "src", + "paths": { + "@/*": ["*"], + "@config": ["config"], + "@controllers/*": ["controllers/*"], + "@interfaces/*": ["interfaces/*"], + "@middlewares/*": ["middlewares/*"], + "@routes/*": ["routes/*"], + "@services/*": ["services/*"], + "@utils/*": ["utils/*"] + } + }, + "module": { + "type": "commonjs" + } +} diff --git a/tdrive/connectors/onlyoffice-connector/assets/empty.docx b/tdrive/connectors/onlyoffice-connector/assets/empty.docx new file mode 100644 index 0000000000000000000000000000000000000000..503a4fa444e8b8569c1f7114423654f62c1d2fbc GIT binary patch literal 10230 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 O`12k--(tz1{`-G9#yfie literal 0 HcmV?d00001 diff --git a/tdrive/connectors/onlyoffice-connector/assets/empty.pptx b/tdrive/connectors/onlyoffice-connector/assets/empty.pptx new file mode 100644 index 0000000000000000000000000000000000000000..c4885e2e92ee4d1e898517a1d0d27d1f927f7911 GIT binary patch literal 8704 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 P02WjO#I>K1-aY*vFOH#; literal 0 HcmV?d00001 diff --git a/tdrive/connectors/onlyoffice-connector/assets/empty.xlsx b/tdrive/connectors/onlyoffice-connector/assets/empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fb6d0eb95a34e989d0bc5bf89af589de00da6ea7 GIT binary patch literal 5933 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 FV1gtEwqpPQ literal 0 HcmV?d00001 diff --git a/tdrive/connectors/onlyoffice-connector/package.json b/tdrive/connectors/onlyoffice-connector/package.json new file mode 100644 index 00000000..beef014d --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/package.json @@ -0,0 +1,56 @@ +{ + "name": "twake-plugins-onlyoffice", + "version": "1.0.0", + "description": "twake onlyoffice and R7 connector", + "scripts": { + "dev": "concurrently \"npm run watch-compile\" \"npm run watch-dev\"", + "watch-compile": "swc src -w --out-dir dist --source-maps --copy-files", + "watch-dev": "nodemon --watch \"dist/**/*\" -e js ./dist/server.js", + "start": "cross-env NODE_ENV=production node dist/server.js", + "build": "swc src -d dist --source-maps --copy-files", + "build:tsc": "tsc && tsc-alias", + "lint": "eslint --ignore-path .gitignore --ext .ts src/", + "lint:fix": "npm run lint -- --fix" + }, + "keywords": [ + "onlyoffice", + "R7", + "twake" + ], + "author": "rezk2ll", + "license": "ISC", + "dependencies": { + "@types/jsonwebtoken": "^9.0.1", + "axios": "^1.1.3", + "concurrently": "^7.5.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "eta": "^1.12.3", + "express": "^4.18.2", + "form-data": "^4.0.0", + "jsonwebtoken": "^9.0.0", + "tslog": "^3.3.4" + }, + "devDependencies": { + "@swc/cli": "^0.1.57", + "@swc/core": "^1.2.220", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.14", + "@types/node": "^18.11.9", + "@typescript-eslint/eslint-plugin": "^5.42.0", + "@typescript-eslint/parser": "^5.42.0", + "config": "^3.3.8", + "cross-env": "^7.0.3", + "dotenv": "^16.0.3", + "eslint": "^8.26.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "node-config": "0.0.2", + "nodemon": "^2.0.19", + "prettier": "^2.7.1", + "ts-node": "^10.9.1", + "tsc-alias": "^1.7.0", + "tsconfig-paths": "^4.1.0", + "typescript": "^4.8.4" + } +} diff --git a/tdrive/connectors/onlyoffice-connector/src/app.ts b/tdrive/connectors/onlyoffice-connector/src/app.ts new file mode 100644 index 00000000..be49575a --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/app.ts @@ -0,0 +1,74 @@ +import express from 'express'; +import { Routes } from '@interfaces/routes.interface'; +import { NODE_ENV } from '@config'; +import cors from 'cors'; +import { renderFile } from 'eta'; +import path from 'path'; +import errorMiddleware from './middlewares/error.middleware'; +import { SERVER_PORT, SERVER_PREFIX } from '@config'; +import loggerService from './services/logger.service'; +import cookieParser from 'cookie-parser'; + +class App { + public app: express.Application; + public env: string; + public port: string | number; + + constructor(routes: Routes[]) { + this.app = express(); + this.env = NODE_ENV; + + this.initViews(); + this.initMiddlewares(); + this.initRoutes(routes); + this.initErrorHandling(); + } + + public listen = () => { + this.app.listen(SERVER_PORT, () => { + loggerService.info(`🚀 App listening on port ${SERVER_PORT}`); + }); + }; + + public getServer = () => this.app; + + private initRoutes = (routes: Routes[]) => { + this.app.use((req, res, next) => { + console.log('Requested: ', req.method, req.originalUrl); + next(); + }); + + routes.forEach(route => { + this.app.use(SERVER_PREFIX, route.router); + }); + + this.app.use( + SERVER_PREFIX.replace(/\/$/, '') + '/assets', + (req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'X-Requested-With'); + next(); + }, + express.static(path.join(__dirname, '../assets')), + ); + }; + + private initMiddlewares = () => { + this.app.use(cors()); + this.app.use(cookieParser()); + this.app.use(express.json()); + this.app.use(express.urlencoded({ extended: true })); + }; + + private initViews = () => { + this.app.engine('eta', renderFile); + this.app.set('view engine', 'eta'); + this.app.set('views', path.join(__dirname, 'views')); + }; + + private initErrorHandling = () => { + this.app.use(errorMiddleware); + }; +} + +export default App; diff --git a/tdrive/connectors/onlyoffice-connector/src/config/index.ts b/tdrive/connectors/onlyoffice-connector/src/config/index.ts new file mode 100644 index 00000000..ca0a3f5e --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/config/index.ts @@ -0,0 +1,14 @@ +import { config } from 'dotenv'; + +config({ path: `.env.${process.env.NODE_ENV || 'development'}.local` }); +export const { + NODE_ENV, + SERVER_PORT, + SECRET_KEY, + CREDENTIALS_ENDPOINT, + ONLY_OFFICE_SERVER, + CREDENTIALS_ID, + CREDENTIALS_SECRET, + SERVER_PREFIX, + SERVER_ORIGIN, +} = process.env; diff --git a/tdrive/connectors/onlyoffice-connector/src/controllers/index.controller.ts b/tdrive/connectors/onlyoffice-connector/src/controllers/index.controller.ts new file mode 100644 index 00000000..eaf5fb2d --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/controllers/index.controller.ts @@ -0,0 +1,126 @@ +import editorService from '@/services/editor.service'; +import { NextFunction, Request, Response } from 'express'; +import { CREDENTIALS_SECRET, SERVER_ORIGIN, SERVER_PREFIX } from '@config'; +import jwt from 'jsonwebtoken'; +import driveService from '@/services/drive.service'; +import { DriveFileType } from '@/interfaces/drive.interface'; +import fileService from '@/services/file.service'; +import { OfficeToken } from '@/interfaces/routes.interface'; +import loggerService from '@/services/logger.service'; + +interface RequestQuery { + mode: string; + company_id: string; + preview: string; + token: string; + file_id: string; + drive_file_id?: string; +} + +interface RequestEditorQuery { + office_token: string; + company_id: string; + file_id: string; +} + +class IndexController { + public index = async (req: Request<{}, {}, {}, RequestQuery>, res: Response, next: NextFunction): Promise => { + try { + const { file_id, drive_file_id, company_id, preview, token } = req.query; + const { user } = req; + + let driveFile: DriveFileType; + if (drive_file_id) { + //Append information about the drive file (versions, location, etc) + driveFile = await driveService.get({ + drive_file_id, + company_id, + user_token: token, + }); + + if (!driveFile) { + throw new Error('Drive file not found'); + } + } + + //Get the file itself + const file = await fileService.get({ + file_id: driveFile?.item?.last_version_cache?.file_metadata?.external_id || file_id, + company_id, + }); + + if (!file) { + throw new Error('File not found'); + } + + //Check whether the user has access to the file and put information to the office_token + const hasAccess = + (!driveFile && (file.user_id === user.id || preview)) || + ['manage', 'write'].includes(driveFile?.access) || + (driveFile?.access === 'read' && preview); + + if (!hasAccess) { + throw new Error('You do not have access to this file'); + } + + const officeToken = jwt.sign( + { + user_id: user.id, //To verify that link is opened by the same user + company_id, + drive_file_id, + file_id: file.id, + file_name: file.filename || file?.metadata?.name || '', + preview: !!preview, + } as OfficeToken, + CREDENTIALS_SECRET, + { + expiresIn: 60 * 60 * 24 * 30, + }, + ); + + res.redirect( + `${SERVER_ORIGIN ?? ''}/${SERVER_PREFIX.replace( + /(\/+$|^\/+)/gm, + '', + )}/editor?office_token=${officeToken}&token=${token}&file_id=${file_id}&company_id=${company_id}&preview=${preview}`, + ); + } catch (error) { + next(error); + } + }; + + public editor = async (req: Request<{}, {}, {}, RequestEditorQuery>, res: Response, next: NextFunction): Promise => { + try { + const { office_token } = req.query; + const { user } = req; + + const officeTokenPayload = jwt.verify(office_token, CREDENTIALS_SECRET) as OfficeToken; + const { preview, user_id, company_id, file_name, file_id, drive_file_id } = officeTokenPayload; + + if (user_id !== user.id) { + throw new Error('You do not have access to this link'); + } + + const initResponse = await editorService.init(company_id, file_name, file_id, user, preview, drive_file_id || file_id); + + const inPageToken = jwt.sign( + { + ...officeTokenPayload, + in_page_token: true, + } as OfficeToken, + CREDENTIALS_SECRET, + ); + + res.render('index', { + ...initResponse, + server: SERVER_ORIGIN.replace(/\/+$/, '') + '/' + SERVER_PREFIX.replace(/(\/+$|^\/+)/, '') || `${req.protocol}://${req.get('host')}/`, + token: inPageToken, + }); + } catch (error) { + loggerService.error(error); + next(error); + } + }; +} + +export default IndexController; diff --git a/tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts b/tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts new file mode 100644 index 00000000..57fa6512 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts @@ -0,0 +1,128 @@ +import { CREDENTIALS_SECRET } from '@/config'; +import { OfficeToken } from '@/interfaces/routes.interface'; +import apiService from '@/services/api.service'; +import driveService from '@/services/drive.service'; +import fileService from '@/services/file.service'; +import loggerService from '@/services/logger.service'; +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +interface RequestQuery { + company_id: string; + file_id: string; + token: string; +} + +interface SaveRequestBody { + filetype: string; + key: string; + status: number; + url: string; + users: string[]; +} + +class OnlyOfficeController { + public read = async (req: Request<{}, {}, {}, RequestQuery>, res: Response, next: NextFunction): Promise => { + try { + const { token } = req.query; + + const officeTokenPayload = jwt.verify(token, CREDENTIALS_SECRET) as OfficeToken; + const { company_id, drive_file_id, file_id, in_page_token } = officeTokenPayload; + let fileId = file_id; + + // check token is an in_page_token + if (!in_page_token) throw new Error('Invalid token, must be a in_page_token'); + + if (drive_file_id) { + //Get the drive file + const driveFile = await driveService.get({ + company_id, + drive_file_id, + }); + if (driveFile) { + fileId = driveFile?.item?.last_version_cache?.file_metadata?.external_id; + } + } + + const file = await fileService.download({ + company_id, + file_id: fileId, + }); + + file.pipe(res); + } catch (error) { + next(error); + } + }; + + public save = async (req: Request<{}, {}, SaveRequestBody, RequestQuery>, res: Response, next: NextFunction): Promise => { + try { + const { url, key } = req.body; + const { token } = req.query; + loggerService.info('Save request', { key, url, token }); + + const officeTokenPayload = jwt.verify(token, CREDENTIALS_SECRET) as OfficeToken; + const { preview, company_id, file_id, user_id, drive_file_id, in_page_token } = officeTokenPayload; + + // check token is an in_page_token and allow save + if (!in_page_token) throw new Error('Invalid token, must be a in_page_token'); + if (preview) throw new Error('Invalid token, must not be a preview token for save operation'); + + if (url) { + loggerService.info('URL present, saving file'); + // If token indicate a drive_file_id then check if we want to create a new version or not + if (drive_file_id) { + loggerService.info('Drive file id present, checking if we need to create a new version'); + //Get the drive file + const driveFile = await driveService.get({ + company_id, + drive_file_id, + }); + + // const createNewVersion = !!driveFile; //Always create a new version because needed by OnlyOffice // driveFile.item.last_version_cache.date_added < Date.now() - 1000 * 60 * 60 * 3; + const createNewVersion = true; + if (createNewVersion) { + const newVersionFile = await fileService.save({ + company_id, + file_id, + url, + create_new: true, + }); + + await driveService.createVersion({ + company_id, + drive_file_id, + file_id: newVersionFile?.resource?.id, + }); + loggerService.info('New version created'); + + res.send({ + error: 0, + }); + + return; + } + } + loggerService.info('Saving file'); + + await fileService.save({ + company_id, + file_id, + url, + user_id: user_id, + }); + } else { + loggerService.error('URL not present, force saving file'); + await apiService.runCommand('forcesave', key); + } + + res.send({ + error: 0, + }); + } catch (error) { + next(error); + } + }; +} + +export default OnlyOfficeController; diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/api.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/api.interface.ts new file mode 100644 index 00000000..06d4ff16 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/api.interface.ts @@ -0,0 +1,31 @@ +import { ResponseType } from 'axios'; + +export interface IApiServiceRequestParams { + url: string; + payload?: T; + token?: string; + responseType?: ResponseType; + headers?: any; +} + +export interface IApiService { + get(params: IApiServiceRequestParams): Promise; + post(params: IApiServiceRequestParams): Promise; + runCommand(c: string, key: string): Promise; +} + +export interface IApiServiceApplicationTokenRequestParams { + id: string; + secret: string; +} + +export interface IApiServiceApplicationTokenResponse { + resource: { + access_token: { + time: number; + expiration: number; + value: string; + type: string; + }; + }; +} diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts new file mode 100644 index 00000000..9d274d66 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/drive.interface.ts @@ -0,0 +1,22 @@ +export type DriveFileType = { + access: 'manage' | 'write' | 'read' | 'none'; + item: { + last_version_cache: { + id: string; + date_added: number; + file_metadata: { + external_id: string; + }; + }; + }; +}; + +export type DriveRequestParams = { + drive_file_id: string; + company_id: string; +}; + +export interface IDriveService { + get: (params: DriveRequestParams) => Promise; + createVersion: (params: { company_id: string; drive_file_id: string; file_id: string }) => Promise; +} diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts new file mode 100644 index 00000000..7aef5de3 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts @@ -0,0 +1,42 @@ +import { UserType } from './user.interface'; + +export type EditorInitRequestParams = { + file_id: string; + company_id: string; + token: string; + user: UserType; + preview: string; +}; + +export type EditConfigInitResult = { + user_id: string; + username: string; + language: string; + user_image: string; + mode: string; + onlyoffice_server: string; + color: string; + company_id: string; + file_id: string; + file_version_id: string; + filename: string; + file_type: string; + editable: boolean; + preview: boolean; +}; + +export interface IEditorService { + init: ( + company_id: string, + file_name: string, + file_version_id: string, + user: UserType, + preview: boolean, + file_id: string, + ) => Promise; +} + +export type ModeParametersType = { + mode: string; + color: string; +}; diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/file.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/file.interface.ts new file mode 100644 index 00000000..39cb66e6 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/file.interface.ts @@ -0,0 +1,23 @@ +export type FileType = { + id: string; + filename: string; + company_id: string; + user_id: string; + metadata: { + name: string; + mime: string; + }; +}; + +export type FileRequestParams = { + file_id: string; + company_id: string; + url?: string; + user_id?: string; + create_new?: boolean; +}; + +export interface IFileService { + get: (params: FileRequestParams) => Promise; + download: (params: FileRequestParams) => Promise; +} diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/routes.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/routes.interface.ts new file mode 100644 index 00000000..7a029546 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/routes.interface.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; + +export interface Routes { + path?: string; + router: Router; +} + +export interface OfficeToken { + user_id: string; + company_id: string; + file_id: string; + file_name: string; + preview: boolean; + drive_file_id?: string; + in_page_token?: boolean; +} diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/user.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/user.interface.ts new file mode 100644 index 00000000..2c0336a3 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/user.interface.ts @@ -0,0 +1,14 @@ +export type UserType = { + id: string; + username: string; + thumbnail: string; + picture: string; + email: string; + preferences: { + locale: string; + }; +}; + +export interface IuserService { + getCurrentUser: (token: string) => Promise; +} diff --git a/tdrive/connectors/onlyoffice-connector/src/middlewares/auth.middleware.ts b/tdrive/connectors/onlyoffice-connector/src/middlewares/auth.middleware.ts new file mode 100644 index 00000000..73eb6222 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/middlewares/auth.middleware.ts @@ -0,0 +1,70 @@ +import { CREDENTIALS_SECRET } from '@/config'; +import loggerService from '@/services/logger.service'; +import userService from '@/services/user.service'; +import { NextFunction, Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; + +interface RequestQuery { + mode: string; + company_id: string; + preview: string; + token: string; + file_id: string; +} + +export default async (req: Request<{}, {}, {}, RequestQuery>, res: Response, next: NextFunction) => { + try { + const token = req.query?.token; + + if (!token && req.cookies.token) { + try { + const userJwt = jwt.verify(req.cookies.token, CREDENTIALS_SECRET) as any; + if (userJwt.id) { + req.user = userJwt; + next(); + return; + } + } catch (error) { + loggerService.error('invalid token', error); + res.clearCookie('token'); + } + } + + if (!token) { + return res.status(401).send({ message: 'invalid token' }); + } + + const user = await userService.getCurrentUser(token); + + if (!user || !user.id) { + return res.status(401).json({ message: 'invalid token' }); + } + + res.cookie( + 'token', + jwt.sign( + { + id: user.id, + username: user.username, + email: user.email, + thumbnail: user.thumbnail, + picture: user.picture, + preferences: { + locale: user?.preferences?.locale, + }, + }, + CREDENTIALS_SECRET, + { + expiresIn: 60 * 60 * 24 * 30, + }, + ), + { maxAge: 60 * 60 * 24 * 30 }, + ); + + req.user = user; + next(); + } catch (error) { + console.error(error); + return res.status(500).send({ message: 'invalid token' }); + } +}; diff --git a/tdrive/connectors/onlyoffice-connector/src/middlewares/error.middleware.ts b/tdrive/connectors/onlyoffice-connector/src/middlewares/error.middleware.ts new file mode 100644 index 00000000..e72ba3d2 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/middlewares/error.middleware.ts @@ -0,0 +1,15 @@ +import loggerService from '@/services/logger.service'; +import { NextFunction, Request, Response } from 'express'; + +export default (error: Error & { status?: number }, req: Request, res: Response, next: NextFunction): void => { + try { + const status: number = error.status || 500; + const message: string = error.message || 'something went wrong'; + + loggerService.error(`[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`); + + res.status(status).json({ message }); + } catch (error) { + next(error); + } +}; diff --git a/tdrive/connectors/onlyoffice-connector/src/middlewares/requirements.middleware.ts b/tdrive/connectors/onlyoffice-connector/src/middlewares/requirements.middleware.ts new file mode 100644 index 00000000..9fd90c0d --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/middlewares/requirements.middleware.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from 'express'; + +interface RequestQuery { + company_id: string; + token: string; +} + +export default (req: Request<{}, {}, {}, RequestQuery>, res: Response, next: NextFunction) => { + const { company_id } = req.query; + + if (!company_id) { + return res.status(400).json({ message: 'invalid request' }); + } + + next(); +}; diff --git a/tdrive/connectors/onlyoffice-connector/src/routes/index.route.ts b/tdrive/connectors/onlyoffice-connector/src/routes/index.route.ts new file mode 100644 index 00000000..e92a14ca --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/routes/index.route.ts @@ -0,0 +1,23 @@ +import IndexController from '@/controllers/index.controller'; +import { Routes } from '@/interfaces/routes.interface'; +import authMiddleware from '@/middlewares/auth.middleware'; +import requirementsMiddleware from '@/middlewares/requirements.middleware'; +import { Router } from 'express'; + +class IndexRoute implements Routes { + public path = '/'; + public router = Router(); + public indexController: IndexController; + + constructor() { + this.indexController = new IndexController(); + this.initRoutes(); + } + + private initRoutes = () => { + this.router.get(this.path, requirementsMiddleware, authMiddleware, this.indexController.index); + this.router.get(this.path + 'editor', requirementsMiddleware, authMiddleware, this.indexController.editor); + }; +} + +export default IndexRoute; diff --git a/tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts b/tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts new file mode 100644 index 00000000..33c8a9b6 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts @@ -0,0 +1,22 @@ +import OnlyOfficeController from '@/controllers/onlyoffice.controller'; +import { Routes } from '@/interfaces/routes.interface'; +import requirementsMiddleware from '@/middlewares/requirements.middleware'; +import { Router } from 'express'; + +class OnlyOfficeRoute implements Routes { + public path = '/'; + public router = Router(); + public onlyOfficeController: OnlyOfficeController; + + constructor() { + this.onlyOfficeController = new OnlyOfficeController(); + this.initRoutes(); + } + + private initRoutes = () => { + this.router.get(`${this.path}:mode/read`, requirementsMiddleware, this.onlyOfficeController.read); + this.router.post(`${this.path}:mode/save`, requirementsMiddleware, this.onlyOfficeController.save); + }; +} + +export default OnlyOfficeRoute; diff --git a/tdrive/connectors/onlyoffice-connector/src/server.ts b/tdrive/connectors/onlyoffice-connector/src/server.ts new file mode 100644 index 00000000..139be10f --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/server.ts @@ -0,0 +1,7 @@ +import App from '@/app'; +import IndexRoute from './routes/index.route'; +import OnlyOfficeRoute from './routes/onlyoffice.route'; + +const app = new App([new IndexRoute(), new OnlyOfficeRoute()]); + +app.listen(); diff --git a/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts new file mode 100644 index 00000000..63eb8ddf --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts @@ -0,0 +1,152 @@ +import { + IApiServiceRequestParams, + IApiService, + IApiServiceApplicationTokenRequestParams, + IApiServiceApplicationTokenResponse, +} from '@/interfaces/api.interface'; +import axios, { Axios, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { CREDENTIALS_ENDPOINT, CREDENTIALS_ID, CREDENTIALS_SECRET, ONLY_OFFICE_SERVER } from '@config'; +import loggerService from './logger.service'; +class ApiService implements IApiService { + private axios: Axios; + private initialized: Promise; + + constructor() { + this.initialized = this.refreshToken(); + this.initialized.catch(error => { + loggerService.error('failed to init API', error); + }); + + setInterval(() => { + this.initialized = this.refreshToken(); + loggerService.info('Refreshing token 🪙'); + }, 1000 * 60); //Every 10 minutes + } + + public get = async (params: IApiServiceRequestParams): Promise => { + const { url, token, responseType, headers } = params; + + await this.initialized; + + const config: AxiosRequestConfig = {}; + + if (token) { + config['headers'] = { + Authorization: `Bearer ${token}`, + ...headers, + }; + } + + if (responseType) { + config['responseType'] = responseType; + } + + return await this.axios.get(url, config); + }; + + public post = async (params: IApiServiceRequestParams): Promise => { + const { url, payload, headers } = params; + + await this.initialized; + try { + return await this.axios.post(url, payload, { + headers: { + ...headers, + }, + }); + } catch (error) { + loggerService.error('Failed to post: ', error.message); + this.refreshToken(); + } + }; + + private handleErrors = (error: any): Promise => { + loggerService.error('Failed Request', error.message); + + return Promise.reject(error); + }; + + private handleResponse = ({ data }: AxiosResponse): T => data; + + private refreshToken = async (): Promise => { + try { + const response = await axios.post( + `${CREDENTIALS_ENDPOINT.replace(/\/$/, '')}/api/console/v1/login`, + { + id: CREDENTIALS_ID, + secret: CREDENTIALS_SECRET, + }, + { + headers: { + Authorization: `Basic ${Buffer.from(`${CREDENTIALS_ID}:${CREDENTIALS_SECRET}`).toString('base64')}`, + }, + }, + ); + + const { + resource: { + access_token: { value }, + }, + } = response.data; + + this.axios = axios.create({ + baseURL: CREDENTIALS_ENDPOINT, + headers: { + Authorization: `Bearer ${value}`, + }, + }); + + this.axios.interceptors.response.use(this.handleResponse, this.handleErrors); + + return value; + } catch (error) { + loggerService.error('failed to get application token', error.message); + loggerService.info('Using token ', CREDENTIALS_ID, CREDENTIALS_SECRET); + loggerService.info(`POST ${CREDENTIALS_ENDPOINT.replace(/\/$/, '')}/api/console/v1/login`); + loggerService.info(`Basic ${Buffer.from(`${CREDENTIALS_ID}:${CREDENTIALS_SECRET}`).toString('base64')}`); + throw Error(error); + } + }; + + public runCommand = async (c: string, key: string): Promise => { + try { + loggerService.info('SENDING COMMAND TO: ', `${ONLY_OFFICE_SERVER}coauthoring/CommandService.ashx`); + const response = await axios.post(`${ONLY_OFFICE_SERVER}coauthoring/CommandService.ashx`, { + c, + key, + userdata: '', + }); + const { data } = response; + switch (data.error) { + case 0: + loggerService.info('File saved successfully'); + break; + case 1: + loggerService.error('Document key is missing or no document with such key could be found.'); + throw new Error('Document key is missing or no document with such key could be found.'); + case 2: + loggerService.error('Callback url not correct.'); + throw new Error('Callback url not correct.'); + case 3: + loggerService.error('Internal server error.'); + throw new Error('Internal server error.'); + case 4: + loggerService.error('No changes were applied to the document before the forcesave command was received.'); + throw new Error('No changes were applied to the document before the forcesave command was received.'); + case 5: + loggerService.error('Command not correct.'); + throw new Error('Command not correct.'); + case 6: + loggerService.error('Invalid token.'); + throw new Error('Invalid token.'); + default: + loggerService.error('Unknown error occurred.'); + throw new Error('Unknown error occurred.'); + } + } catch (error) { + loggerService.error(`Error executing command: ${c}, ${error}`); + } + }; +} + +export default new ApiService(); diff --git a/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts new file mode 100644 index 00000000..5b8fc15c --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts @@ -0,0 +1,49 @@ +import { DriveFileType, IDriveService } from '@/interfaces/drive.interface'; +import apiService from './api.service'; +import loggerService from './logger.service'; + +class DriveService implements IDriveService { + public get = async (params: { company_id: string; drive_file_id: string; user_token?: string }): Promise => { + try { + const { company_id, drive_file_id } = params; + const resource = await apiService.get({ + url: `/internal/services/documents/v1/companies/${company_id}/item/${drive_file_id}`, + token: params.user_token, + }); + + return resource; + } catch (error) { + loggerService.error('Failed to fetch file metadata: ', error.message); + + return Promise.reject(); + } + }; + + public createVersion = async (params: { + company_id: string; + drive_file_id: string; + file_id: string; + }): Promise => { + try { + const { company_id, drive_file_id, file_id } = params; + const resource = await apiService.post<{}, DriveFileType['item']['last_version_cache']>({ + url: `/internal/services/documents/v1/companies/${company_id}/item/${drive_file_id}/version`, + payload: { + drive_item_id: drive_file_id, + provider: 'internal', + file_metadata: { + external_id: file_id, + source: 'internal', + }, + }, + }); + + return resource; + } catch (error) { + loggerService.error('Failed to create version: ', error.message); + return Promise.reject(); + } + }; +} + +export default new DriveService(); diff --git a/tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts new file mode 100644 index 00000000..1dc5c29f --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts @@ -0,0 +1,91 @@ +import { EditConfigInitResult, IEditorService, ModeParametersType } from '@/interfaces/editor.interface'; +import { UserType } from '@/interfaces/user.interface'; +import { ONLY_OFFICE_SERVER } from '@config'; + +class EditorService implements IEditorService { + public init = async ( + company_id: string, + file_name: string, + file_version_id: string, + user: UserType, + preview: boolean, + file_id: string, + ): Promise => { + const { color, mode: fileMode } = this.getFileMode(file_name); + + return { + color, + file_id, + file_version_id, + file_type: file_name.split('.').pop(), + filename: file_name, + language: user.preferences.locale || 'en', + mode: fileMode, + onlyoffice_server: ONLY_OFFICE_SERVER, + user_id: user.id, + user_image: user.thumbnail || user.picture || '', + username: user.username, + company_id, + preview, + editable: !preview, + }; + }; + + private getFileMode = (filename: string): ModeParametersType => { + const extension = filename.split('.').pop(); + + if ( + [ + 'doc', + 'docm', + 'docx', + 'docxf', + 'dot', + 'dotm', + 'dotx', + 'epub', + 'fodt', + 'fb2', + 'htm', + 'html', + 'mht', + 'odt', + 'oform', + 'ott', + 'oxps', + 'pdf', + 'rtf', + 'txt', + 'djvu', + 'xml', + 'xps', + ].includes(extension) + ) { + return { + mode: 'word', + color: '#aa5252', + }; + } + + if (['csv', 'fods', 'ods', 'ots', 'xls', 'xlsb', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx'].includes(extension)) { + return { + mode: 'cell', + color: '#40865c', + }; + } + + if (['fodp', 'odp', 'otp', 'pot', 'potm', 'potx', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx'].includes(extension)) { + return { + mode: 'slide', + color: '#aa5252', + }; + } + + return { + mode: 'text', + color: 'grey', + }; + }; +} + +export default new EditorService(); diff --git a/tdrive/connectors/onlyoffice-connector/src/services/file.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/file.service.ts new file mode 100644 index 00000000..72fb4769 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/services/file.service.ts @@ -0,0 +1,82 @@ +import { FileRequestParams, FileType, IFileService } from '@/interfaces/file.interface'; +import apiService from './api.service'; +import loggerService from './logger.service'; +import { Stream } from 'stream'; +import FormData from 'form-data'; + +class FileService implements IFileService { + public get = async (params: FileRequestParams): Promise => { + try { + const { company_id, file_id } = params; + const { resource } = await apiService.get<{ resource: FileType }>({ + url: `/internal/services/files/v1/companies/${company_id}/files/${file_id}`, + }); + + return resource; + } catch (error) { + loggerService.error('Failed to fetch file metadata: ', error.message); + + return Promise.reject(); + } + }; + + public download = async (params: FileRequestParams): Promise => { + try { + const { company_id, file_id } = params; + const file = await apiService.get({ + url: `/internal/services/files/v1/companies/${company_id}/files/${file_id}/download`, + responseType: 'stream', + }); + + return file; + } catch (error) { + loggerService.error('Failed to download file: ', error.message); + } + }; + + public save = async (params: FileRequestParams): Promise<{ resource: FileType }> => { + try { + const { company_id, file_id, url, create_new } = params; + + if (!url) { + throw Error('no url found'); + } + + const originalFile = await this.get(params); + + if (!originalFile) { + throw Error('original file not found'); + } + + const newFile = await apiService.get({ + url, + responseType: 'stream', + }); + + const form = new FormData(); + + const nameSplit = (originalFile.metadata.name || '').split('.'); + const filename = + nameSplit[0].replace(/-[0-9]{8}-[0-9]{4}$/, '') + + (!create_new ? '.' : `-${new Date().toISOString().split('.')[0].split(':').slice(0, 2).join('').replace(/-/gm, '').split('T').join('-')}.`) + + nameSplit.slice(1).join('.'); + form.append('file', newFile, { + filename, + }); + + loggerService.info('Saving file version: ', filename); + + return await apiService.post({ + url: create_new + ? `/internal/services/files/v1/companies/${company_id}/files?thumbnail_sync=1` + : `/internal/services/files/v1/companies/${company_id}/files/${file_id}?thumbnail_sync=1`, + payload: form, + headers: form.getHeaders(), + }); + } catch (error) { + loggerService.error('Failed to save file: ', error.message); + } + }; +} + +export default new FileService(); diff --git a/tdrive/connectors/onlyoffice-connector/src/services/logger.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/logger.service.ts new file mode 100644 index 00000000..5bb9d148 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/services/logger.service.ts @@ -0,0 +1,5 @@ +import { Logger } from 'tslog'; + +export default new Logger({ + name: 'twake-onlyoffice-plugin', +}); diff --git a/tdrive/connectors/onlyoffice-connector/src/services/user.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/user.service.ts new file mode 100644 index 00000000..b9de6ce9 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/services/user.service.ts @@ -0,0 +1,22 @@ +import { IuserService, UserType } from '@/interfaces/user.interface'; +import apiService from './api.service'; +import loggerService from './logger.service'; + +class UserService implements IuserService { + public getCurrentUser = async (token: string): Promise => { + try { + const { resource } = await apiService.get<{ resource: UserType }>({ + url: '/internal/services/users/v1/users/me', + token, + }); + + return resource; + } catch (error) { + loggerService.error('Failed to fetch the current user', error.message); + + return null; + } + }; +} + +export default new UserService(); diff --git a/tdrive/connectors/onlyoffice-connector/src/types/express.d.ts b/tdrive/connectors/onlyoffice-connector/src/types/express.d.ts new file mode 100644 index 00000000..68f58470 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/types/express.d.ts @@ -0,0 +1,9 @@ +import { UserType } from '@/interfaces/user.interface'; + +declare global { + namespace Express { + interface Request { + user?: UserType; + } + } +} diff --git a/tdrive/connectors/onlyoffice-connector/src/views/index.eta b/tdrive/connectors/onlyoffice-connector/src/views/index.eta new file mode 100644 index 00000000..f4f6c30f --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/views/index.eta @@ -0,0 +1,92 @@ + + + + + + + + + + +
+ + + + diff --git a/tdrive/connectors/onlyoffice-connector/tsconfig.json b/tdrive/connectors/onlyoffice-connector/tsconfig.json new file mode 100644 index 00000000..0752229e --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "target": "es2017", + "lib": ["es2017", "esnext.asynciterable"], + "typeRoots": ["node_modules/@types"], + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "module": "commonjs", + "pretty": true, + "sourceMap": true, + "declaration": true, + "outDir": "dist", + "allowJs": true, + "noEmit": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "importHelpers": true, + "baseUrl": "src", + "paths": { + "@/*": ["*"], + "@config": ["config"], + "@controllers/*": ["controllers/*"], + "@interfaces/*": ["interfaces/*"], + "@middlewares/*": ["middlewares/*"], + "@routes/*": ["routes/*"], + "@services/*": ["services/*"], + "@utils/*": ["utils/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.json", ".env"], + "exclude": ["node_modules", "src/http", "src/logs", "src/tests"] +} diff --git a/tdrive/default-configuration/onlyoffice/local.json b/tdrive/default-configuration/onlyoffice/local.json new file mode 100644 index 00000000..4417d67d --- /dev/null +++ b/tdrive/default-configuration/onlyoffice/local.json @@ -0,0 +1,57 @@ +{ + "services": { + "CoAuthoring": { + "request-filtering-agent": { + "allowPrivateIPAddress": true, + "allowMetaIPAddress": true + }, + "sql": { + "type": "postgres", + "dbHost": "onlyoffice-postgresql", + "dbPort": "5432", + "dbName": "onlyoffice", + "dbUser": "onlyoffice", + "dbPass": "onlyoffice" + }, + "token": { + "enable": { + "request": { + "inbox": false, + "outbox": false + }, + "browser": false + }, + "inbox": { + "header": "Authorization", + "inBody": false + }, + "outbox": { + "header": "Authorization", + "inBody": false + } + }, + "secret": { + "inbox": { + "string": "TKBsR5SQaA2il4kvaf6x4IP9A0k1rWi4" + }, + "outbox": { + "string": "TKBsR5SQaA2il4kvaf6x4IP9A0k1rWi4" + }, + "session": { + "string": "TKBsR5SQaA2il4kvaf6x4IP9A0k1rWi4" + } + } + } + }, + "rabbitmq": { + "url": "amqp://guest:guest@onlyoffice-rabbitmq" + }, + "queue": { + "type": "rabbitmq" + }, + "storage": { + "fs": { + "secretString": "hDcqEmLCiJ5nWEK1lt2b" + } + } +} diff --git a/tdrive/docker-compose.dev.mongo.yml b/tdrive/docker-compose.dev.mongo.yml index 4307c3f8..6bf92ae3 100644 --- a/tdrive/docker-compose.dev.mongo.yml +++ b/tdrive/docker-compose.dev.mongo.yml @@ -62,10 +62,12 @@ services: - tdrive_network onlyoffice-connector: - image: onlyoffice-connector + build: + context: . + dockerfile: docker/onlyoffice-connector/Dockerfile environment: - CREDENTIALS_ENDPOINT=http://tdrive_node:4000 - - ONLY_OFFICE_SERVER=http://localhost:8090/ + - ONLY_OFFICE_SERVER=http://onlyoffice:8090/ - SERVER_ORIGIN=http://onlyoffice-connector:5000 - SERVER_PORT=5000 - SERVER_PREFIX=/plugins/onlyoffice @@ -110,7 +112,6 @@ services: onlyoffice: image: docker.io/onlyoffice/documentserver - hostname: onlyoffice ports: - 8090:80 networks: diff --git a/tdrive/docker/onlyoffice-connector/Dockerfile b/tdrive/docker/onlyoffice-connector/Dockerfile new file mode 100644 index 00000000..3294b4a4 --- /dev/null +++ b/tdrive/docker/onlyoffice-connector/Dockerfile @@ -0,0 +1,11 @@ +FROM node:16 + +WORKDIR /usr/src/app + +COPY connectors/onlyoffice-connector/package*.json ./ +RUN npm install + +COPY connectors/onlyoffice-connector/ . +RUN npm run build + +CMD [ "npm", "start" ] \ No newline at end of file