diff --git a/packages/core/package.json b/packages/core/package.json index 5b23c29f6..cf9cdab03 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -58,6 +58,7 @@ "koa-proxies": "^0.12.1", "koa-router": "^12.0.0", "koa-send": "^5.0.1", + "lru-cache": "^7.14.1", "nanoid": "^4.0.0", "oidc-provider": "^7.13.0", "p-retry": "^5.1.2", diff --git a/packages/core/src/app/init.ts b/packages/core/src/app/init.ts index 5dfd858c5..17fd5aae6 100644 --- a/packages/core/src/app/init.ts +++ b/packages/core/src/app/init.ts @@ -1,26 +1,12 @@ import fs from 'fs/promises'; -import https from 'https'; +import http2 from 'http2'; import { deduplicate } from '@silverhand/essentials'; import chalk from 'chalk'; import type Koa from 'koa'; -import compose from 'koa-compose'; -import koaLogger from 'koa-logger'; -import mount from 'koa-mount'; -import envSet, { MountedApps } from '#src/env-set/index.js'; -import koaCheckDemoApp from '#src/middleware/koa-check-demo-app.js'; -import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js'; -import koaErrorHandler from '#src/middleware/koa-error-handler.js'; -import koaI18next from '#src/middleware/koa-i18next.js'; -import koaOIDCErrorHandler from '#src/middleware/koa-oidc-error-handler.js'; -import koaRootProxy from '#src/middleware/koa-root-proxy.js'; -import koaSlonikErrorHandler from '#src/middleware/koa-slonik-error-handler.js'; -import koaSpaProxy from '#src/middleware/koa-spa-proxy.js'; -import koaSpaSessionGuard from '#src/middleware/koa-spa-session-guard.js'; -import koaWelcomeProxy from '#src/middleware/koa-welcome-proxy.js'; -import initOidc from '#src/oidc/init.js'; -import initRouter from '#src/routes/init.js'; +import envSet from '#src/env-set/index.js'; +import { tenantPool } from '#src/tenants/index.js'; const logListening = () => { const { localhostUrl, endpoint } = envSet.values; @@ -30,39 +16,21 @@ const logListening = () => { } }; +const defaultTenant = 'default'; + export default async function initApp(app: Koa): Promise { - app.use(koaLogger()); - app.use(koaErrorHandler()); - app.use(koaOIDCErrorHandler()); - app.use(koaSlonikErrorHandler()); - app.use(koaConnectorErrorHandler()); - app.use(koaI18next()); + app.use(async (ctx, next) => { + // TODO: add multi-tenancy logic + const tenant = tenantPool.get(defaultTenant); - const provider = initOidc(app); - initRouter(app, provider); - - app.use(mount('/', koaRootProxy())); - - app.use(mount('/' + MountedApps.Welcome, koaWelcomeProxy())); - - app.use( - mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console)) - ); - - app.use( - mount( - '/' + MountedApps.DemoApp, - compose([koaCheckDemoApp(), koaSpaProxy(MountedApps.DemoApp, 5003, MountedApps.DemoApp)]) - ) - ); - - app.use(compose([koaSpaSessionGuard(provider), koaSpaProxy()])); + return tenant.run(ctx, next); + }); const { isHttpsEnabled, httpsCert, httpsKey, port } = envSet.values; if (isHttpsEnabled && httpsCert && httpsKey) { - https - .createServer( + http2 + .createSecureServer( { cert: await fs.readFile(httpsCert), key: await fs.readFile(httpsKey) }, app.callback() ) @@ -73,6 +41,7 @@ export default async function initApp(app: Koa): Promise { return; } + // Chrome doesn't allow insecure http/2 servers app.listen(port, () => { logListening(); }); diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts new file mode 100644 index 000000000..2268fb89a --- /dev/null +++ b/packages/core/src/tenants/Tenant.ts @@ -0,0 +1,64 @@ +import type { MiddlewareType } from 'koa'; +import Koa from 'koa'; +import compose from 'koa-compose'; +import koaLogger from 'koa-logger'; +import mount from 'koa-mount'; +import type { Provider } from 'oidc-provider'; + +import { MountedApps } from '#src/env-set/index.js'; +import koaCheckDemoApp from '#src/middleware/koa-check-demo-app.js'; +import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js'; +import koaErrorHandler from '#src/middleware/koa-error-handler.js'; +import koaI18next from '#src/middleware/koa-i18next.js'; +import koaOIDCErrorHandler from '#src/middleware/koa-oidc-error-handler.js'; +import koaRootProxy from '#src/middleware/koa-root-proxy.js'; +import koaSlonikErrorHandler from '#src/middleware/koa-slonik-error-handler.js'; +import koaSpaProxy from '#src/middleware/koa-spa-proxy.js'; +import koaSpaSessionGuard from '#src/middleware/koa-spa-session-guard.js'; +import koaWelcomeProxy from '#src/middleware/koa-welcome-proxy.js'; +import initOidc from '#src/oidc/init.js'; +import initRouter from '#src/routes/init.js'; + +export default class Tenant { + public readonly provider: Provider; + + protected readonly app: Koa; + + get run(): MiddlewareType { + return mount(this.app); + } + + constructor(public id: string) { + const app = new Koa(); + const provider = initOidc(app); + + app.use(koaLogger()); + app.use(koaErrorHandler()); + app.use(koaOIDCErrorHandler()); + app.use(koaSlonikErrorHandler()); + app.use(koaConnectorErrorHandler()); + app.use(koaI18next()); + + initRouter(app, provider); + + app.use(mount('/', koaRootProxy())); + + app.use(mount('/' + MountedApps.Welcome, koaWelcomeProxy())); + + app.use( + mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console)) + ); + + app.use( + mount( + '/' + MountedApps.DemoApp, + compose([koaCheckDemoApp(), koaSpaProxy(MountedApps.DemoApp, 5003, MountedApps.DemoApp)]) + ) + ); + + app.use(compose([koaSpaSessionGuard(provider), koaSpaProxy()])); + + this.app = app; + this.provider = provider; + } +} diff --git a/packages/core/src/tenants/index.ts b/packages/core/src/tenants/index.ts new file mode 100644 index 000000000..13a85bff9 --- /dev/null +++ b/packages/core/src/tenants/index.ts @@ -0,0 +1,22 @@ +import LRUCache from 'lru-cache'; + +import Tenant from './Tenant.js'; + +class TenantPool { + protected cache = new LRUCache({ max: 500 }); + + get(tenantId: string): Tenant { + const tenant = this.cache.get(tenantId); + + if (tenant) { + return tenant; + } + + const newTenant = new Tenant(tenantId); + this.cache.set(tenantId, newTenant); + + return newTenant; + } +} + +export const tenantPool = new TenantPool(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1263cb63f..a867c4d62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,7 @@ importers: koa-router: ^12.0.0 koa-send: ^5.0.1 lint-staged: ^13.0.0 + lru-cache: ^7.14.1 nanoid: ^4.0.0 node-mocks-http: ^1.12.1 nodemon: ^2.0.19 @@ -355,6 +356,7 @@ importers: koa-proxies: 0.12.1_koa@2.13.4 koa-router: 12.0.0 koa-send: 5.0.1 + lru-cache: 7.14.1 nanoid: 4.0.0 oidc-provider: 7.13.0 p-retry: 5.1.2 @@ -10245,6 +10247,11 @@ packages: dependencies: yallist: 4.0.0 + /lru-cache/7.14.1: + resolution: {integrity: sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==} + engines: {node: '>=12'} + dev: false + /lz-string/1.4.4: resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} hasBin: true