From 78d3bb604586622884c1e01a3a75071a936b33f5 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 16 May 2022 13:43:23 +0800 Subject: [PATCH] refactor(ui): add session guard to ui page (#803) * refactor(ui): add session guard to ui page add session guard to ui page * test(core): fix ut fix ut * fix(core): fix typo fix typo --- packages/core/src/app/init.ts | 3 + .../koa-client-session-guard.test.ts | 77 +++++++++++++++++++ .../middleware/koa-client-session-guard.ts | 44 +++++++++++ packages/phrases/src/locales/en.ts | 5 +- packages/phrases/src/locales/zh-cn.ts | 5 +- packages/ui/src/App.tsx | 4 + .../ui/src/pages/ErrorPage/index.test.tsx | 4 +- packages/ui/src/pages/ErrorPage/index.tsx | 9 ++- 8 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 packages/core/src/middleware/koa-client-session-guard.test.ts create mode 100644 packages/core/src/middleware/koa-client-session-guard.ts diff --git a/packages/core/src/app/init.ts b/packages/core/src/app/init.ts index 528d6318e..9ff77a0e5 100644 --- a/packages/core/src/app/init.ts +++ b/packages/core/src/app/init.ts @@ -7,6 +7,7 @@ import koaLogger from 'koa-logger'; import mount from 'koa-mount'; import envSet, { MountedApps } from '@/env-set'; +import koaClientSessionGuard from '@/middleware/koa-client-session-guard'; import koaConnectorErrorHandler from '@/middleware/koa-connector-error-handle'; import koaErrorHandler from '@/middleware/koa-error-handler'; import koaI18next from '@/middleware/koa-i18next'; @@ -37,6 +38,8 @@ export default async function initApp(app: Koa): Promise { app.use( mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console)) ); + + app.use(koaClientSessionGuard(provider)); app.use(koaSpaProxy()); const { httpsCert, httpsKey, port } = envSet.values; diff --git a/packages/core/src/middleware/koa-client-session-guard.test.ts b/packages/core/src/middleware/koa-client-session-guard.test.ts new file mode 100644 index 000000000..8511a2bd8 --- /dev/null +++ b/packages/core/src/middleware/koa-client-session-guard.test.ts @@ -0,0 +1,77 @@ +import { Provider } from 'oidc-provider'; + +import { MountedApps } from '@/env-set'; +import { createContextWithRouteParameters } from '@/utils/test-utils'; + +import koaClientSessionGuard, { sessionNotFoundPath } from './koa-client-session-guard'; + +jest.mock('fs/promises', () => ({ + ...jest.requireActual('fs/promises'), + readdir: jest.fn().mockResolvedValue(['index.js']), +})); + +jest.mock('oidc-provider', () => ({ + Provider: jest.fn(() => ({ + interactionDetails: jest.fn(), + })), +})); + +describe('koaClientSessionGuard', () => { + const envBackup = process.env; + + beforeEach(() => { + process.env = { ...envBackup }; + }); + + afterEach(() => { + 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 redirect`, async () => { + const provider = new Provider(''); + const ctx = createContextWithRouteParameters({ + url: `/${app}/foo`, + }); + + await koaClientSessionGuard(provider)(ctx, next); + + expect(ctx.redirect).not.toBeCalled(); + }); + } + + it('should not redirect if session found', async () => { + const provider = new Provider(''); + const ctx = createContextWithRouteParameters({ + url: `/sign-in`, + }); + await koaClientSessionGuard(provider)(ctx, next); + expect(ctx.redirect).not.toBeCalled(); + }); + + it('should not redirect if path is sessionNotFoundPath', async () => { + const provider = new Provider(''); + + (provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found')); + const ctx = createContextWithRouteParameters({ + url: `${sessionNotFoundPath}`, + }); + await koaClientSessionGuard(provider)(ctx, next); + expect(ctx.redirect).not.toBeCalled(); + }); + + it('should redirect if session not found', async () => { + const provider = new Provider(''); + + (provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found')); + const ctx = createContextWithRouteParameters({ + url: '/sign-in', + }); + await koaClientSessionGuard(provider)(ctx, next); + expect(ctx.redirect).toBeCalled(); + }); +}); diff --git a/packages/core/src/middleware/koa-client-session-guard.ts b/packages/core/src/middleware/koa-client-session-guard.ts new file mode 100644 index 000000000..5ee218fcc --- /dev/null +++ b/packages/core/src/middleware/koa-client-session-guard.ts @@ -0,0 +1,44 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import { MiddlewareType } from 'koa'; +import { IRouterParamContext } from 'koa-router'; +import { Provider } from 'oidc-provider'; + +import { MountedApps } from '@/env-set'; +import { fromRoot } from '@/env-set/parameters'; + +export const sessionNotFoundPath = '/unknown-session'; + +export default function koaSpaSessionGuard< + StateT, + ContextT extends IRouterParamContext, + ResponseBodyT +>(provider: Provider): MiddlewareType { + return async (ctx, next) => { + const requestPath = ctx.request.path; + const packagesPath = fromRoot ? 'packages/' : '..'; + const distPath = path.join(packagesPath, 'ui', 'dist'); + + // Guard client routes only + if (Object.values(MountedApps).some((app) => requestPath.startsWith(`/${app}`))) { + return next(); + } + + try { + // Find session + await provider.interactionDetails(ctx.req, ctx.res); + } catch { + const spaDistFiles = await fs.readdir(distPath); + + if ( + !spaDistFiles.some((file) => requestPath.startsWith('/' + file)) && + !ctx.request.path.endsWith(sessionNotFoundPath) + ) { + ctx.redirect(sessionNotFoundPath); + } + + return next(); + } + }; +} diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 0f572ece9..9ab018761 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -83,8 +83,9 @@ const translation = { passwords_do_not_match: 'Passwords do not match.', agree_terms_required: 'You must agree to the Terms of Use before continuing.', invalid_passcode: 'The passcode is invalid.', - request: 'Request Error:{{message}}', - unknown: 'Request Error, please try again later.', + request: 'Request error {{message}}', + unknown: 'Unknown error, please try again later.', + invalid_session: 'Session not found. Please go back and sign in again.', }, }, admin_console: { diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index d1c2e3702..1d45bb3ab 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -83,8 +83,9 @@ const translation = { passwords_do_not_match: '密码不匹配。', agree_terms_required: '你需要同意使用条款以继续。', invalid_passcode: '无效的验证码。', - request: '请求异常:{{ message }}', - unknown: '请求异常,请稍后重试。', + request: '请求错误:{{ message }}', + unknown: '未知错误,请稍后重试。', + invalid_session: '未找到有效的会话,请重新登录。', }, }, admin_console: { diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 50afe324a..9273ec102 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -54,6 +54,10 @@ const App = () => { } /> } /> } /> + } + /> } /> diff --git a/packages/ui/src/pages/ErrorPage/index.test.tsx b/packages/ui/src/pages/ErrorPage/index.test.tsx index e3a8acc38..4230bc2d7 100644 --- a/packages/ui/src/pages/ErrorPage/index.test.tsx +++ b/packages/ui/src/pages/ErrorPage/index.test.tsx @@ -8,10 +8,10 @@ describe('ErrorPage Page', () => { it('render properly', () => { const { queryByText } = render( - + ); expect(queryByText('description.not_found')).not.toBeNull(); - expect(queryByText('error message')).not.toBeNull(); + expect(queryByText('error.invalid_email')).not.toBeNull(); }); }); diff --git a/packages/ui/src/pages/ErrorPage/index.tsx b/packages/ui/src/pages/ErrorPage/index.tsx index 3854927c9..944d20c12 100644 --- a/packages/ui/src/pages/ErrorPage/index.tsx +++ b/packages/ui/src/pages/ErrorPage/index.tsx @@ -10,20 +10,23 @@ import * as styles from './index.module.scss'; type Props = { title?: TFuncKey<'translation', 'main_flow'>; - message?: string; + message?: TFuncKey<'translation', 'main_flow'>; + rawMessage?: string; }; -const ErrorPage = ({ title = 'description.not_found', message }: Props) => { +const ErrorPage = ({ title = 'description.not_found', message, rawMessage }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); const navigate = useNavigate(); + const errorMessage = rawMessage || (message && t(message)); + return (
{t(title)}
- {message &&
{message}
} + {errorMessage &&
{errorMessage}
}