mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(core): add welcome route (#1080)
* feat(core): add welcome route add welcome route * fix(ui): fix some koa middleware fix some koa middleware * fix(core): ut fix ut fix * refactor(core): refactor welcome user guard refactor welcome user guard
This commit is contained in:
parent
dc7f9ccdb6
commit
f6f562a8ba
10 changed files with 139 additions and 21 deletions
|
@ -46,6 +46,8 @@ const Main = () => {
|
|||
<Suspense fallback={<LogtoLoading message="general.loading" />}>
|
||||
<Routes>
|
||||
<Route path="callback" element={<Callback />} />
|
||||
{/* TODO: add register route */}
|
||||
<Route path="register" element={<div>register</div>} />
|
||||
<Route element={<AppContent />}>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
<Route path="get-started" element={<GetStarted />} />
|
||||
|
|
|
@ -14,9 +14,11 @@ import koaErrorHandler from '@/middleware/koa-error-handler';
|
|||
import koaI18next from '@/middleware/koa-i18next';
|
||||
import koaLog from '@/middleware/koa-log';
|
||||
import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
||||
import koaProxyGuard from '@/middleware/koa-proxy-guard';
|
||||
import koaRootProxy from '@/middleware/koa-root-proxy';
|
||||
import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
|
||||
import koaSpaProxy from '@/middleware/koa-spa-proxy';
|
||||
import koaSpaSessionGuard from '@/middleware/koa-spa-session-guard';
|
||||
import koaWelcomeProxy from '@/middleware/koa-welcome-proxy';
|
||||
import initOidc from '@/oidc/init';
|
||||
import initRouter from '@/routes/init';
|
||||
|
||||
|
@ -37,9 +39,14 @@ export default async function initApp(app: Koa): Promise<void> {
|
|||
const provider = await initOidc(app);
|
||||
initRouter(app, provider);
|
||||
|
||||
app.use(mount('/', koaRootProxy()));
|
||||
|
||||
app.use(mount('/' + MountedApps.Welcome, koaWelcomeProxy()));
|
||||
|
||||
app.use(
|
||||
mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console))
|
||||
);
|
||||
|
||||
app.use(
|
||||
mount(
|
||||
'/' + MountedApps.DemoApp,
|
||||
|
@ -47,8 +54,7 @@ export default async function initApp(app: Koa): Promise<void> {
|
|||
)
|
||||
);
|
||||
|
||||
app.use(koaProxyGuard(provider));
|
||||
app.use(koaSpaProxy());
|
||||
app.use(compose([koaSpaSessionGuard(provider), koaSpaProxy()]));
|
||||
|
||||
const { isHttpsEnabled, httpsCert, httpsKey, port } = envSet.values;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export enum MountedApps {
|
|||
Oidc = 'oidc',
|
||||
Console = 'console',
|
||||
DemoApp = 'demo-app',
|
||||
Welcome = 'welcome',
|
||||
}
|
||||
|
||||
const loadEnvValues = async () => {
|
||||
|
|
29
packages/core/src/middleware/koa-root-proxy.test.ts
Normal file
29
packages/core/src/middleware/koa-root-proxy.test.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
import koaRootProxy from './koa-root-proxy';
|
||||
|
||||
describe('koaRootProxy', () => {
|
||||
const next = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('empty path should directly return', async () => {
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: '/',
|
||||
});
|
||||
|
||||
await koaRootProxy()(ctx, next);
|
||||
|
||||
expect(next).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('non-empty path should return next', async () => {
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: '/console',
|
||||
});
|
||||
await koaRootProxy()(ctx, next);
|
||||
expect(next).toBeCalled();
|
||||
});
|
||||
});
|
21
packages/core/src/middleware/koa-root-proxy.ts
Normal file
21
packages/core/src/middleware/koa-root-proxy.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { MiddlewareType } from 'koa';
|
||||
import { IRouterParamContext } from 'koa-router';
|
||||
|
||||
export default function koaRootProxy<
|
||||
StateT,
|
||||
ContextT extends IRouterParamContext,
|
||||
ResponseBodyT
|
||||
>(): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
const requestPath = ctx.request.path;
|
||||
|
||||
// Empty path return 404
|
||||
if (requestPath === '/') {
|
||||
ctx.throw(404);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
|
@ -3,7 +3,7 @@ import { Provider } from 'oidc-provider';
|
|||
import { MountedApps } from '@/env-set';
|
||||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
import koaProxyGuard, { sessionNotFoundPath, guardedPath } from './koa-proxy-guard';
|
||||
import koaSpaSessionGuard, { sessionNotFoundPath, guardedPath } from './koa-spa-session-guard';
|
||||
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
|
@ -16,7 +16,7 @@ jest.mock('oidc-provider', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
describe('koaProxyGuard', () => {
|
||||
describe('koaSpaSessionGuard', () => {
|
||||
const envBackup = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -38,7 +38,7 @@ describe('koaProxyGuard', () => {
|
|||
url: `/${app}/foo`,
|
||||
});
|
||||
|
||||
await koaProxyGuard(provider)(ctx, next);
|
||||
await koaSpaSessionGuard(provider)(ctx, next);
|
||||
|
||||
expect(ctx.redirect).not.toBeCalled();
|
||||
});
|
||||
|
@ -51,7 +51,7 @@ describe('koaProxyGuard', () => {
|
|||
const ctx = createContextWithRouteParameters({
|
||||
url: `${sessionNotFoundPath}`,
|
||||
});
|
||||
await koaProxyGuard(provider)(ctx, next);
|
||||
await koaSpaSessionGuard(provider)(ctx, next);
|
||||
expect(ctx.redirect).not.toBeCalled();
|
||||
});
|
||||
|
||||
|
@ -62,7 +62,7 @@ describe('koaProxyGuard', () => {
|
|||
const ctx = createContextWithRouteParameters({
|
||||
url: '/callback/github',
|
||||
});
|
||||
await koaProxyGuard(provider)(ctx, next);
|
||||
await koaSpaSessionGuard(provider)(ctx, next);
|
||||
expect(ctx.redirect).not.toBeCalled();
|
||||
});
|
||||
|
||||
|
@ -71,7 +71,7 @@ describe('koaProxyGuard', () => {
|
|||
const ctx = createContextWithRouteParameters({
|
||||
url: `/sign-in`,
|
||||
});
|
||||
await koaProxyGuard(provider)(ctx, next);
|
||||
await koaSpaSessionGuard(provider)(ctx, next);
|
||||
expect(ctx.redirect).not.toBeCalled();
|
||||
});
|
||||
|
||||
|
@ -84,7 +84,7 @@ describe('koaProxyGuard', () => {
|
|||
const ctx = createContextWithRouteParameters({
|
||||
url: `${path}/foo`,
|
||||
});
|
||||
await koaProxyGuard(provider)(ctx, next);
|
||||
await koaSpaSessionGuard(provider)(ctx, next);
|
||||
expect(ctx.redirect).toBeCalled();
|
||||
});
|
||||
}
|
|
@ -2,8 +2,6 @@ import { MiddlewareType } from 'koa';
|
|||
import { IRouterParamContext } from 'koa-router';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { MountedApps } from '@/env-set';
|
||||
|
||||
// Need To Align With UI
|
||||
export const sessionNotFoundPath = '/unknown-session';
|
||||
export const guardedPath = ['/sign-in', '/register', '/social-register'];
|
||||
|
@ -15,15 +13,6 @@ export default function koaSpaSessionGuard<
|
|||
>(provider: Provider): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
const requestPath = ctx.request.path;
|
||||
|
||||
// Empty path Redirect
|
||||
if (requestPath === '/') {
|
||||
ctx.redirect(`/${MountedApps.Console}`);
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
// Session guard
|
||||
const isPreview = ctx.request.URL.searchParams.get('preview');
|
||||
const isSessionRequiredPath = guardedPath.some((path) => requestPath.startsWith(path));
|
||||
|
||||
|
@ -32,6 +21,8 @@ export default function koaSpaSessionGuard<
|
|||
await provider.interactionDetails(ctx.req, ctx.res);
|
||||
} catch {
|
||||
ctx.redirect(sessionNotFoundPath);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
41
packages/core/src/middleware/koa-welcome-proxy.test.ts
Normal file
41
packages/core/src/middleware/koa-welcome-proxy.test.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { MountedApps } from '@/env-set';
|
||||
import { hasActiveUsers } from '@/queries/user';
|
||||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
import koaWelcomeProxy from './koa-welcome-proxy';
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
hasActiveUsers: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('koaWelcomeProxy', () => {
|
||||
const next = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should redirect to admin console if has AdminUsers', async () => {
|
||||
(hasActiveUsers as jest.Mock).mockResolvedValue(true);
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${MountedApps.Welcome}`,
|
||||
});
|
||||
|
||||
await koaWelcomeProxy()(ctx, next);
|
||||
|
||||
expect(ctx.redirect).toBeCalledWith(`/${MountedApps.Console}`);
|
||||
expect(next).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should redirect to register if has no AdminUsers', async () => {
|
||||
(hasActiveUsers as jest.Mock).mockResolvedValue(false);
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${MountedApps.Welcome}`,
|
||||
});
|
||||
|
||||
await koaWelcomeProxy()(ctx, next);
|
||||
expect(ctx.redirect).toBeCalledWith(`/${MountedApps.Console}/register`);
|
||||
expect(next).not.toBeCalled();
|
||||
});
|
||||
});
|
20
packages/core/src/middleware/koa-welcome-proxy.ts
Normal file
20
packages/core/src/middleware/koa-welcome-proxy.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { MiddlewareType } from 'koa';
|
||||
import { IRouterParamContext } from 'koa-router';
|
||||
|
||||
import { hasActiveUsers } from '@/queries/user';
|
||||
|
||||
export default function koaWelcomeProxy<
|
||||
StateT,
|
||||
ContextT extends IRouterParamContext,
|
||||
ResponseBodyT
|
||||
>(): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
return async (ctx) => {
|
||||
if (await hasActiveUsers()) {
|
||||
ctx.redirect('/console');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.redirect('/console/register');
|
||||
};
|
||||
}
|
|
@ -147,3 +147,10 @@ export const deleteUserIdentity = async (userId: string, connectorId: string) =>
|
|||
where ${fields.id}=${userId}
|
||||
returning *
|
||||
`);
|
||||
|
||||
export const hasActiveUsers = async () =>
|
||||
envSet.pool.exists(sql`
|
||||
select ${fields.id}
|
||||
from ${table}
|
||||
limit 1
|
||||
`);
|
||||
|
|
Loading…
Add table
Reference in a new issue