mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
Merge pull request #3030 from logto-io/gao-separate-console-server
refactor(core)!: separate admin console server
This commit is contained in:
commit
f3fb0ec2a4
14 changed files with 172 additions and 226 deletions
2
.github/workflows/integration-test.yml
vendored
2
.github/workflows/integration-test.yml
vendored
|
@ -101,5 +101,3 @@ jobs:
|
|||
cd tests/packages/integration-tests
|
||||
pnpm build
|
||||
pnpm test:${{ matrix.test_target }}
|
||||
env:
|
||||
INTEGRATION_TESTS_LOGTO_URL: http://localhost:3001
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import fs from 'fs/promises';
|
||||
import http2 from 'http2';
|
||||
|
||||
import { deduplicate } from '@silverhand/essentials';
|
||||
import { toTitle } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import type Koa from 'koa';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { defaultTenant, tenantPool } from '#src/tenants/index.js';
|
||||
|
||||
const logListening = () => {
|
||||
const { localhostUrl, endpoint } = EnvSet.values;
|
||||
const logListening = (type: 'core' | 'admin' = 'core') => {
|
||||
const urlSet = type === 'core' ? EnvSet.values.urlSet : EnvSet.values.adminUrlSet;
|
||||
|
||||
for (const url of deduplicate([localhostUrl, endpoint.toString()])) {
|
||||
console.log(chalk.bold(chalk.green(`App is running at ${url}`)));
|
||||
for (const url of urlSet.deduplicated()) {
|
||||
console.log(chalk.bold(chalk.green(`${toTitle(type)} app is running at ${url}`)));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -43,23 +43,40 @@ export default async function initApp(app: Koa): Promise<void> {
|
|||
return tenant.run(ctx, next);
|
||||
});
|
||||
|
||||
const { isHttpsEnabled, httpsCert, httpsKey, port } = EnvSet.values;
|
||||
const { isHttpsEnabled, httpsCert, httpsKey, urlSet, adminUrlSet } = EnvSet.values;
|
||||
|
||||
if (isHttpsEnabled && httpsCert && httpsKey) {
|
||||
http2
|
||||
.createSecureServer(
|
||||
const createHttp2Server = async () =>
|
||||
http2.createSecureServer(
|
||||
{ cert: await fs.readFile(httpsCert), key: await fs.readFile(httpsKey) },
|
||||
app.callback()
|
||||
)
|
||||
.listen(port, () => {
|
||||
logListening();
|
||||
);
|
||||
|
||||
const coreServer = await createHttp2Server();
|
||||
coreServer.listen(urlSet.port, () => {
|
||||
logListening();
|
||||
});
|
||||
|
||||
// Create another server if admin localhost enabled
|
||||
if (!adminUrlSet.isLocalhostDisabled) {
|
||||
const adminServer = await createHttp2Server();
|
||||
adminServer.listen(adminUrlSet.port, () => {
|
||||
logListening('admin');
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Chrome doesn't allow insecure http/2 servers
|
||||
app.listen(port, () => {
|
||||
// Chrome doesn't allow insecure HTTP/2 servers, stick with HTTP for localhost.
|
||||
app.listen(urlSet.port, () => {
|
||||
logListening();
|
||||
});
|
||||
|
||||
// Create another server if admin localhost enabled
|
||||
if (!adminUrlSet.isLocalhostDisabled) {
|
||||
app.listen(adminUrlSet.port, () => {
|
||||
logListening('admin');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
52
packages/core/src/env-set/GlobalValues.ts
Normal file
52
packages/core/src/env-set/GlobalValues.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import net from 'net';
|
||||
|
||||
import { tryThat } from '@logto/shared';
|
||||
import { assertEnv, getEnv, getEnvAsStringArray } from '@silverhand/essentials';
|
||||
|
||||
import UrlSet from './UrlSet.js';
|
||||
import { isTrue } from './parameters.js';
|
||||
import { throwErrorWithDsnMessage } from './throw-errors.js';
|
||||
|
||||
const enableMultiTenancyKey = 'ENABLE_MULTI_TENANCY';
|
||||
const developmentTenantIdKey = 'DEVELOPMENT_TENANT_ID';
|
||||
|
||||
type MultiTenancyMode = 'domain' | 'env';
|
||||
|
||||
export default class GlobalValues {
|
||||
public readonly isProduction = getEnv('NODE_ENV') === 'production';
|
||||
public readonly isTest = getEnv('NODE_ENV') === 'test';
|
||||
public readonly isIntegrationTest = isTrue(getEnv('INTEGRATION_TEST'));
|
||||
|
||||
public readonly httpsCert = process.env.HTTPS_CERT_PATH;
|
||||
public readonly httpsKey = process.env.HTTPS_KEY_PATH;
|
||||
public readonly isHttpsEnabled = Boolean(this.httpsCert && this.httpsKey);
|
||||
|
||||
public readonly isMultiTenancy = isTrue(getEnv(enableMultiTenancyKey));
|
||||
|
||||
public readonly urlSet = new UrlSet(this.isHttpsEnabled, 3001);
|
||||
public readonly adminUrlSet = new UrlSet(this.isHttpsEnabled, 3002, 'ADMIN_');
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage);
|
||||
public readonly developmentTenantId = getEnv(developmentTenantIdKey);
|
||||
public readonly userDefaultRoleNames = getEnvAsStringArray('USER_DEFAULT_ROLE_NAMES');
|
||||
public readonly developmentUserId = getEnv('DEVELOPMENT_USER_ID');
|
||||
public readonly trustProxyHeader = isTrue(getEnv('TRUST_PROXY_HEADER'));
|
||||
public readonly ignoreConnectorVersionCheck = isTrue(getEnv('IGNORE_CONNECTOR_VERSION_CHECK'));
|
||||
|
||||
public get dbUrl(): string {
|
||||
return this.databaseUrl;
|
||||
}
|
||||
|
||||
public get endpoint(): string {
|
||||
return this.urlSet.endpoint;
|
||||
}
|
||||
|
||||
public get multiTenancyMode(): MultiTenancyMode {
|
||||
const { hostname } = new URL(this.endpoint);
|
||||
|
||||
return this.isMultiTenancy && !net.isIP(hostname) && hostname !== 'localhost'
|
||||
? 'domain'
|
||||
: 'env';
|
||||
}
|
||||
}
|
48
packages/core/src/env-set/UrlSet.ts
Normal file
48
packages/core/src/env-set/UrlSet.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { deduplicate, getEnv, trySafe } from '@silverhand/essentials';
|
||||
|
||||
import { isTrue } from './parameters.js';
|
||||
|
||||
const localhostDisabledMessage = 'Localhost has been disabled in this URL Set.';
|
||||
|
||||
export default class UrlSet {
|
||||
readonly #port = Number(getEnv(this.envPrefix + 'PORT') || this.defaultPort);
|
||||
readonly #endpoint = getEnv(this.envPrefix + 'ENDPOINT');
|
||||
|
||||
public readonly isLocalhostDisabled = isTrue(getEnv(this.envPrefix + 'DISABLE_LOCALHOST'));
|
||||
|
||||
constructor(
|
||||
public readonly isHttpsEnabled: boolean,
|
||||
protected readonly defaultPort: number,
|
||||
protected readonly envPrefix: string = ''
|
||||
) {}
|
||||
|
||||
public deduplicated(): string[] {
|
||||
return deduplicate(
|
||||
[trySafe(() => this.localhostUrl), trySafe(() => this.endpoint)].filter(
|
||||
(value): value is string => typeof value === 'string'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public get port() {
|
||||
if (this.isLocalhostDisabled) {
|
||||
throw new Error(localhostDisabledMessage);
|
||||
}
|
||||
|
||||
return this.#port;
|
||||
}
|
||||
|
||||
public get localhostUrl() {
|
||||
return `${this.isHttpsEnabled ? 'https' : 'http'}://localhost:${this.port}`;
|
||||
}
|
||||
|
||||
public get endpoint() {
|
||||
const value = this.#endpoint || this.localhostUrl;
|
||||
|
||||
if (this.isLocalhostDisabled && new URL(value).hostname === 'localhost') {
|
||||
throw new Error(localhostDisabledMessage);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
import net from 'net';
|
||||
|
||||
import { tryThat } from '@logto/shared';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { assertEnv, getEnv, getEnvAsStringArray } from '@silverhand/essentials';
|
||||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import type { QueryClient } from '@withtyped/server';
|
||||
import type { DatabasePool } from 'slonik';
|
||||
|
@ -10,12 +6,12 @@ import type { DatabasePool } from 'slonik';
|
|||
import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
|
||||
import { appendPath } from '#src/utils/url.js';
|
||||
|
||||
import GlobalValues from './GlobalValues.js';
|
||||
import { checkAlterationState } from './check-alteration-state.js';
|
||||
import createPool from './create-pool.js';
|
||||
import createQueryClient from './create-query-client.js';
|
||||
import loadOidcValues from './oidc.js';
|
||||
import { isTrue } from './parameters.js';
|
||||
import { throwErrorWithDsnMessage, throwNotLoadedError } from './throw-errors.js';
|
||||
import { throwNotLoadedError } from './throw-errors.js';
|
||||
|
||||
export enum MountedApps {
|
||||
Api = 'api',
|
||||
|
@ -25,72 +21,8 @@ export enum MountedApps {
|
|||
Welcome = 'welcome',
|
||||
}
|
||||
|
||||
type MultiTenancyMode = 'domain' | 'env';
|
||||
|
||||
const enableMultiTenancyKey = 'ENABLE_MULTI_TENANCY';
|
||||
const developmentTenantIdKey = 'DEVELOPMENT_TENANT_ID';
|
||||
|
||||
const loadEnvValues = () => {
|
||||
const isProduction = getEnv('NODE_ENV') === 'production';
|
||||
const isTest = getEnv('NODE_ENV') === 'test';
|
||||
const isIntegrationTest = isTrue(getEnv('INTEGRATION_TEST'));
|
||||
const isHttpsEnabled = Boolean(process.env.HTTPS_CERT_PATH && process.env.HTTPS_KEY_PATH);
|
||||
const isMultiTenancy = isTrue(getEnv(enableMultiTenancyKey));
|
||||
const port = Number(getEnv('PORT', '3001'));
|
||||
const localhostUrl = `${isHttpsEnabled ? 'https' : 'http'}://localhost:${port}`;
|
||||
const endpoint = getEnv('ENDPOINT', localhostUrl);
|
||||
const databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage);
|
||||
|
||||
const { hostname } = new URL(endpoint);
|
||||
const multiTenancyMode: MultiTenancyMode =
|
||||
isMultiTenancy && !net.isIP(hostname) && hostname !== 'localhost' ? 'domain' : 'env';
|
||||
const developmentTenantId = getEnv(developmentTenantIdKey);
|
||||
|
||||
if (!isMultiTenancy && developmentTenantId) {
|
||||
throw new Error(
|
||||
`Multi-tenancy is disabled but development tenant env \`${developmentTenantIdKey}\` found. Please enable multi-tenancy by setting \`${enableMultiTenancyKey}\` to true.`
|
||||
);
|
||||
}
|
||||
|
||||
if (isMultiTenancy && multiTenancyMode === 'env') {
|
||||
if (isProduction) {
|
||||
throw new Error(
|
||||
`Multi-tenancy is enabled but the endpoint is an IP address: ${endpoint.toString()}.\n\n` +
|
||||
'An endpoint with a valid domain is required for multi-tenancy mode.'
|
||||
);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'[warn]',
|
||||
`Multi-tenancy is enabled but the endpoint is an IP address: ${endpoint.toString()}.\n\n` +
|
||||
`Logto is using \`${developmentTenantIdKey}\` env (current value: ${developmentTenantId}) for tenant recognition which is not supported in production.`
|
||||
);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
isTest,
|
||||
isIntegrationTest,
|
||||
isProduction,
|
||||
isHttpsEnabled,
|
||||
isMultiTenancy,
|
||||
httpsCert: process.env.HTTPS_CERT_PATH,
|
||||
httpsKey: process.env.HTTPS_KEY_PATH,
|
||||
port,
|
||||
localhostUrl,
|
||||
endpoint,
|
||||
multiTenancyMode,
|
||||
dbUrl: databaseUrl,
|
||||
userDefaultRoleNames: getEnvAsStringArray('USER_DEFAULT_ROLE_NAMES'),
|
||||
developmentUserId: getEnv('DEVELOPMENT_USER_ID'),
|
||||
developmentTenantId,
|
||||
trustProxyHeader: isTrue(getEnv('TRUST_PROXY_HEADER')),
|
||||
adminConsoleUrl: appendPath(endpoint, '/console'),
|
||||
ignoreConnectorVersionCheck: isTrue(getEnv('IGNORE_CONNECTOR_VERSION_CHECK')),
|
||||
});
|
||||
};
|
||||
|
||||
export class EnvSet {
|
||||
static values: ReturnType<typeof loadEnvValues> = loadEnvValues();
|
||||
static values = new GlobalValues();
|
||||
static default = new EnvSet(EnvSet.values.dbUrl);
|
||||
|
||||
static get isTest() {
|
||||
|
|
|
@ -5,8 +5,7 @@ import Koa from 'koa';
|
|||
|
||||
dotenv.config({ path: await findUp('.env', {}) });
|
||||
|
||||
// Import after env has configured
|
||||
|
||||
// Import after env has been configured
|
||||
const { loadConnectorFactories } = await import('./utils/connectors/factories.js');
|
||||
const { EnvSet } = await import('./env-set/index.js');
|
||||
const { default: initI18n } = await import('./i18n/init.js');
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
import type { MiddlewareType } from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import { appendPath } from '#src/utils/url.js';
|
||||
|
||||
export default function koaWelcomeProxy<
|
||||
export default function koaConsoleRedirectProxy<
|
||||
StateT,
|
||||
ContextT extends IRouterParamContext,
|
||||
ResponseBodyT
|
||||
>(queries: Queries): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
const { hasActiveUsers } = queries.users;
|
||||
const { adminConsoleUrl } = EnvSet.values;
|
||||
|
||||
return async (ctx) => {
|
||||
if (await hasActiveUsers()) {
|
||||
ctx.redirect(adminConsoleUrl.toString());
|
||||
return async (ctx, next) => {
|
||||
const hasUser = await hasActiveUsers();
|
||||
|
||||
if ((ctx.path === '/' || ctx.path === '/console') && !hasUser) {
|
||||
ctx.redirect('/console/welcome');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.redirect(appendPath(adminConsoleUrl, '/welcome').toString());
|
||||
if (ctx.path === '/console/welcome' && hasUser) {
|
||||
ctx.redirect('/console');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import koaRootProxy from './koa-root-proxy.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import type { MiddlewareType } from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { appendPath } from '#src/utils/url.js';
|
||||
|
||||
export default function koaRootProxy<
|
||||
StateT,
|
||||
ContextT extends IRouterParamContext,
|
||||
ResponseBodyT
|
||||
>(): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
const { endpoint } = EnvSet.values;
|
||||
|
||||
return async (ctx, next) => {
|
||||
const requestPath = ctx.request.path;
|
||||
|
||||
// Redirect root path to the Admin Console welcome page
|
||||
if (requestPath === '/') {
|
||||
ctx.redirect(appendPath(endpoint, '/welcome').toString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { EnvSet, MountedApps } from '#src/env-set/index.js';
|
||||
import { MockQueries } from '#src/test-utils/tenant.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const hasActiveUsers = jest.fn();
|
||||
const queries = new MockQueries({ users: { hasActiveUsers } });
|
||||
|
||||
const koaWelcomeProxy = await pickDefault(import('./koa-welcome-proxy.js'));
|
||||
|
||||
describe('koaWelcomeProxy', () => {
|
||||
const next = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should redirect to admin console if has AdminUsers', async () => {
|
||||
const { endpoint } = EnvSet.values;
|
||||
hasActiveUsers.mockResolvedValue(true);
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${MountedApps.Welcome}`,
|
||||
});
|
||||
|
||||
await koaWelcomeProxy(queries)(ctx, next);
|
||||
|
||||
expect(ctx.redirect).toBeCalledWith(`${endpoint}/${MountedApps.Console}`);
|
||||
expect(next).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should redirect to welcome page if has no Users', async () => {
|
||||
const { endpoint } = EnvSet.values;
|
||||
hasActiveUsers.mockResolvedValue(false);
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${MountedApps.Welcome}`,
|
||||
});
|
||||
|
||||
await koaWelcomeProxy(queries)(ctx, next);
|
||||
expect(ctx.redirect).toBeCalledWith(`${endpoint}/${MountedApps.Console}/welcome`);
|
||||
expect(next).not.toBeCalled();
|
||||
});
|
||||
});
|
|
@ -14,11 +14,8 @@ import { appendPath } from '#src/utils/url.js';
|
|||
import { getConstantClientMetadata } from './utils.js';
|
||||
|
||||
const buildAdminConsoleClientMetadata = (envSet: EnvSet): AllClientMetadata => {
|
||||
const { localhostUrl, adminConsoleUrl } = EnvSet.values;
|
||||
const urls = deduplicate([
|
||||
appendPath(localhostUrl, '/console').toString(),
|
||||
adminConsoleUrl.toString(),
|
||||
]);
|
||||
const { adminUrlSet } = EnvSet.values;
|
||||
const urls = adminUrlSet.deduplicated().map((url) => appendPath(url, '/console').toString());
|
||||
|
||||
return {
|
||||
...getConstantClientMetadata(envSet, ApplicationType.SPA),
|
||||
|
@ -32,11 +29,8 @@ const buildAdminConsoleClientMetadata = (envSet: EnvSet): AllClientMetadata => {
|
|||
const buildDemoAppUris = (
|
||||
oidcClientMetadata: OidcClientMetadata
|
||||
): Pick<OidcClientMetadata, 'redirectUris' | 'postLogoutRedirectUris'> => {
|
||||
const { localhostUrl, endpoint } = EnvSet.values;
|
||||
const urls = [
|
||||
appendPath(localhostUrl, MountedApps.DemoApp).toString(),
|
||||
appendPath(endpoint, MountedApps.DemoApp).toString(),
|
||||
];
|
||||
const { urlSet } = EnvSet.values;
|
||||
const urls = urlSet.deduplicated().map((url) => appendPath(url, MountedApps.DemoApp).toString());
|
||||
|
||||
const data = {
|
||||
redirectUris: deduplicate([...urls, ...oidcClientMetadata.redirectUris]),
|
||||
|
|
|
@ -8,14 +8,13 @@ import type Provider from 'oidc-provider';
|
|||
import { EnvSet, MountedApps } from '#src/env-set/index.js';
|
||||
import koaCheckDemoApp from '#src/middleware/koa-check-demo-app.js';
|
||||
import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js';
|
||||
import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js';
|
||||
import koaErrorHandler from '#src/middleware/koa-error-handler.js';
|
||||
import koaI18next from '#src/middleware/koa-i18next.js';
|
||||
import koaOIDCErrorHandler from '#src/middleware/koa-oidc-error-handler.js';
|
||||
import koaRootProxy from '#src/middleware/koa-root-proxy.js';
|
||||
import koaSlonikErrorHandler from '#src/middleware/koa-slonik-error-handler.js';
|
||||
import koaSpaProxy from '#src/middleware/koa-spa-proxy.js';
|
||||
import koaSpaSessionGuard from '#src/middleware/koa-spa-session-guard.js';
|
||||
import koaWelcomeProxy from '#src/middleware/koa-welcome-proxy.js';
|
||||
import type { ModelRouters } from '#src/model-routers/index.js';
|
||||
import { createModelRouters } from '#src/model-routers/index.js';
|
||||
import initOidc from '#src/oidc/init.js';
|
||||
|
@ -80,17 +79,17 @@ export default class Tenant implements TenantContext {
|
|||
app.use(koaConnectorErrorHandler());
|
||||
app.use(koaI18next());
|
||||
|
||||
// Mount APIs
|
||||
const apisApp = initRouter({ provider, queries, libraries, modelRouters, envSet });
|
||||
app.use(mount('/api', apisApp));
|
||||
|
||||
app.use(mount('/', koaRootProxy()));
|
||||
|
||||
app.use(mount('/' + MountedApps.Welcome, koaWelcomeProxy(queries)));
|
||||
|
||||
// Mount Admin Console
|
||||
app.use(koaConsoleRedirectProxy(queries));
|
||||
app.use(
|
||||
mount('/' + MountedApps.Console, koaSpaProxy(MountedApps.Console, 5002, MountedApps.Console))
|
||||
);
|
||||
|
||||
// Mount demo app
|
||||
app.use(
|
||||
mount(
|
||||
'/' + MountedApps.DemoApp,
|
||||
|
@ -101,6 +100,7 @@ export default class Tenant implements TenantContext {
|
|||
)
|
||||
);
|
||||
|
||||
// Mount UI
|
||||
app.use(compose([koaSpaSessionGuard(provider), koaSpaProxy()]));
|
||||
|
||||
this.app = app;
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { SignInIdentifier, demoAppApplicationId } from '@logto/schemas';
|
||||
import { assertEnv } from '@silverhand/essentials';
|
||||
import { getEnv } from '@silverhand/essentials';
|
||||
|
||||
export const logtoUrl = assertEnv('INTEGRATION_TESTS_LOGTO_URL');
|
||||
export const logtoUrl = getEnv('INTEGRATION_TESTS_LOGTO_URL', 'http://localhost:3001');
|
||||
export const logtoConsoleUrl = getEnv(
|
||||
'INTEGRATION_TESTS_LOGTO_CONSOLE_URL',
|
||||
'http://localhost:3002'
|
||||
);
|
||||
|
||||
export const discoveryUrl = `${logtoUrl}/oidc/.well-known/openid-configuration`;
|
||||
|
||||
export const demoAppRedirectUri = `${logtoUrl}/${demoAppApplicationId}`;
|
||||
export const adminConsoleRedirectUri = `${logtoUrl}/console/callback`;
|
||||
export const adminConsoleRedirectUri = `${logtoConsoleUrl}/console/callback`;
|
||||
|
||||
export const signUpIdentifiers = {
|
||||
username: [SignInIdentifier.Username],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { logtoUrl } from '#src/constants.js';
|
||||
import { logtoConsoleUrl } from '#src/constants.js';
|
||||
import { generatePassword } from '#src/utils.js';
|
||||
|
||||
describe('smoke testing', () => {
|
||||
|
@ -7,11 +7,11 @@ describe('smoke testing', () => {
|
|||
|
||||
it('opens with app element and navigates to welcome page', async () => {
|
||||
const navigation = page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||
await page.goto(logtoUrl);
|
||||
await page.goto(logtoConsoleUrl);
|
||||
await navigation;
|
||||
|
||||
await expect(page.waitForSelector('#app')).resolves.not.toBeNull();
|
||||
expect(page.url()).toBe(new URL('console/welcome', logtoUrl).href);
|
||||
expect(page.url()).toBe(new URL('console/welcome', logtoConsoleUrl).href);
|
||||
});
|
||||
|
||||
it('registers a new admin account and automatically signs in', async () => {
|
||||
|
@ -22,7 +22,7 @@ describe('smoke testing', () => {
|
|||
await createAccountButton.click();
|
||||
await navigateToRegister;
|
||||
|
||||
expect(page.url()).toBe(new URL('register', logtoUrl).href);
|
||||
expect(page.url()).toBe(new URL('register', logtoConsoleUrl).href);
|
||||
|
||||
const usernameField = await page.waitForSelector('input[name=new-username]');
|
||||
const submitButton = await page.waitForSelector('button');
|
||||
|
@ -33,7 +33,7 @@ describe('smoke testing', () => {
|
|||
await submitButton.click();
|
||||
await navigateToSignIn;
|
||||
|
||||
expect(page.url()).toBe(new URL('register/username/password', logtoUrl).href);
|
||||
expect(page.url()).toBe(new URL('register/username/password', logtoConsoleUrl).href);
|
||||
|
||||
const passwordField = await page.waitForSelector('input[name=newPassword]');
|
||||
const confirmPasswordField = await page.waitForSelector('input[name=confirmPassword]');
|
||||
|
@ -45,7 +45,7 @@ describe('smoke testing', () => {
|
|||
await saveButton.click();
|
||||
await navigateToGetStarted;
|
||||
|
||||
expect(page.url()).toBe(new URL('console/get-started', logtoUrl).href);
|
||||
expect(page.url()).toBe(new URL('console/get-started', logtoConsoleUrl).href);
|
||||
});
|
||||
|
||||
it('signs out of admin console', async () => {
|
||||
|
@ -62,7 +62,7 @@ describe('smoke testing', () => {
|
|||
await signOutButton.click();
|
||||
await navigation;
|
||||
|
||||
expect(page.url()).toBe(new URL('sign-in', logtoUrl).href);
|
||||
expect(page.url()).toBe(new URL('sign-in', logtoConsoleUrl).href);
|
||||
});
|
||||
|
||||
it('signs in to admin console', async () => {
|
||||
|
@ -77,7 +77,7 @@ describe('smoke testing', () => {
|
|||
await submitButton.click();
|
||||
await navigation;
|
||||
|
||||
expect(page.url()).toBe(new URL('console/get-started', logtoUrl).href);
|
||||
expect(page.url()).toBe(new URL('console/get-started', logtoConsoleUrl).href);
|
||||
|
||||
const userElement = await page.waitForSelector('div[class$=topbar] > div:last-child');
|
||||
const usernameString = await userElement.$eval('div > div', (element) => element.textContent);
|
||||
|
|
Loading…
Add table
Reference in a new issue