0
Fork 0
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:
simeng-li 2022-06-09 11:42:52 +08:00 committed by GitHub
parent dc7f9ccdb6
commit f6f562a8ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 139 additions and 21 deletions

View file

@ -46,6 +46,8 @@ const Main = () => {
<Suspense fallback={<LogtoLoading message="general.loading" />}> <Suspense fallback={<LogtoLoading message="general.loading" />}>
<Routes> <Routes>
<Route path="callback" element={<Callback />} /> <Route path="callback" element={<Callback />} />
{/* TODO: add register route */}
<Route path="register" element={<div>register</div>} />
<Route element={<AppContent />}> <Route element={<AppContent />}>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
<Route path="get-started" element={<GetStarted />} /> <Route path="get-started" element={<GetStarted />} />

View file

@ -14,9 +14,11 @@ import koaErrorHandler from '@/middleware/koa-error-handler';
import koaI18next from '@/middleware/koa-i18next'; import koaI18next from '@/middleware/koa-i18next';
import koaLog from '@/middleware/koa-log'; import koaLog from '@/middleware/koa-log';
import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler'; 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 koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
import koaSpaProxy from '@/middleware/koa-spa-proxy'; 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 initOidc from '@/oidc/init';
import initRouter from '@/routes/init'; import initRouter from '@/routes/init';
@ -37,9 +39,14 @@ export default async function initApp(app: Koa): Promise<void> {
const provider = await initOidc(app); const provider = await initOidc(app);
initRouter(app, provider); initRouter(app, provider);
app.use(mount('/', koaRootProxy()));
app.use(mount('/' + MountedApps.Welcome, koaWelcomeProxy()));
app.use( app.use(
mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console)) mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console))
); );
app.use( app.use(
mount( mount(
'/' + MountedApps.DemoApp, '/' + MountedApps.DemoApp,
@ -47,8 +54,7 @@ export default async function initApp(app: Koa): Promise<void> {
) )
); );
app.use(koaProxyGuard(provider)); app.use(compose([koaSpaSessionGuard(provider), koaSpaProxy()]));
app.use(koaSpaProxy());
const { isHttpsEnabled, httpsCert, httpsKey, port } = envSet.values; const { isHttpsEnabled, httpsCert, httpsKey, port } = envSet.values;

View file

@ -9,6 +9,7 @@ export enum MountedApps {
Oidc = 'oidc', Oidc = 'oidc',
Console = 'console', Console = 'console',
DemoApp = 'demo-app', DemoApp = 'demo-app',
Welcome = 'welcome',
} }
const loadEnvValues = async () => { const loadEnvValues = async () => {

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

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

View file

@ -3,7 +3,7 @@ import { Provider } from 'oidc-provider';
import { MountedApps } from '@/env-set'; import { MountedApps } from '@/env-set';
import { createContextWithRouteParameters } from '@/utils/test-utils'; 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.mock('fs/promises', () => ({
...jest.requireActual('fs/promises'), ...jest.requireActual('fs/promises'),
@ -16,7 +16,7 @@ jest.mock('oidc-provider', () => ({
})), })),
})); }));
describe('koaProxyGuard', () => { describe('koaSpaSessionGuard', () => {
const envBackup = process.env; const envBackup = process.env;
beforeEach(() => { beforeEach(() => {
@ -38,7 +38,7 @@ describe('koaProxyGuard', () => {
url: `/${app}/foo`, url: `/${app}/foo`,
}); });
await koaProxyGuard(provider)(ctx, next); await koaSpaSessionGuard(provider)(ctx, next);
expect(ctx.redirect).not.toBeCalled(); expect(ctx.redirect).not.toBeCalled();
}); });
@ -51,7 +51,7 @@ describe('koaProxyGuard', () => {
const ctx = createContextWithRouteParameters({ const ctx = createContextWithRouteParameters({
url: `${sessionNotFoundPath}`, url: `${sessionNotFoundPath}`,
}); });
await koaProxyGuard(provider)(ctx, next); await koaSpaSessionGuard(provider)(ctx, next);
expect(ctx.redirect).not.toBeCalled(); expect(ctx.redirect).not.toBeCalled();
}); });
@ -62,7 +62,7 @@ describe('koaProxyGuard', () => {
const ctx = createContextWithRouteParameters({ const ctx = createContextWithRouteParameters({
url: '/callback/github', url: '/callback/github',
}); });
await koaProxyGuard(provider)(ctx, next); await koaSpaSessionGuard(provider)(ctx, next);
expect(ctx.redirect).not.toBeCalled(); expect(ctx.redirect).not.toBeCalled();
}); });
@ -71,7 +71,7 @@ describe('koaProxyGuard', () => {
const ctx = createContextWithRouteParameters({ const ctx = createContextWithRouteParameters({
url: `/sign-in`, url: `/sign-in`,
}); });
await koaProxyGuard(provider)(ctx, next); await koaSpaSessionGuard(provider)(ctx, next);
expect(ctx.redirect).not.toBeCalled(); expect(ctx.redirect).not.toBeCalled();
}); });
@ -84,7 +84,7 @@ describe('koaProxyGuard', () => {
const ctx = createContextWithRouteParameters({ const ctx = createContextWithRouteParameters({
url: `${path}/foo`, url: `${path}/foo`,
}); });
await koaProxyGuard(provider)(ctx, next); await koaSpaSessionGuard(provider)(ctx, next);
expect(ctx.redirect).toBeCalled(); expect(ctx.redirect).toBeCalled();
}); });
} }

View file

@ -2,8 +2,6 @@ import { MiddlewareType } from 'koa';
import { IRouterParamContext } from 'koa-router'; import { IRouterParamContext } from 'koa-router';
import { Provider } from 'oidc-provider'; import { Provider } from 'oidc-provider';
import { MountedApps } from '@/env-set';
// Need To Align With UI // Need To Align With UI
export const sessionNotFoundPath = '/unknown-session'; export const sessionNotFoundPath = '/unknown-session';
export const guardedPath = ['/sign-in', '/register', '/social-register']; export const guardedPath = ['/sign-in', '/register', '/social-register'];
@ -15,15 +13,6 @@ export default function koaSpaSessionGuard<
>(provider: Provider): MiddlewareType<StateT, ContextT, ResponseBodyT> { >(provider: Provider): MiddlewareType<StateT, ContextT, ResponseBodyT> {
return async (ctx, next) => { return async (ctx, next) => {
const requestPath = ctx.request.path; 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 isPreview = ctx.request.URL.searchParams.get('preview');
const isSessionRequiredPath = guardedPath.some((path) => requestPath.startsWith(path)); const isSessionRequiredPath = guardedPath.some((path) => requestPath.startsWith(path));
@ -32,6 +21,8 @@ export default function koaSpaSessionGuard<
await provider.interactionDetails(ctx.req, ctx.res); await provider.interactionDetails(ctx.req, ctx.res);
} catch { } catch {
ctx.redirect(sessionNotFoundPath); ctx.redirect(sessionNotFoundPath);
return;
} }
} }

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

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

View file

@ -147,3 +147,10 @@ export const deleteUserIdentity = async (userId: string, connectorId: string) =>
where ${fields.id}=${userId} where ${fields.id}=${userId}
returning * returning *
`); `);
export const hasActiveUsers = async () =>
envSet.pool.exists(sql`
select ${fields.id}
from ${table}
limit 1
`);