mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): connect console (#306)
* feat(core): connect console * fix(core): prod spa dist * fix(core): test * test(core): refactor spa proxy tests
This commit is contained in:
parent
5734333dea
commit
365c63b2c7
10 changed files with 149 additions and 142 deletions
|
@ -7,7 +7,7 @@
|
|||
"lerna": "lerna",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"prepare": "if test \"$NODE_ENV\" != \"production\" && test \"$CI\" != \"true\" ; then husky install ; fi",
|
||||
"dev": "lerna run --stream prepack && lerna --scope=@logto/{core,ui} exec -- pnpm dev"
|
||||
"dev": "lerna run --stream prepack && lerna --scope=@logto/{core,ui,console} exec -- pnpm dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^13.2.1",
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
"preinstall": "npx only-allow pnpm",
|
||||
"precommit": "lint-staged",
|
||||
"start": "parcel src/index.html",
|
||||
"dev": "PORT=5002 parcel src/index.html --public-url /console --no-hmr",
|
||||
"check": "tsc --noEmit",
|
||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall",
|
||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --public-url /console",
|
||||
"lint": "eslint --ext .ts --ext .tsx src",
|
||||
"stylelint": "stylelint \"src/**/*.scss\""
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as koaErrorHandler from '@/middleware/koa-error-handler';
|
|||
import * as koaI18next from '@/middleware/koa-i18next';
|
||||
import * as koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
||||
import * as koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
||||
import * as koaUIProxy from '@/middleware/koa-ui-proxy';
|
||||
import * as koaSpaProxy from '@/middleware/koa-spa-proxy';
|
||||
import * as koaUserLog from '@/middleware/koa-user-log';
|
||||
import * as initOidc from '@/oidc/init';
|
||||
import * as initRouter from '@/routes/init';
|
||||
|
@ -20,7 +20,7 @@ describe('App Init', () => {
|
|||
koaI18next,
|
||||
koaOIDCErrorHandler,
|
||||
koaSlonikErrorHandler,
|
||||
koaUIProxy,
|
||||
koaSpaProxy,
|
||||
koaUserLog,
|
||||
];
|
||||
const initMethods = [initRouter, initOidc];
|
||||
|
|
|
@ -3,14 +3,15 @@ import https from 'https';
|
|||
|
||||
import Koa from 'koa';
|
||||
import koaLogger from 'koa-logger';
|
||||
import mount from 'koa-mount';
|
||||
|
||||
import { port } from '@/env/consts';
|
||||
import { MountedApps, port } from '@/env/consts';
|
||||
import koaConnectorErrorHandler from '@/middleware/koa-connector-error-handle';
|
||||
import koaErrorHandler from '@/middleware/koa-error-handler';
|
||||
import koaI18next from '@/middleware/koa-i18next';
|
||||
import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
||||
import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
||||
import koaUIProxy from '@/middleware/koa-ui-proxy';
|
||||
import koaSpaProxy from '@/middleware/koa-spa-proxy';
|
||||
import koaUserLog from '@/middleware/koa-user-log';
|
||||
import initOidc from '@/oidc/init';
|
||||
import initRouter from '@/routes/init';
|
||||
|
@ -29,7 +30,10 @@ export default async function initApp(app: Koa): Promise<void> {
|
|||
const provider = await initOidc(app);
|
||||
initRouter(app, provider);
|
||||
|
||||
app.use(koaUIProxy());
|
||||
app.use(
|
||||
mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console))
|
||||
);
|
||||
app.use(koaSpaProxy());
|
||||
|
||||
const { HTTPS_CERT, HTTPS_KEY } = process.env;
|
||||
|
||||
|
|
6
packages/core/src/env/consts.ts
vendored
6
packages/core/src/env/consts.ts
vendored
|
@ -3,7 +3,11 @@ import { assertEnv, getEnv } from '@silverhand/essentials';
|
|||
export const signIn = assertEnv('UI_SIGN_IN_ROUTE');
|
||||
export const isProduction = getEnv('NODE_ENV') === 'production';
|
||||
export const port = Number(getEnv('PORT', '3001'));
|
||||
export const mountedApps = Object.freeze(['api', 'oidc']);
|
||||
export enum MountedApps {
|
||||
Api = 'api',
|
||||
Oidc = 'oidc',
|
||||
Console = 'console',
|
||||
}
|
||||
export const developmentUserId = getEnv('DEVELOPMENT_USER_ID');
|
||||
|
||||
// Trusting TLS offloading proxies: https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#trusting-tls-offloading-proxies
|
||||
|
|
72
packages/core/src/middleware/koa-spa-proxy.test.ts
Normal file
72
packages/core/src/middleware/koa-spa-proxy.test.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { MountedApps } from '@/env/consts';
|
||||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
import koaSpaProxy from './koa-spa-proxy';
|
||||
|
||||
const mockProxyMiddleware = jest.fn();
|
||||
const mockStaticMiddleware = jest.fn();
|
||||
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
readdir: jest.fn().mockResolvedValue(['sign-in']),
|
||||
}));
|
||||
|
||||
jest.mock('koa-proxies', () => jest.fn(() => mockProxyMiddleware));
|
||||
jest.mock('koa-static', () => jest.fn(() => mockStaticMiddleware));
|
||||
|
||||
describe('koaSpaProxy middleware', () => {
|
||||
const envBackup = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...envBackup };
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
for (const app of Object.values(MountedApps)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
it(`${app} path should not call dev proxy`, async () => {
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${app}/foo`,
|
||||
});
|
||||
|
||||
await koaSpaProxy()(ctx, next);
|
||||
|
||||
expect(mockProxyMiddleware).not.toBeCalled();
|
||||
});
|
||||
}
|
||||
|
||||
it('dev env should call dev proxy for SPA paths', async () => {
|
||||
const ctx = createContextWithRouteParameters();
|
||||
await koaSpaProxy()(ctx, next);
|
||||
expect(mockProxyMiddleware).toBeCalled();
|
||||
});
|
||||
|
||||
it('production env should overwrite the request path to root if no target ui file are detected', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: '/foo',
|
||||
});
|
||||
|
||||
const { default: proxy } = await import('./koa-spa-proxy');
|
||||
await proxy()(ctx, next);
|
||||
|
||||
expect(mockStaticMiddleware).toBeCalled();
|
||||
expect(ctx.request.path).toEqual('/');
|
||||
});
|
||||
|
||||
it('production env should call the static middleware if path hit the ui file directory', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const { default: proxy } = await import('./koa-spa-proxy');
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: '/sign-in',
|
||||
});
|
||||
|
||||
await proxy()(ctx, next);
|
||||
expect(mockStaticMiddleware).toBeCalled();
|
||||
});
|
||||
});
|
59
packages/core/src/middleware/koa-spa-proxy.ts
Normal file
59
packages/core/src/middleware/koa-spa-proxy.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { MiddlewareType } from 'koa';
|
||||
import proxy from 'koa-proxies';
|
||||
import { IRouterParamContext } from 'koa-router';
|
||||
import serveStatic from 'koa-static';
|
||||
|
||||
import { isProduction, MountedApps } from '@/env/consts';
|
||||
|
||||
export default function koaSpaProxy<StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
|
||||
packagePath = 'ui',
|
||||
port = 5001,
|
||||
prefix = ''
|
||||
): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
type Middleware = MiddlewareType<StateT, ContextT, ResponseBodyT>;
|
||||
|
||||
const distPath = path.join('..', packagePath, 'dist');
|
||||
|
||||
const spaProxy: Middleware = isProduction
|
||||
? serveStatic(distPath)
|
||||
: proxy('*', {
|
||||
target: `http://localhost:${port}`,
|
||||
changeOrigin: true,
|
||||
logs: true,
|
||||
rewrite: (requestPath) => {
|
||||
// Static files
|
||||
if (requestPath.includes('.')) {
|
||||
return '/' + path.join(prefix, requestPath);
|
||||
}
|
||||
|
||||
// In-app routes
|
||||
return requestPath;
|
||||
},
|
||||
});
|
||||
|
||||
return async (ctx, next) => {
|
||||
const requestPath = ctx.request.path;
|
||||
|
||||
// Route has been handled by one of mounted apps
|
||||
if (
|
||||
Object.values(MountedApps).some((app) => app !== prefix && requestPath.startsWith(`/${app}`))
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!isProduction) {
|
||||
return spaProxy(ctx, next);
|
||||
}
|
||||
|
||||
const spaDistFiles = await fs.readdir(distPath);
|
||||
|
||||
if (!spaDistFiles.some((file) => requestPath.startsWith('/' + file))) {
|
||||
ctx.request.path = '/';
|
||||
}
|
||||
|
||||
return spaProxy(ctx, next);
|
||||
};
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
import { mountedApps } from '@/env/consts';
|
||||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
import koaUIProxy from './koa-ui-proxy';
|
||||
|
||||
const mockProxyMiddleware = jest.fn();
|
||||
const mockStaticMiddleware = jest.fn();
|
||||
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
readdir: jest.fn().mockResolvedValue(['sign-in']),
|
||||
}));
|
||||
|
||||
jest.mock('koa-proxies', () => jest.fn(() => mockProxyMiddleware));
|
||||
jest.mock('koa-static', () => jest.fn(() => mockStaticMiddleware));
|
||||
|
||||
describe('koaUIProxy middleware', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
for (const app of mountedApps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
it(`${app} path should not call uiProxy`, async () => {
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${app}/foo`,
|
||||
});
|
||||
|
||||
await koaUIProxy()(ctx, next);
|
||||
|
||||
expect(mockProxyMiddleware).not.toBeCalled();
|
||||
});
|
||||
}
|
||||
|
||||
it('dev env should call proxy middleware for ui paths', async () => {
|
||||
const ctx = createContextWithRouteParameters();
|
||||
await koaUIProxy()(ctx, next);
|
||||
expect(mockProxyMiddleware).toBeCalled();
|
||||
});
|
||||
|
||||
it('production env should overwrite the request path to root if no target ui file are detected', async () => {
|
||||
// Mock the @/env/consts
|
||||
jest.mock('@/env/consts', () => ({
|
||||
...jest.requireActual('@/env/consts'),
|
||||
isProduction: true,
|
||||
}));
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable unicorn/prefer-module */
|
||||
const koaUIProxyModule = require('./koa-ui-proxy') as { default: typeof koaUIProxy };
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
/* eslint-enable @typescript-eslint/no-var-requires */
|
||||
/* eslint-enable unicorn/prefer-module */
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: '/foo',
|
||||
});
|
||||
|
||||
await koaUIProxyModule.default()(ctx, next);
|
||||
expect(mockStaticMiddleware).toBeCalled();
|
||||
expect(ctx.request.path).toEqual('/');
|
||||
});
|
||||
|
||||
it('production env should call the static middleware if path hit the ui file directory', async () => {
|
||||
// Mock the @/env/consts
|
||||
jest.mock('@/env/consts', () => ({
|
||||
...jest.requireActual('@/env/consts'),
|
||||
isProduction: true,
|
||||
}));
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable unicorn/prefer-module */
|
||||
const koaUIProxyModule = require('./koa-ui-proxy') as { default: typeof koaUIProxy };
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
/* eslint-enable @typescript-eslint/no-var-requires */
|
||||
/* eslint-enable unicorn/prefer-module */
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: '/sign-in',
|
||||
});
|
||||
|
||||
await koaUIProxyModule.default()(ctx, next);
|
||||
expect(mockStaticMiddleware).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
import fs from 'fs/promises';
|
||||
|
||||
import { MiddlewareType } from 'koa';
|
||||
import proxy from 'koa-proxies';
|
||||
import { IRouterParamContext } from 'koa-router';
|
||||
import serveStatic from 'koa-static';
|
||||
|
||||
import { isProduction, mountedApps } from '@/env/consts';
|
||||
|
||||
const PATH_TO_UI_DIST = '../ui/dist';
|
||||
|
||||
export default function koaUIProxy<
|
||||
StateT,
|
||||
ContextT extends IRouterParamContext,
|
||||
ResponseBodyT
|
||||
>(): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
type Middleware = MiddlewareType<StateT, ContextT, ResponseBodyT>;
|
||||
|
||||
const uiProxy: Middleware = isProduction
|
||||
? serveStatic(PATH_TO_UI_DIST)
|
||||
: proxy('*', {
|
||||
target: 'http://localhost:5001',
|
||||
changeOrigin: true,
|
||||
logs: true,
|
||||
});
|
||||
|
||||
return async (ctx, next) => {
|
||||
// Route has been handled by one of mounted apps
|
||||
if (mountedApps.some((app) => ctx.request.path.startsWith(`/${app}`))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!isProduction) {
|
||||
return uiProxy(ctx, next);
|
||||
}
|
||||
|
||||
const uiDistFiles = await fs.readdir(PATH_TO_UI_DIST);
|
||||
|
||||
if (!uiDistFiles.some((file) => ctx.request.path.startsWith(`/${file}`))) {
|
||||
ctx.request.path = '/';
|
||||
}
|
||||
|
||||
return uiProxy(ctx, next);
|
||||
};
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
"preinstall": "npx only-allow pnpm",
|
||||
"precommit": "lint-staged",
|
||||
"start": "parcel src/index.html",
|
||||
"dev": "PORT=5001 pnpm start",
|
||||
"dev": "PORT=5001 parcel src/index.html --no-hmr",
|
||||
"check": "tsc --noEmit",
|
||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall",
|
||||
"lint": "eslint --ext .ts --ext .tsx src",
|
||||
|
|
Loading…
Reference in a new issue