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" />}>
|
<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 />} />
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
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 { 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();
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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}
|
where ${fields.id}=${userId}
|
||||||
returning *
|
returning *
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const hasActiveUsers = async () =>
|
||||||
|
envSet.pool.exists(sql`
|
||||||
|
select ${fields.id}
|
||||||
|
from ${table}
|
||||||
|
limit 1
|
||||||
|
`);
|
||||||
|
|
Loading…
Add table
Reference in a new issue