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 00000000..503a4fa4 Binary files /dev/null and b/tdrive/connectors/onlyoffice-connector/assets/empty.docx differ diff --git a/tdrive/connectors/onlyoffice-connector/assets/empty.pptx b/tdrive/connectors/onlyoffice-connector/assets/empty.pptx new file mode 100644 index 00000000..c4885e2e Binary files /dev/null and b/tdrive/connectors/onlyoffice-connector/assets/empty.pptx differ diff --git a/tdrive/connectors/onlyoffice-connector/assets/empty.xlsx b/tdrive/connectors/onlyoffice-connector/assets/empty.xlsx new file mode 100644 index 00000000..fb6d0eb9 Binary files /dev/null and b/tdrive/connectors/onlyoffice-connector/assets/empty.xlsx differ diff --git a/tdrive/connectors/onlyoffice-connector/assets/logo.png b/tdrive/connectors/onlyoffice-connector/assets/logo.png new file mode 100644 index 00000000..b4033eac Binary files /dev/null and b/tdrive/connectors/onlyoffice-connector/assets/logo.png differ 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