🖥️ Onlyoffice connector moved into the TwakeDrive repository (#366)
This commit is contained in:
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 150,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"semi": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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;
|
||||||
@@ -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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ResponseType } from 'axios';
|
||||||
|
|
||||||
|
export interface IApiServiceRequestParams<T> {
|
||||||
|
url: string;
|
||||||
|
payload?: T;
|
||||||
|
token?: string;
|
||||||
|
responseType?: ResponseType;
|
||||||
|
headers?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApiService {
|
||||||
|
get<T>(params: IApiServiceRequestParams<T>): Promise<T>;
|
||||||
|
post<T>(params: IApiServiceRequestParams<T>): Promise<T>;
|
||||||
|
runCommand<T>(c: string, key: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApiServiceApplicationTokenRequestParams {
|
||||||
|
id: string;
|
||||||
|
secret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApiServiceApplicationTokenResponse {
|
||||||
|
resource: {
|
||||||
|
access_token: {
|
||||||
|
time: number;
|
||||||
|
expiration: number;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<DriveFileType>;
|
||||||
|
createVersion: (params: { company_id: string; drive_file_id: string; file_id: string }) => Promise<DriveFileType['item']['last_version_cache']>;
|
||||||
|
}
|
||||||
@@ -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<EditConfigInitResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModeParametersType = {
|
||||||
|
mode: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
@@ -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<FileType>;
|
||||||
|
download: (params: FileRequestParams) => Promise<any>;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<UserType>;
|
||||||
|
}
|
||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
@@ -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<string>;
|
||||||
|
|
||||||
|
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 <T>(params: IApiServiceRequestParams<T>): Promise<T> => {
|
||||||
|
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 <T, R>(params: IApiServiceRequestParams<T>): Promise<R> => {
|
||||||
|
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<any> => {
|
||||||
|
loggerService.error('Failed Request', error.message);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleResponse = <T>({ data }: AxiosResponse): T => data;
|
||||||
|
|
||||||
|
private refreshToken = async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<IApiServiceApplicationTokenRequestParams, { data: IApiServiceApplicationTokenResponse }>(
|
||||||
|
`${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<void> => {
|
||||||
|
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();
|
||||||
@@ -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<DriveFileType> => {
|
||||||
|
try {
|
||||||
|
const { company_id, drive_file_id } = params;
|
||||||
|
const resource = await apiService.get<DriveFileType>({
|
||||||
|
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<DriveFileType['item']['last_version_cache']> => {
|
||||||
|
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();
|
||||||
@@ -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<EditConfigInitResult> => {
|
||||||
|
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();
|
||||||
@@ -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<FileType> => {
|
||||||
|
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<any> => {
|
||||||
|
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<Stream>({
|
||||||
|
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<any, { resource: FileType }>({
|
||||||
|
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();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Logger } from 'tslog';
|
||||||
|
|
||||||
|
export default new Logger({
|
||||||
|
name: 'twake-onlyoffice-plugin',
|
||||||
|
});
|
||||||
@@ -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<UserType> => {
|
||||||
|
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();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { UserType } from '@/interfaces/user.interface';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: UserType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="<%= it.onlyoffice_server %>web-apps/apps/api/documents/api.js"></script>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no,shrink-to-fit=no" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="height: 100%">
|
||||||
|
<div id="onlyoffice_container" style="top: 0; left: 0; width: 100%; height: 100%; position: absolute; color: #fff; text-align: center; background-color: <%= it.color %>"></div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
window.user = {
|
||||||
|
id: "<%= it.user_id %>",
|
||||||
|
username: "<%= it.username %>",
|
||||||
|
language: "<%= it.language %>",
|
||||||
|
userimage: "<%= it.user_image %>"
|
||||||
|
};
|
||||||
|
window.mode = "<%= it.mode %>"
|
||||||
|
window.baseURL = `<%= it.server %>/${window.mode}/`;
|
||||||
|
|
||||||
|
$('#onlyoffice_container').html("<div id='onlyoffice_container_instance'></div>");
|
||||||
|
|
||||||
|
let doc = {
|
||||||
|
title: "<%= it.filename %>",
|
||||||
|
url: `${window.baseURL}read?file_id=<%= it.file_id %>&company_id=<%= it.company_id %>&token=<%= it.token %>`,
|
||||||
|
fileType: "<%= it.file_type %>",
|
||||||
|
key: "<%= it.file_version_id %>",
|
||||||
|
token: "<%= it.file_id %>",
|
||||||
|
permissions: {
|
||||||
|
download: true,
|
||||||
|
edit: <%= it.editable %>,
|
||||||
|
preview: <%= it.preview %>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const documentChangeHandler = function (event) {
|
||||||
|
$.ajax({
|
||||||
|
url: `${window.baseURL}save?file_id=<%= it.file_id %>&company_id=<%= it.company_id %>&token=<%= it.token %>`,
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
file_id: "<%= it.file_id %>",
|
||||||
|
key: "<%= it.file_version_id %>",
|
||||||
|
token: "<%= it.token %>"
|
||||||
|
}),
|
||||||
|
success: function() {
|
||||||
|
console.log('save success');
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
console.log('save error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.docEditor = new DocsAPI.DocEditor('onlyoffice_container_instance', {
|
||||||
|
scrollSensitivity: window.mode === 'text' ? 100 : 40,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
documentType: window.mode,
|
||||||
|
document: doc,
|
||||||
|
token: "<%= it.file_id %>",
|
||||||
|
type: screen.width < 600 ? 'mobile' : 'desktop',
|
||||||
|
editorConfig: {
|
||||||
|
callbackUrl: `${window.baseURL}save?file_id=<%= it.file_id %>&company_id=<%= it.company_id %>&token=<%= it.token %>`,
|
||||||
|
lang: window.user.language,
|
||||||
|
user: {
|
||||||
|
id: window.user.id,
|
||||||
|
name: window.user.username,
|
||||||
|
},
|
||||||
|
customization: {
|
||||||
|
chat: false,
|
||||||
|
compactToolbar: true,
|
||||||
|
about: false,
|
||||||
|
feedback: false,
|
||||||
|
goback: {
|
||||||
|
text: '',
|
||||||
|
blank: false,
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
onDocumentStateChange: documentChangeHandler,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,10 +62,12 @@ services:
|
|||||||
- tdrive_network
|
- tdrive_network
|
||||||
|
|
||||||
onlyoffice-connector:
|
onlyoffice-connector:
|
||||||
image: onlyoffice-connector
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/onlyoffice-connector/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
- CREDENTIALS_ENDPOINT=http://tdrive_node:4000
|
- 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_ORIGIN=http://onlyoffice-connector:5000
|
||||||
- SERVER_PORT=5000
|
- SERVER_PORT=5000
|
||||||
- SERVER_PREFIX=/plugins/onlyoffice
|
- SERVER_PREFIX=/plugins/onlyoffice
|
||||||
@@ -110,7 +112,6 @@ services:
|
|||||||
|
|
||||||
onlyoffice:
|
onlyoffice:
|
||||||
image: docker.io/onlyoffice/documentserver
|
image: docker.io/onlyoffice/documentserver
|
||||||
hostname: onlyoffice
|
|
||||||
ports:
|
ports:
|
||||||
- 8090:80
|
- 8090:80
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -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" ]
|
||||||
Reference in New Issue
Block a user