0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

Merge pull request #51 from logto-io/gao--refactor-routes

refactor(core): align `next()` usage and add prod mode for ui proxy
This commit is contained in:
Gao Sun 2021-07-30 13:58:57 +08:00 committed by GitHub
commit 181f198260
13 changed files with 146 additions and 61 deletions

View file

@ -10,7 +10,8 @@
"precommit": "lint-staged",
"build": "rm -rf build/ && tsc",
"lint": "eslint --format pretty --ext .ts src",
"dev": "rm -rf build/ && tsc-watch --preserveWatchOutput --onSuccess \"node ./build/index.js\""
"dev": "rm -rf build/ && tsc-watch --preserveWatchOutput --onSuccess \"node ./build/index.js\"",
"start": "NODE_ENV=production node build/index.js"
},
"dependencies": {
"@logto/essentials": "^1.1.0-rc.2",
@ -29,6 +30,7 @@
"koa-mount": "^4.0.0",
"koa-proxies": "^0.12.1",
"koa-router": "^10.0.0",
"koa-static": "^5.0.0",
"lodash.pick": "^4.4.0",
"module-alias": "^2.2.2",
"nanoid": "^3.1.23",
@ -44,6 +46,7 @@
"@types/koa-logger": "^3.1.1",
"@types/koa-mount": "^4.0.0",
"@types/koa-router": "^7.4.2",
"@types/koa-static": "^4.0.2",
"@types/lodash.pick": "^4.4.6",
"@types/node": "^16.3.1",
"@types/oidc-provider": "^7.4.1",

View file

@ -9,5 +9,7 @@ export const routes = Object.freeze({
},
});
export const isProduction = getEnv('NODE_ENV') === 'production';
export const port = Number(getEnv('PORT', '3001'));
export const oidcIssuer = getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`);
export const mountedApps = Object.freeze(['api', 'oidc']);

View file

@ -1,12 +1,12 @@
declare module 'koa-body' {
import { IKoaBodyOptions } from 'node_modules/koa-body';
import { Middleware } from 'koa';
import { MiddlewareType } from 'koa';
declare function koaBody<
StateT = Record<string, unknown>,
ContextT = Record<string, unknown>,
ResponseBodyT = any
>(options?: IKoaBodyOptions): Middleware<StateT, ContextT, ResponseBodyT>;
>(options?: IKoaBodyOptions): MiddlewareType<StateT, ContextT, ResponseBodyT>;
export = koaBody;
}

13
packages/core/src/include.d/koa.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
import { DefaultState, DefaultContext, ParameterizedContext, Next } from 'koa';
declare module 'koa' {
// Have to do this patch since `compose.Middleware` returns `any`.
export type KoaNext<T> = () => Promise<T>;
export type KoaMiddleware<T, R> = (context: T, next: KoaNext<R>) => Promise<void>;
export type MiddlewareType<
StateT = DefaultState,
ContextT = DefaultContext,
ResponseBodyT = any,
NextT = void
> = KoaMiddleware<ParameterizedContext<StateT, ContextT, ResponseBodyT>, NextT>;
}

View file

@ -3,21 +3,22 @@ import Router from 'koa-router';
import { Provider } from 'oidc-provider';
import signInRoutes from '@/routes/sign-in';
import registerRoutes from '@/routes/register';
import uiProxy from '@/proxies/ui';
import swaggerRoutes from '@/routes/swagger';
import mount from 'koa-mount';
const createRouter = (provider: Provider): Router => {
const router = new Router({ prefix: '/api' });
const router = new Router();
router.use(signInRoutes(provider));
router.use(registerRoutes());
router.use(swaggerRoutes());
signInRoutes(router, provider);
registerRoutes(router);
swaggerRoutes(router);
return router;
};
export default function initRouter(app: Koa, provider: Provider): Router {
export default function initRouter(app: Koa, provider: Provider) {
const router = createRouter(provider);
app.use(router.routes()).use(uiProxy()).use(router.allowedMethods());
return router;
const apisApp = new Koa().use(router.routes()).use(router.allowedMethods());
app.use(mount('/api', apisApp));
}

View file

@ -3,8 +3,9 @@ import koaLogger from 'koa-logger';
import koaErrorHandler from '@/middleware/koa-error-handler';
import { port } from '@/consts';
import koaUIProxy from '@/middleware/koa-ui-proxy';
import initOidc from './oidc';
import initRouter from './router';
import initRouter from './apis';
export default async function initApp(app: Koa): Promise<void> {
app.use(koaErrorHandler());
@ -13,6 +14,8 @@ export default async function initApp(app: Koa): Promise<void> {
const provider = await initOidc(app);
initRouter(app, provider);
app.use(koaUIProxy());
app.listen(port, () => {
console.log(`App is listening on port ${port}`);
});

View file

@ -1,6 +1,6 @@
import RequestError from '@/errors/RequestError';
import { has } from '@logto/essentials';
import { Middleware } from 'koa';
import { MiddlewareType } from 'koa';
import koaBody from 'koa-body';
import { IMiddleware, IRouterParamContext } from 'koa-router';
import { ZodType } from 'zod';
@ -51,12 +51,12 @@ export default function koaGuard<
query,
body,
params,
}: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT>): Middleware<
}: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT>): MiddlewareType<
StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT
> {
const guard: Middleware<
const guard: MiddlewareType<
StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT
@ -72,24 +72,21 @@ export default function koaGuard<
throw new RequestError('guard.invalid_input', error);
}
await next();
return next();
};
const guardMiddleware: WithGuardConfig<
Middleware<
MiddlewareType<
StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT
>
> = async function (ctx, next) {
if (body) {
await koaBody<StateT, ContextT>()(ctx, async () => {
await guard(ctx, next);
});
return;
return koaBody<StateT, ContextT>()(ctx, async () => guard(ctx, next));
}
await guard(ctx, next);
return guard(ctx, next);
};
guardMiddleware.config = { query, body, params };

View file

@ -0,0 +1,41 @@
import fs from 'fs';
import { Middleware } from 'koa';
import proxy from 'koa-proxies';
import serveStatic from 'koa-static';
import { IRouterParamContext } from 'koa-router';
import { isProduction, mountedApps } from '@/consts';
const PATH_TO_UI_DIST = '../ui/build/public';
const uiDistFiles = fs.readdirSync(PATH_TO_UI_DIST);
export default function koaUIProxy<
StateT,
ContextT extends IRouterParamContext,
ResponseBodyT
>(): Middleware<StateT, ContextT, ResponseBodyT> {
const developmentProxy = proxy('*', {
target: 'http://localhost:5000',
changeOrigin: true,
logs: true,
});
const staticProxy = serveStatic(PATH_TO_UI_DIST);
return async (context, next) => {
// Route has been handled by one of mounted apps
if (mountedApps.some((app) => context.request.path.startsWith(`/${app}`))) {
return next();
}
if (!isProduction) {
await developmentProxy(context, next);
return next();
}
if (!uiDistFiles.some((file) => context.request.path.startsWith(`/${file}`))) {
context.request.path = '/';
}
await staticProxy(context, next);
return next();
};
}

View file

@ -1,10 +0,0 @@
import proxy from 'koa-proxies';
// CAUTION: this is for testing only
export default function uiProxy() {
return proxy(/^\/(?!api|oidc).*$/, {
target: 'http://localhost:5000',
changeOrigin: true,
logs: true,
});
}

View file

@ -22,9 +22,7 @@ const generateUserId = async (maxRetries = 500) => {
throw new Error('Cannot generate user ID in reasonable retries');
};
export default function registerRoutes() {
const router = new Router();
export default function registerRoutes(router: Router) {
router.post(
'/register',
koaGuard({
@ -33,7 +31,7 @@ export default function registerRoutes() {
password: string().min(6),
}),
}),
async (ctx) => {
async (ctx, next) => {
const { username, password } = ctx.guard.body;
if (await hasUser(username)) {
@ -59,8 +57,7 @@ export default function registerRoutes() {
});
ctx.body = { id };
return next();
}
);
return router.routes();
}

View file

@ -9,13 +9,11 @@ import koaGuard from '@/middleware/koa-guard';
import RequestError from '@/errors/RequestError';
import { LogtoErrorCode } from '@logto/phrases';
export default function signInRoutes(provider: Provider) {
const router = new Router();
export default function signInRoutes(router: Router, provider: Provider) {
router.post(
'/sign-in',
koaGuard({ body: object({ username: string().optional(), password: string().optional() }) }),
async (ctx) => {
async (ctx, next) => {
const {
prompt: { name },
} = await provider.interactionDetails(ctx.req, ctx.res);
@ -60,10 +58,12 @@ export default function signInRoutes(provider: Provider) {
} else {
throw new Error(`Prompt not supported: ${name}`);
}
return next();
}
);
router.post('/sign-in/consent', async (ctx) => {
router.post('/sign-in/consent', async (ctx, next) => {
const { session, grantId, params, prompt } = await provider.interactionDetails(
ctx.req,
ctx.res
@ -95,16 +95,17 @@ export default function signInRoutes(provider: Provider) {
{ mergeWithLastSubmission: true }
);
ctx.body = { redirectTo };
return next();
});
router.post('/sign-in/abort', async (ctx) => {
router.post('/sign-in/abort', async (ctx, next) => {
await provider.interactionDetails(ctx.req, ctx.res);
const error: LogtoErrorCode = 'oidc.aborted';
const redirectTo = await provider.interactionResult(ctx.req, ctx.res, {
error,
});
ctx.body = { redirectTo };
return next();
});
return router.routes();
}

View file

@ -4,10 +4,8 @@ import { isGuardMiddleware, WithGuardConfig } from '@/middleware/koa-guard';
import { toTitle } from '@/utils/string';
import { zodTypeToSwagger } from '@/utils/zod';
export default function swaggerRoutes() {
const router = new Router();
router.get('/swagger.json', async (ctx) => {
export default function swaggerRoutes(router: Router) {
router.get('/swagger.json', async (ctx, next) => {
const routes = ctx.router.stack.map(({ path, stack, methods }) => {
const guard = stack.find((function_): function_ is WithGuardConfig<IMiddleware> =>
isGuardMiddleware(function_)
@ -17,16 +15,15 @@ export default function swaggerRoutes() {
const paths = Object.fromEntries(
routes.map<[string, OpenAPIV3.PathItemObject]>(({ path, methods, guard }) => {
const trimmedPath = path.slice(4);
const body = guard?.config.body;
return [
trimmedPath,
`/api${path}`,
Object.fromEntries(
methods.map<[string, OpenAPIV3.OperationObject]>((method) => [
method.toLowerCase(),
{
tags: [toTitle(trimmedPath.split('/')[1] ?? 'General')],
tags: [toTitle(path.split('/')[1] ?? 'General')],
requestBody: body && {
required: true,
content: {
@ -57,7 +54,7 @@ export default function swaggerRoutes() {
};
ctx.body = document;
});
return router.routes();
return next();
});
}

View file

@ -27,6 +27,7 @@ importers:
'@types/koa-logger': ^3.1.1
'@types/koa-mount': ^4.0.0
'@types/koa-router': ^7.4.2
'@types/koa-static': ^4.0.2
'@types/lodash.pick': ^4.4.6
'@types/node': ^16.3.1
'@types/oidc-provider': ^7.4.1
@ -45,6 +46,7 @@ importers:
koa-mount: ^4.0.0
koa-proxies: ^0.12.1
koa-router: ^10.0.0
koa-static: ^5.0.0
lint-staged: ^11.1.1
lodash.pick: ^4.4.0
module-alias: ^2.2.2
@ -74,6 +76,7 @@ importers:
koa-mount: 4.0.0
koa-proxies: 0.12.1_koa@2.13.1
koa-router: 10.0.0
koa-static: 5.0.0
lodash.pick: 4.4.0
module-alias: 2.2.2
nanoid: 3.1.23
@ -88,6 +91,7 @@ importers:
'@types/koa-logger': 3.1.1
'@types/koa-mount': 4.0.0
'@types/koa-router': 7.4.4
'@types/koa-static': 4.0.2
'@types/lodash.pick': 4.4.6
'@types/node': 16.4.6
'@types/oidc-provider': 7.4.2
@ -3150,6 +3154,19 @@ packages:
'@types/koa': 2.13.4
dev: true
/@types/koa-send/4.1.3:
resolution: {integrity: sha512-daaTqPZlgjIJycSTNjKpHYuKhXYP30atFc1pBcy6HHqB9+vcymDgYTguPdx9tO4HMOqNyz6bz/zqpxt5eLR+VA==}
dependencies:
'@types/koa': 2.13.4
dev: true
/@types/koa-static/4.0.2:
resolution: {integrity: sha512-ns/zHg+K6XVPMuohjpOlpkR1WLa4VJ9czgUP9bxkCDn0JZBtUWbD/wKDZzPGDclkQK1bpAEScufCHOy8cbfL0w==}
dependencies:
'@types/koa': 2.13.4
'@types/koa-send': 4.1.3
dev: true
/@types/koa/2.13.4:
resolution: {integrity: sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==}
dependencies:
@ -5719,7 +5736,6 @@ packages:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
dependencies:
ms: 2.1.3
dev: true
/debug/4.3.2:
resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==}
@ -7900,7 +7916,6 @@ packages:
inherits: 2.0.3
setprototypeof: 1.1.0
statuses: 1.5.0
dev: true
/http-errors/1.7.2:
resolution: {integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==}
@ -8196,7 +8211,6 @@ packages:
/inherits/2.0.3:
resolution: {integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=}
dev: true
/inherits/2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -9526,6 +9540,27 @@ packages:
- supports-color
dev: false
/koa-send/5.0.1:
resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==}
engines: {node: '>= 8'}
dependencies:
debug: 4.3.2
http-errors: 1.8.0
resolve-path: 1.4.0
transitivePeerDependencies:
- supports-color
dev: false
/koa-static/5.0.0:
resolution: {integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==}
engines: {node: '>= 7.6.0'}
dependencies:
debug: 3.2.7
koa-send: 5.0.1
transitivePeerDependencies:
- supports-color
dev: false
/koa/2.13.1:
resolution: {integrity: sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w==}
engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4}
@ -10413,7 +10448,6 @@ packages:
/ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/multi-fork/0.0.2:
resolution: {integrity: sha1-gFiuxGFBJMftqhWBm4juiJ0+tOA=}
@ -11371,7 +11405,6 @@ packages:
/path-is-absolute/1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
engines: {node: '>=0.10.0'}
dev: true
/path-is-inside/1.0.2:
resolution: {integrity: sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=}
@ -13048,6 +13081,14 @@ packages:
global-dirs: 0.1.1
dev: true
/resolve-path/1.4.0:
resolution: {integrity: sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=}
engines: {node: '>= 0.8'}
dependencies:
http-errors: 1.6.3
path-is-absolute: 1.0.1
dev: false
/resolve-pathname/3.0.0:
resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==}
dev: false
@ -13468,7 +13509,6 @@ packages:
/setprototypeof/1.1.0:
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
dev: true
/setprototypeof/1.1.1:
resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==}