🖥️ 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
|
||||
|
||||
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:
|
||||
|
||||
@@ -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