diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 26ab979c64..f6a607b855 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -86,6 +86,7 @@ specifiers: '@rush-temp/server-contact': file:./projects/server-contact.tgz '@rush-temp/server-contact-resources': file:./projects/server-contact-resources.tgz '@rush-temp/server-core': file:./projects/server-core.tgz + '@rush-temp/server-token': file:./projects/server-token.tgz '@rush-temp/server-tool': file:./projects/server-tool.tgz '@rush-temp/server-ws': file:./projects/server-ws.tgz '@rush-temp/setting': file:./projects/setting.tgz @@ -162,6 +163,7 @@ specifiers: intl-messageformat: ^9.7.1 jpeg-js: ~0.4.3 just-clone: ^3.2.1 + jwt-simple: ~0.5.6 koa: ^2.13.1 koa-bodyparser: ^4.3.0 koa-router: ^10.1.1 @@ -278,6 +280,7 @@ dependencies: '@rush-temp/server-contact': file:projects/server-contact.tgz '@rush-temp/server-contact-resources': file:projects/server-contact-resources.tgz '@rush-temp/server-core': file:projects/server-core.tgz + '@rush-temp/server-token': file:projects/server-token.tgz '@rush-temp/server-tool': file:projects/server-tool.tgz '@rush-temp/server-ws': file:projects/server-ws.tgz '@rush-temp/setting': file:projects/setting.tgz @@ -354,6 +357,7 @@ dependencies: intl-messageformat: 9.10.0 jpeg-js: 0.4.3 just-clone: 3.2.1 + jwt-simple: 0.5.6 koa: 2.13.4 koa-bodyparser: 4.3.0 koa-router: 10.1.1 @@ -11422,7 +11426,7 @@ packages: dev: false file:projects/account.tgz: - resolution: {integrity: sha512-T69SM17sZnYKxu3FyjI47F+wFix2aKjpenQxDThYp5qNkNgZ4/1+znXlJUhUUL+GQpmcLDJ3kcnu/2dkcM1NGg==, tarball: file:projects/account.tgz} + resolution: {integrity: sha512-5AF4BBNU65wTk1D+dIOhXhzHGi6QDuGpi/DOEAwF4A1I0f6+J3bq9T5SU/2zXY0i9TLYhD3szFi5zrGcx/KGAg==, tarball: file:projects/account.tgz} name: '@rush-temp/account' version: 0.0.0 dependencies: @@ -11811,7 +11815,7 @@ packages: dev: false file:projects/dev-account.tgz: - resolution: {integrity: sha512-gKJqJzfPozNrp2KQpEqOwYvlO7xVnxH8/re0ECfdLyDMlHtC3fypTxdprYL/7FheupKZp3YTY4T3MCG2+SG40w==, tarball: file:projects/dev-account.tgz} + resolution: {integrity: sha512-hsYsHT6KU6FPYdWhJ6Npwqu2rZmjOoj8FqbV4q3Mie9kG/CAuKAMm2nFw7Vv2E9lpIaeLAJyP4sVI3WkBIGkOg==, tarball: file:projects/dev-account.tgz} name: '@rush-temp/dev-account' version: 0.0.0 dependencies: @@ -11979,7 +11983,7 @@ packages: dev: false file:projects/front.tgz: - resolution: {integrity: sha512-iSBgFbScuEX0S/r4FJh/z02rKgeaMPKCe0Z7Unr/g2AzgeO65k1G6uaT/mtlr1ulaYixN+dMMmaxzkJT5psDeQ==, tarball: file:projects/front.tgz} + resolution: {integrity: sha512-RXsa4jlZB6UdPjSIAHmf07BEcWlH6N26QnAVFQ3QL5VdqLi73ohsPQV9seKz36c5jGsA//Z0BS9QYVCETuHdgA==, tarball: file:projects/front.tgz} name: '@rush-temp/front' version: 0.0.0 dependencies: @@ -12012,7 +12016,7 @@ packages: dev: false file:projects/generator.tgz: - resolution: {integrity: sha512-Difehi/KDbulPB9s3UOP1fSeLfvWQwpSUr77OnMF31EoCLAXT+9ugFduvlveFdnYCttYKpgGO2bvaxBaH7krdA==, tarball: file:projects/generator.tgz} + resolution: {integrity: sha512-FfLadJ6fn6vv/PvtsqrXqC/kRieldqLFfWmeb1Q2wPTnMmWv7r71Hj9tcM8G9BG7HBNolsdqt2yHHHm+SowMTg==, tarball: file:projects/generator.tgz} name: '@rush-temp/generator' version: 0.0.0 dependencies: @@ -13292,6 +13296,31 @@ packages: eslint-plugin-import: 2.25.3_eslint@7.32.0 eslint-plugin-node: 11.1.0_eslint@7.32.0 eslint-plugin-promise: 5.2.0_eslint@7.32.0 + jwt-simple: 0.5.6 + minio: 7.0.26 + prettier: 2.5.1 + typescript: 4.5.4 + transitivePeerDependencies: + - supports-color + dev: false + + file:projects/server-token.tgz: + resolution: {integrity: sha512-74lvKW1J8vMQI7r+UUFCO8KDItftmBVTc9ecLL9kbWHdFT/kfR1ua57O8XG7MMJDmBVsWylHmM1kr6lxwNpNBA==, tarball: file:projects/server-token.tgz} + name: '@rush-temp/server-token' + version: 0.0.0 + dependencies: + '@rushstack/heft': 0.41.8 + '@types/heft-jest': 1.0.2 + '@types/minio': 7.0.11 + '@types/node': 16.11.14 + '@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237 + '@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4 + eslint: 7.32.0 + eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a + eslint-plugin-import: 2.25.3_eslint@7.32.0 + eslint-plugin-node: 11.1.0_eslint@7.32.0 + eslint-plugin-promise: 5.2.0_eslint@7.32.0 + jwt-simple: 0.5.6 minio: 7.0.26 prettier: 2.5.1 typescript: 4.5.4 @@ -13300,7 +13329,7 @@ packages: dev: false file:projects/server-tool.tgz: - resolution: {integrity: sha512-jA31a+Q2vADtUml9IaD4IKCxzzqWKz74NIRkBVhY3VqQpiXup+g+Pa2Rf2f3J57yT8aQKCpf63YMysWR42iQjQ==, tarball: file:projects/server-tool.tgz} + resolution: {integrity: sha512-Cib8Y9814bARmY3GxSBzMKpmJvQiz149kzwokA3IIbvAqHw+ndpuZYF7gcOG6c6y8975CNltyx4wSlUiojLFfg==, tarball: file:projects/server-tool.tgz} name: '@rush-temp/server-tool' version: 0.0.0 dependencies: @@ -13328,7 +13357,7 @@ packages: dev: false file:projects/server-ws.tgz: - resolution: {integrity: sha512-MSFFpLjIMFt0oyH4+8JUkNOkCNtdEtMDoxcyN7+kDdz44wSZjSOmheJHYkXO6JTEffcaaRhQ9vO/e7MBNMeoxQ==, tarball: file:projects/server-ws.tgz} + resolution: {integrity: sha512-kesBl2gLp031syY1dFuI26w339buhEKja2/9LjlYpV4sX1E8l8+a2RmPOlE6tBRhQ+bEEq/l5YIYNHxBBhGuxg==, tarball: file:projects/server-ws.tgz} name: '@rush-temp/server-ws' version: 0.0.0 dependencies: @@ -13763,7 +13792,7 @@ packages: dev: false file:projects/tool.tgz: - resolution: {integrity: sha512-yAIi2mb58Lhm+ysa7lZxq+at3RSkc5UlkZhhcPRX9BUq+8dHV0PyJZQb+sH2ys77588F+4CV7jCpxmKERe3HYQ==, tarball: file:projects/tool.tgz} + resolution: {integrity: sha512-tQSiv1J1yUul6SD2WNyzzSCJeS1VCZgWkun9xGIJELvQ+iZN6S4fznI35eMLBppJWD0XIYHmeOzNCfjX4PWw7g==, tarball: file:projects/tool.tgz} name: '@rush-temp/tool' version: 0.0.0 dependencies: diff --git a/dev/account/package.json b/dev/account/package.json index 7198365d99..ce33fc3ded 100644 --- a/dev/account/package.json +++ b/dev/account/package.json @@ -28,6 +28,6 @@ }, "dependencies": { "@anticrm/platform": "~0.6.5", - "jwt-simple": "^0.5.6" + "@anticrm/server-token": "~0.6.0" } } diff --git a/dev/account/src/account.ts b/dev/account/src/account.ts index f957ed96a3..84af3b2874 100644 --- a/dev/account/src/account.ts +++ b/dev/account/src/account.ts @@ -17,7 +17,7 @@ import type { Request, Response } from '@anticrm/platform' import platform, { Status, Severity } from '@anticrm/platform' -import { encode } from 'jwt-simple' +import { generateToken } from '@anticrm/server-token' interface LoginInfo { token: string @@ -37,7 +37,7 @@ function login (endpoint: string, email: string, password: string, workspace: st return { error: new Status(Severity.ERROR, platform.status.Unauthorized, {}) } } - const token = encode({ email, workspace }, 'secret') + const token = generateToken(email, workspace) return { result: { token, endpoint } } } diff --git a/dev/generator/package.json b/dev/generator/package.json index 096c7c6c69..02360d4261 100644 --- a/dev/generator/package.json +++ b/dev/generator/package.json @@ -39,7 +39,6 @@ "dependencies": { "commander": "^8.1.0", "@anticrm/account": "~0.6.0", - "jwt-simple": "^0.5.6", "@anticrm/core": "~0.6.11", "@anticrm/contact": "~0.6.2", "@anticrm/model-all": "~0.6.0", @@ -59,6 +58,7 @@ "minio": "^7.0.19", "@types/pdfkit": "~0.12.3", "@anticrm/task": "~0.6.0", - "jpeg-js": "~0.4.3" + "jpeg-js": "~0.4.3", + "@anticrm/server-token": "~0.6.0" } } diff --git a/dev/generator/src/connect.ts b/dev/generator/src/connect.ts index bba42f2067..a76df3476a 100644 --- a/dev/generator/src/connect.ts +++ b/dev/generator/src/connect.ts @@ -3,14 +3,14 @@ import client from '@anticrm/client' import clientResources from '@anticrm/client-resources' import { Client } from '@anticrm/core' import { setMetadata } from '@anticrm/platform' -import { encode } from 'jwt-simple' +import { generateToken } from '@anticrm/server-token' // eslint-disable-next-line const WebSocket = require('ws') export async function connect (transactorUrl: string, workspace: string): Promise { console.log('connecting to transactor...') - const token = encode({ email: 'anticrm@hc.engineering', workspace }, 'secret') + const token = generateToken('anticrm@hc.engineering', workspace) // We need to override default factory with 'ws' one. setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) diff --git a/dev/tool/package.json b/dev/tool/package.json index 97ca04572e..048a3bdb78 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -42,7 +42,6 @@ "mongodb": "^4.1.1", "commander": "^8.1.0", "@anticrm/account": "~0.6.0", - "jwt-simple": "^0.5.6", "@anticrm/core": "~0.6.11", "@anticrm/contact": "~0.6.2", "minio": "^7.0.19", @@ -58,6 +57,7 @@ "@elastic/elasticsearch": "^7.14.0", "@anticrm/elastic": "~0.6.0", "@anticrm/server-core": "~0.6.1", + "@anticrm/server-token": "~0.6.0", "@anticrm/model-attachment": "~0.6.0", "@anticrm/mongo": "~0.6.0", "@anticrm/dev-storage": "~0.6.0", diff --git a/dev/tool/src/connect.ts b/dev/tool/src/connect.ts index bba42f2067..a76df3476a 100644 --- a/dev/tool/src/connect.ts +++ b/dev/tool/src/connect.ts @@ -3,14 +3,14 @@ import client from '@anticrm/client' import clientResources from '@anticrm/client-resources' import { Client } from '@anticrm/core' import { setMetadata } from '@anticrm/platform' -import { encode } from 'jwt-simple' +import { generateToken } from '@anticrm/server-token' // eslint-disable-next-line const WebSocket = require('ws') export async function connect (transactorUrl: string, workspace: string): Promise { console.log('connecting to transactor...') - const token = encode({ email: 'anticrm@hc.engineering', workspace }, 'secret') + const token = generateToken('anticrm@hc.engineering', workspace) // We need to override default factory with 'ws' one. setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index b409856de6..60ad3f1ed2 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -28,7 +28,8 @@ import { upgradeWorkspace } from '@anticrm/account' import { setMetadata } from '@anticrm/platform' -import toolPlugin, { generateToken, prepareTools, version } from '@anticrm/server-tool' +import { generateToken } from '@anticrm/server-token' +import toolPlugin, { prepareTools, version } from '@anticrm/server-tool' import { program } from 'commander' import { Db, MongoClient } from 'mongodb' import { rebuildElastic } from './elastic' diff --git a/rush.json b/rush.json index 300bbd5d0e..af2c7a5ba7 100644 --- a/rush.json +++ b/rush.json @@ -496,6 +496,11 @@ "projectFolder": "server/core", "shouldPublish": true }, + { + "packageName": "@anticrm/server-token", + "projectFolder": "server/token", + "shouldPublish": true + }, { "packageName": "@anticrm/server", "projectFolder": "server/server", diff --git a/server/account/package.json b/server/account/package.json index e99abb8af4..b499c9762b 100644 --- a/server/account/package.json +++ b/server/account/package.json @@ -36,9 +36,9 @@ "@anticrm/contact": "~0.6.2", "@anticrm/client-resources": "~0.6.4", "@anticrm/client": "~0.6.1", - "jwt-simple": "~0.5.6", "ws": "^8.2.0", "@anticrm/model": "~0.6.0", - "@anticrm/server-tool": "~0.6.0" + "@anticrm/server-tool": "~0.6.0", + "@anticrm/server-token": "~0.6.0" } } diff --git a/server/account/src/index.ts b/server/account/src/index.ts index 43e8aa3e7d..e2ba423b07 100644 --- a/server/account/src/index.ts +++ b/server/account/src/index.ts @@ -27,7 +27,8 @@ import platform, { Status, StatusCode } from '@anticrm/platform' -import toolPlugin, { connect, initModel, upgradeModel, version, decodeToken, generateToken } from '@anticrm/server-tool' +import toolPlugin, { connect, initModel, upgradeModel, version } from '@anticrm/server-tool' +import { decodeToken, generateToken } from '@anticrm/server-token' import { pbkdf2Sync, randomBytes } from 'crypto' import { Binary, Db, ObjectId } from 'mongodb' @@ -393,7 +394,7 @@ export async function assignWorkspace (db: Db, email: string, workspace: string) } async function createEmployeeAccount (account: Account, workspace: string): Promise { - const connection = await connect(getTransactor(), workspace, account.email) + const connection = await connect(getTransactor(), workspace, false, account.email) try { const ops = new TxOperations(connection, core.account.System) @@ -471,7 +472,7 @@ export async function changeName (db: Db, token: string, first: string, last: st } async function updateEmployeeAccount (account: Account, workspace: string): Promise { - const connection = await connect(getTransactor(), workspace, account.email) + const connection = await connect(getTransactor(), workspace, false, account.email) try { const ops = new TxOperations(connection, core.account.System) diff --git a/server/core/src/types.ts b/server/core/src/types.ts index a4f1f52eb8..1e3bf52ce1 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -76,13 +76,6 @@ export interface FullTextAdapter { */ export type FullTextAdapterFactory = (url: string, workspace: string) => Promise -/** - * @public - */ -export interface Token { - workspace: string -} - /** * @public */ diff --git a/server/front/package.json b/server/front/package.json index b1492429dc..21b0f75628 100644 --- a/server/front/package.json +++ b/server/front/package.json @@ -43,8 +43,8 @@ "uuid": "^8.3.2", "cors": "^2.8.5", "@anticrm/elastic": "~0.6.0", - "jwt-simple": "^0.5.6", "@anticrm/server-core": "~0.6.1", + "@anticrm/server-token": "~0.6.0", "@anticrm/attachment": "~0.6.0", "@anticrm/contrib": "~0.6.0", "minio": "^7.0.19" diff --git a/server/front/src/__start.ts b/server/front/src/__start.ts index 26358a5f2f..805f35ba65 100644 --- a/server/front/src/__start.ts +++ b/server/front/src/__start.ts @@ -14,8 +14,8 @@ // limitations under the License. // -import { start } from './app' import { Client } from 'minio' +import { start } from './app' const SERVER_PORT = parseInt(process.env.SERVER_PORT ?? '8080') @@ -77,4 +77,14 @@ if (modelVersion === undefined) { const config = { transactorEndpoint, elasticUrl, minio, accountsUrl, uploadUrl, modelVersion } console.log('Starting Front service with', config) -start(config, SERVER_PORT) +const shutdown = start(config, SERVER_PORT) + +const close = (): void => { + console.trace('Exiting from server') + console.log('Shutdown request accepted') + shutdown() + process.exit(0) +} + +process.on('SIGINT', close) +process.on('SIGTERM', close) diff --git a/server/front/src/app.ts b/server/front/src/app.ts index b02c0428b5..bb915935f5 100644 --- a/server/front/src/app.ts +++ b/server/front/src/app.ts @@ -17,36 +17,16 @@ import attachment from '@anticrm/attachment' import { Account, Doc, Ref, Space } from '@anticrm/core' import { createElasticAdapter } from '@anticrm/elastic' -// import { TxFactory } from '@anticrm/core' -import type { IndexedDoc, Token } from '@anticrm/server-core' +import type { IndexedDoc } from '@anticrm/server-core' +import { decodeToken } from '@anticrm/server-token' import cors from 'cors' import express from 'express' import fileUpload, { UploadedFile } from 'express-fileupload' import https from 'https' -import { decode } from 'jwt-simple' -// import { createContributingClient } from '@anticrm/contrib' import { Client, ItemBucketMetadata } from 'minio' import { join, resolve } from 'path' import { v4 as uuid } from 'uuid' -// import { createElasticAdapter } from '@anticrm/elastic' - -// const BUCKET = 'anticrm-upload-9e4e89c' - -// async function awsUpload (file: UploadedFile): Promise { -// const id = uuid() -// const s3 = new S3() -// const resp = await s3.upload({ -// Bucket: BUCKET, -// Key: id, -// Body: file.data, -// ContentType: file.mimetype, -// ACL: 'public-read' -// }).promise() -// console.log(resp) -// return id -// } - async function minioUpload (minio: Client, workspace: string, file: UploadedFile): Promise { const id = uuid() const meta: ItemBucketMetadata = { @@ -59,25 +39,11 @@ async function minioUpload (minio: Client, workspace: string, file: UploadedFile return id } -// async function createAttachment (endpoint: string, token: string, account: Ref, space: Ref, attachedTo: Ref, collection: string, name: string, file: string): Promise { -// const txFactory = new TxFactory(account) -// const tx = txFactory.createTxCreateDoc(chunter.class.Attachment, space, { -// attachedTo, -// collection, -// name, -// file -// }) -// const url = new URL(`/${token}`, endpoint) -// const client = await createContributingClient(url.href) -// await client.tx(tx) -// client.close() -// } - /** * @public * @param port - */ -export function start (config: { transactorEndpoint: string, elasticUrl: string, minio: Client, accountsUrl: string, uploadUrl: string, modelVersion: string }, port: number): void { +export function start (config: { transactorEndpoint: string, elasticUrl: string, minio: Client, accountsUrl: string, uploadUrl: string, modelVersion: string }, port: number): () => void { const app = express() app.use(cors()) @@ -104,7 +70,7 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string, app.get('/files', async (req, res) => { try { const token = req.query.token as string - const payload = decode(token, 'secret', false) as Token + const payload = decodeToken(token) const uuid = req.query.file as string const stat = await config.minio.statObject(payload.workspace, uuid) @@ -153,7 +119,7 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string, try { const token = authHeader.split(' ')[1] - const payload = decode(token ?? '', 'secret', false) as Token + const payload = decodeToken(token) // const fileId = await awsUpload(file as UploadedFile) const uuid = await minioUpload(config.minio, payload.workspace, file) console.log('uploaded uuid', uuid) @@ -207,7 +173,7 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string, } const token = authHeader.split(' ')[1] - const payload = decode(token ?? '', 'secret', false) as Token + const payload = decodeToken(token) const uuid = req.query.file as string await config.minio.removeObject(payload.workspace, uuid) @@ -227,7 +193,7 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string, return } const token = authHeader.split(' ')[1] - const payload = decode(token ?? '', 'secret', false) as Token + const payload = decodeToken(token) const url = req.query.url as string const cookie = req.query.cookie as string | undefined const attachedTo = req.query.attachedTo as Ref | undefined @@ -305,7 +271,7 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string, return } const token = authHeader.split(' ')[1] - const payload = decode(token ?? '', 'secret', false) as Token + const payload = decodeToken(token) const { url, cookie, attachedTo, space } = req.body console.log('importing from', url) @@ -377,5 +343,8 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string, response.sendFile(join(dist, 'index.html')) }) - app.listen(port) + const server = app.listen(port) + return () => { + server.close() + } } diff --git a/server/server/package.json b/server/server/package.json index d9e9e94310..62460bf641 100644 --- a/server/server/package.json +++ b/server/server/package.json @@ -46,6 +46,6 @@ "elastic-apm-node": "~3.26.0", "minio": "~7.0.26", "@anticrm/server-contact": "~0.6.1", - "@anticrm/server-contact-resources": "~0.6.0" + "@anticrm/server-contact-resources": "~0.6.0" } } diff --git a/server/token/.eslintrc.js b/server/token/.eslintrc.js new file mode 100644 index 0000000000..5da5872d4a --- /dev/null +++ b/server/token/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/server/token/.npmignore b/server/token/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server/token/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server/token/config/rig.json b/server/token/config/rig.json new file mode 100644 index 0000000000..af1257a896 --- /dev/null +++ b/server/token/config/rig.json @@ -0,0 +1,18 @@ +// The "rig.json" file directs tools to look for their config files in an external package. +// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + /** + * (Required) The name of the rig package to inherit from. + * It should be an NPM package name with the "-rig" suffix. + */ + "rigPackageName": "@anticrm/platform-rig" + + /** + * (Optional) Selects a config profile from the rig package. The name must consist of + * lowercase alphanumeric words separated by hyphens, for example "sample-profile". + * If omitted, then the "default" profile will be used." + */ + // "rigProfile": "your-profile-name" +} diff --git a/server/token/package.json b/server/token/package.json new file mode 100644 index 0000000000..c22d1dd1bb --- /dev/null +++ b/server/token/package.json @@ -0,0 +1,36 @@ +{ + "name": "@anticrm/server-token", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "heft build", + "build:watch": "tsc", + "lint:fix": "eslint --fix src", + "lint": "eslint src", + "format": "prettier --write src && eslint --fix src" + }, + "devDependencies": { + "@anticrm/platform-rig": "~0.6.0", + "@types/heft-jest": "^1.0.2", + "@types/node": "^16.4.10", + "@types/minio": "~7.0.11", + "@typescript-eslint/eslint-plugin": "^5.4.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-node": "^11.1.0", + "eslint": "^7.32.0", + "@typescript-eslint/parser": "^5.4.0", + "eslint-config-standard-with-typescript": "^21.0.1", + "prettier": "^2.4.1", + "@rushstack/heft": "^0.41.1", + "typescript": "^4.3.5" + }, + "dependencies": { + "@anticrm/core": "~0.6.11", + "@anticrm/platform": "~0.6.5", + "minio": "~7.0.26", + "jwt-simple": "~0.5.6" + } +} diff --git a/server/token/src/index.ts b/server/token/src/index.ts new file mode 100644 index 0000000000..dd6f940800 --- /dev/null +++ b/server/token/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { default } from './plugin' +export * from './token' diff --git a/server/token/src/plugin.ts b/server/token/src/plugin.ts new file mode 100644 index 0000000000..ecea82802b --- /dev/null +++ b/server/token/src/plugin.ts @@ -0,0 +1,33 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Metadata, Plugin } from '@anticrm/platform' +import { plugin } from '@anticrm/platform' + +/** + * @public + */ +export const serverTokenId = 'server-token' as Plugin + +/** + * @public + */ +const serverToken = plugin(serverTokenId, { + metadata: { + Secret: '' as Metadata + } +}) + +export default serverToken diff --git a/server/token/src/token.ts b/server/token/src/token.ts new file mode 100644 index 0000000000..c50ca83a9a --- /dev/null +++ b/server/token/src/token.ts @@ -0,0 +1,32 @@ +import { getMetadata } from '@anticrm/platform' +import serverPlugin from './plugin' +import { encode, decode } from 'jwt-simple' + +/** + * @public + */ +export interface Token { + email: string + workspace: string + extra?: Record +} + +const getSecret = (): string => { + return getMetadata(serverPlugin.metadata.Secret) ?? 'secret' +} + +/** + * @public + */ +export function generateToken (email: string, workspace: string, extra?: Record): string { + return encode({ ...(extra ?? {}), email, workspace }, getSecret()) +} + +/** + * @public + */ +export function decodeToken (token: string): Token { + const value = decode(token, getSecret(), false) + const { email, workspace, ...extra } = value + return { email, workspace, extra } +} diff --git a/server/token/tsconfig.json b/server/token/tsconfig.json new file mode 100644 index 0000000000..548666d1a2 --- /dev/null +++ b/server/token/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "target": "ES2019", + "rootDir": "./src", + "outDir": "./lib", + "esModuleInterop": true + } +} \ No newline at end of file diff --git a/server/tool/package.json b/server/tool/package.json index 13cb83ce6c..a01c20fe94 100644 --- a/server/tool/package.json +++ b/server/tool/package.json @@ -36,8 +36,8 @@ "@anticrm/contact": "~0.6.2", "@anticrm/client-resources": "~0.6.4", "@anticrm/client": "~0.6.1", - "jwt-simple": "~0.5.6", "ws": "^8.2.0", - "@anticrm/model": "~0.6.0" + "@anticrm/model": "~0.6.0", + "@anticrm/server-token": "~0.6.0" } } diff --git a/server/tool/src/connect.ts b/server/tool/src/connect.ts index 81a5a50d69..9912a7788f 100644 --- a/server/tool/src/connect.ts +++ b/server/tool/src/connect.ts @@ -17,19 +17,14 @@ import client from '@anticrm/client' import clientResources from '@anticrm/client-resources' import { Client } from '@anticrm/core' -import { getMetadata, setMetadata } from '@anticrm/platform' -import { encode } from 'jwt-simple' - -import toolPlugin from './plugin' +import { setMetadata } from '@anticrm/platform' +import { generateToken } from '@anticrm/server-token' /** * @public */ -export async function connect (transactorUrl: string, workspace: string, email?: string): Promise { - const token = encode( - { email: email ?? 'anticrm@hc.engineering', workspace }, - getMetadata(toolPlugin.metadata.Secret) ?? 'secret' - ) +export async function connect (transactorUrl: string, workspace: string, reloadModel: boolean, email?: string): Promise { + const token = generateToken(email ?? 'anticrm@hc.engineering', workspace, reloadModel ? { model: 'reload' } : undefined) // We need to override default factory with 'ws' one. // eslint-disable-next-line diff --git a/server/tool/src/index.ts b/server/tool/src/index.ts index de02111639..fa9abf111c 100644 --- a/server/tool/src/index.ts +++ b/server/tool/src/index.ts @@ -16,8 +16,6 @@ import contact from '@anticrm/contact' import core, { DOMAIN_TX, Tx } from '@anticrm/core' import builder, { createDeps, migrateOperations } from '@anticrm/model-all' -import { getMetadata } from '@anticrm/platform' -import { decode, encode } from 'jwt-simple' import { Client } from 'minio' import { Document, MongoClient } from 'mongodb' import { connect } from './connect' @@ -99,7 +97,7 @@ export async function initModel (transactorUrl: string, dbName: string): Promise console.log(`${result.insertedCount} model transactions inserted.`) console.log('creating data...') - const connection = await connect(transactorUrl, dbName) + const connection = await connect(transactorUrl, dbName, true) try { await createDeps(connection) } catch (e) { @@ -155,7 +153,7 @@ export async function upgradeModel ( console.log('Apply upgrade operations') - const connection = await connect(transactorUrl, dbName) + const connection = await connect(transactorUrl, dbName, true) for (const op of migrateOperations) { await op.upgrade(connection) } @@ -165,21 +163,3 @@ export async function upgradeModel ( await client.close() } } - -const getSecret = (): string => { - return getMetadata(toolPlugin.metadata.Secret) ?? 'secret' -} - -/** - * @public - */ -export function generateToken (email: string, workspace: string): string { - return encode({ email, workspace }, getSecret()) -} - -/** - * @public - */ -export function decodeToken (token: string): { email: string, workspace: string} { - return decode(token, getSecret()) -} diff --git a/server/ws/package.json b/server/ws/package.json index 829427f795..b78ace703d 100644 --- a/server/ws/package.json +++ b/server/ws/package.json @@ -29,10 +29,10 @@ "typescript": "^4.3.5" }, "dependencies": { - "jwt-simple": "^0.5.6", "ws": "^8.0.0", "@anticrm/platform": "~0.6.5", "@anticrm/core": "~0.6.11", - "@anticrm/server-core": "~0.6.1" + "@anticrm/server-core": "~0.6.1", + "@anticrm/server-token": "~0.6.0" } } diff --git a/server/ws/src/__tests__/remote.test.ts.txt b/server/ws/src/__tests__/remote.test.ts.txt index cf6b565d8a..5ac74cbff5 100644 --- a/server/ws/src/__tests__/remote.test.ts.txt +++ b/server/ws/src/__tests__/remote.test.ts.txt @@ -16,7 +16,6 @@ import { readResponse, serialize } from '@anticrm/platform' import type { Token } from '@anticrm/server-core' -import { encode } from 'jwt-simple' import WebSocket from 'ws' describe('server', () => { diff --git a/server/ws/src/__tests__/server.test.ts b/server/ws/src/__tests__/server.test.ts index e32ea0352b..e8523dd7a4 100644 --- a/server/ws/src/__tests__/server.test.ts +++ b/server/ws/src/__tests__/server.test.ts @@ -16,8 +16,7 @@ import { readResponse, serialize } from '@anticrm/platform' import { start, disableLogging } from '../server' -import type { Token } from '@anticrm/server-core' -import { encode } from 'jwt-simple' +import { generateToken } from '@anticrm/server-token' import WebSocket from 'ws' import type { Doc, Ref, Class, DocumentQuery, FindOptions, FindResult, Tx, TxResult, MeasureContext } from '@anticrm/core' @@ -38,11 +37,8 @@ describe('server', () => { }), 3333) function connect (): WebSocket { - const payload: Token = { - workspace: 'latest' - } - const token = encode(payload, 'secret') - return new WebSocket('ws://localhost:3333/' + token) + const token: string = generateToken('', 'latest') + return new WebSocket(`ws://localhost:3333/${token}`) } it('should connect to server', (done) => { diff --git a/server/ws/src/server.ts b/server/ws/src/server.ts index 1daf73c3b9..413f15f227 100644 --- a/server/ws/src/server.ts +++ b/server/ws/src/server.ts @@ -16,12 +16,11 @@ import { Class, Doc, DocumentQuery, FindOptions, FindResult, MeasureContext, Ref, ServerStorage, Tx, TxResult } from '@anticrm/core' import { readRequest, Response, serialize, unknownError } from '@anticrm/platform' -import type { Token } from '@anticrm/server-core' +import { decodeToken, Token } from '@anticrm/server-token' import { createServer, IncomingMessage } from 'http' -import { decode } from 'jwt-simple' import WebSocket, { Server } from 'ws' -let LOGGING_ENABLED = false +let LOGGING_ENABLED = true export function disableLogging (): void { LOGGING_ENABLED = false } @@ -60,38 +59,57 @@ class SessionManager { async addSession (ws: WebSocket, token: Token, storageFactory: (ws: string) => Promise): Promise { const workspace = this.workspaces.get(token.workspace) if (workspace === undefined) { - const storage = await storageFactory(token.workspace) - const session = new Session(this, token, storage) - const workspace: Workspace = { - storage, - sessions: [[session, ws]] - } - this.workspaces.set(token.workspace, workspace) - return session + return await this.createWorkspace(storageFactory, token, ws) } else { + if (token.extra?.model === 'reload') { + console.log('reloading workspace', JSON.stringify(token)) + // If upgrade client is used. + // Drop all existing clients + if (workspace.sessions.length > 0) { + for (const s of workspace.sessions) { + this.close(s[1], token.workspace, 0, 'upgrade') + } + } + return await this.createWorkspace(storageFactory, token, ws) + } + const session = new Session(this, token, workspace.storage) workspace.sessions.push([session, ws]) return session } } - close (ws: WebSocket, token: Token, code: number, reason: string): void { + private async createWorkspace (storageFactory: (ws: string) => Promise, token: Token, ws: WebSocket): Promise { + const storage = await storageFactory(token.workspace) + const session = new Session(this, token, storage) + const workspace: Workspace = { + storage, + sessions: [[session, ws]] + } + this.workspaces.set(token.workspace, workspace) + return session + } + + close (ws: WebSocket, workspaceId: string, code: number, reason: string): void { if (LOGGING_ENABLED) console.log(`closing websocket, code: ${code}, reason: ${reason}`) - const workspace = this.workspaces.get(token.workspace) + const workspace = this.workspaces.get(workspaceId) if (workspace === undefined) { - throw new Error('internal: cannot find sessions') + console.error(new Error('internal: cannot find sessions')) + return } workspace.sessions = workspace.sessions.filter(session => session[1] !== ws) if (workspace.sessions.length === 0) { - if (LOGGING_ENABLED) console.log('no sessions for workspace', token.workspace) - this.workspaces.delete(token.workspace) + if (LOGGING_ENABLED) console.log('no sessions for workspace', workspaceId) + this.workspaces.delete(workspaceId) + workspace.storage.close().catch(err => console.error(err)) } } broadcast (from: Session | null, token: Token, resp: Response): void { const workspace = this.workspaces.get(token.workspace) if (workspace === undefined) { - throw new Error('internal: cannot find sessions') + console.error(new Error('internal: cannot find sessions')) + return } if (LOGGING_ENABLED) console.log(`server broadcasting to ${workspace.sessions.length} clients...`) const msg = serialize(resp) @@ -138,7 +156,7 @@ export function start (ctx: MeasureContext, storageFactory: (workspace: string) const session = await sessions.addSession(ws, token, storageFactory) // eslint-disable-next-line @typescript-eslint/no-misused-promises ws.on('message', async (msg: string) => await handleRequest(ctx, session, ws, msg)) - ws.on('close', (code: number, reason: string) => sessions.close(ws, token, code, reason)) + ws.on('close', (code: number, reason: string) => sessions.close(ws, token.workspace, code, reason)) for (const msg of buffer) { await handleRequest(ctx, session, ws, msg) @@ -149,7 +167,7 @@ export function start (ctx: MeasureContext, storageFactory: (workspace: string) server.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => { const token = request.url?.substring(1) // remove leading '/' try { - const payload = decode(token ?? '', 'secret', false) + const payload = decodeToken(token ?? '') console.log('client connected with payload', payload) wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request, payload)) } catch (err) {