0
Fork 0
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:
Gao Sun 2022-03-03 14:25:32 +08:00 committed by GitHub
parent 5734333dea
commit 365c63b2c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 142 deletions

View file

@ -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",

View file

@ -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\""
},

View file

@ -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];

View file

@ -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;

View file

@ -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

View 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();
});
});

View 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);
};
}

View file

@ -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();
});
});

View file

@ -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);
};
}

View file

@ -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",