mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
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
This commit is contained in:
parent
4020096319
commit
78d3bb6045
8 changed files with 142 additions and 9 deletions
|
@ -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<void> {
|
|||
app.use(
|
||||
mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console))
|
||||
);
|
||||
|
||||
app.use(koaClientSessionGuard(provider));
|
||||
app.use(koaSpaProxy());
|
||||
|
||||
const { httpsCert, httpsKey, port } = envSet.values;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
44
packages/core/src/middleware/koa-client-session-guard.ts
Normal file
44
packages/core/src/middleware/koa-client-session-guard.ts
Normal file
|
@ -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<StateT, ContextT, ResponseBodyT> {
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -54,6 +54,10 @@ const App = () => {
|
|||
<Route path="/callback/:connector" element={<Callback />} />
|
||||
<Route path="/social-register/:connector" element={<SocialRegister />} />
|
||||
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
|
||||
<Route
|
||||
path="/unknown-session"
|
||||
element={<ErrorPage message="error.invalid_session" />}
|
||||
/>
|
||||
<Route path="*" element={<ErrorPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
|
|
@ -8,10 +8,10 @@ describe('ErrorPage Page', () => {
|
|||
it('render properly', () => {
|
||||
const { queryByText } = render(
|
||||
<MemoryRouter>
|
||||
<ErrorPage title="description.not_found" message="error message" />
|
||||
<ErrorPage title="description.not_found" message="error.invalid_email" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
expect(queryByText('error message')).not.toBeNull();
|
||||
expect(queryByText('error.invalid_email')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
<div className={styles.wrapper}>
|
||||
<NavBar />
|
||||
<div className={styles.container}>
|
||||
<ErrorIcon />
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
{message && <div className={styles.message}>{message}</div>}
|
||||
{errorMessage && <div className={styles.message}>{errorMessage}</div>}
|
||||
</div>
|
||||
<Button
|
||||
className={styles.backButton}
|
||||
|
|
Loading…
Add table
Reference in a new issue