0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

refactor(core): use tenant context for route inits

This commit is contained in:
Gao Sun 2023-01-09 16:58:02 +08:00
parent a68b34971a
commit 26f8511f93
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
41 changed files with 209 additions and 141 deletions

View file

@ -116,7 +116,17 @@
],
"default-case": "off",
"import/extensions": "off"
}
},
"overrides": [
{
"files": [
"*.test.ts"
],
"rules": {
"@typescript-eslint/ban-ts-comment": "off"
}
}
]
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}

View file

@ -6,7 +6,7 @@ import chalk from 'chalk';
import type Koa from 'koa';
import envSet from '#src/env-set/index.js';
import { tenantPool } from '#src/tenants/index.js';
import { tenantPool, defaultTenant } from '#src/tenants/index.js';
const logListening = () => {
const { localhostUrl, endpoint } = envSet.values;
@ -16,8 +16,6 @@ const logListening = () => {
}
};
const defaultTenant = 'default';
export default async function initApp(app: Koa): Promise<void> {
app.use(async (ctx, next) => {
// TODO: add multi-tenancy logic

View file

@ -1,10 +1,7 @@
import Koa from 'koa';
import initOidc from './init.js';
describe('oidc provider init', () => {
it('init should not throw', async () => {
const app = new Koa();
expect(() => initOidc(app)).not.toThrow();
expect(() => initOidc()).not.toThrow();
});
});

View file

@ -5,8 +5,6 @@ import { readFileSync } from 'fs';
import { userClaims } from '@logto/core-kit';
import { CustomClientMetadataKey } from '@logto/schemas';
import { tryThat } from '@logto/shared';
import type Koa from 'koa';
import mount from 'koa-mount';
import { Provider, errors } from 'oidc-provider';
import snakecaseKeys from 'snakecase-keys';
@ -23,7 +21,7 @@ import assertThat from '#src/utils/assert-that.js';
import { claimToUserKey, getUserClaims } from './scope.js';
export default function initOidc(app: Koa): Provider {
export default function initOidc(): Provider {
const { issuer, cookieKeys, privateJwks, defaultIdTokenTtl, defaultRefreshTokenTtl } =
envSet.oidc;
const logoutSource = readFileSync('static/html/logout.html', 'utf8');
@ -33,6 +31,7 @@ export default function initOidc(app: Koa): Provider {
path: '/',
signed: true,
} as const);
const oidc = new Provider(issuer, {
adapter: postgresAdapter,
renderError: (_ctx, _out, error) => {
@ -192,7 +191,5 @@ export default function initOidc(app: Koa): Provider {
// Provide audit log context for event listeners
oidc.use(koaAuditLog());
app.use(mount('/oidc', oidc.app));
return oidc;
}

View file

@ -11,7 +11,7 @@ import { DeletionError } from '#src/errors/SlonikError/index.js';
const { table, fields } = convertToIdentifiers(Passcodes);
const createPasscodeQueries = (pool: CommonQueryMethods) => {
export const createPasscodeQueries = (pool: CommonQueryMethods) => {
const findUnconsumedPasscodeByJtiAndType = async (jti: string, type: VerificationCodeType) =>
pool.maybeOne<Passcode>(sql`
select ${sql.join(Object.values(fields), sql`, `)}

View file

@ -16,7 +16,7 @@ import { findUsersRolesByRoleId, findUsersRolesByUserId } from './users-roles.js
const { table, fields } = convertToIdentifiers(Users);
const createUserQueries = (pool: CommonQueryMethods) => {
export const createUserQueries = (pool: CommonQueryMethods) => {
const findUserByUsername = async (username: string) =>
pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}

View file

@ -11,9 +11,11 @@ import {
} from '#src/queries/users-roles.js';
import assertThat from '#src/utils/assert-that.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function adminUserRoleRoutes<T extends AuthedRouter>(router: T) {
export default function adminUserRoleRoutes<T extends AuthedRouter>(
...[router]: RouterInitArgs<T>
) {
router.get(
'/users/:userId/roles',
koaGuard({

View file

@ -36,9 +36,9 @@ import {
import assertThat from '#src/utils/assert-that.js';
import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
export default function adminUserRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get('/users', koaPagination(), async (ctx, next) => {
const { limit, offset } = ctx.pagination;
const { searchParams } = ctx.request.URL;

View file

@ -14,11 +14,11 @@ import {
findTotalNumberOfApplications,
} from '#src/queries/application.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
const applicationId = buildIdGenerator(21);
export default function applicationRoutes<T extends AuthedRouter>(router: T) {
export default function applicationRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get('/applications', koaPagination(), async (ctx, next) => {
const { limit, offset } = ctx.pagination;

View file

@ -5,14 +5,14 @@ import { verifyBearerTokenFromRequest } from '#src/middleware/koa-auth.js';
import koaGuard from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.js';
import type { AnonymousRouter } from './types.js';
import type { AnonymousRouter, RouterInitArgs } from './types.js';
/**
* Authn stands for authentication.
* This router will have a route `/authn` to authenticate tokens with a general manner.
* For now, we only implement the API for Hasura authentication.
*/
export default function authnRoutes<T extends AnonymousRouter>(router: T) {
export default function authnRoutes<T extends AnonymousRouter>(...[router]: RouterInitArgs<T>) {
router.get(
'/authn/hasura',
koaGuard({

View file

@ -25,7 +25,7 @@ import {
} from '#src/queries/connector.js';
import assertThat from '#src/utils/assert-that.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
const transpileLogtoConnector = ({
dbEntry,
@ -39,7 +39,7 @@ const transpileLogtoConnector = ({
const generateConnectorId = buildIdGenerator(12);
export default function connectorRoutes<T extends AuthedRouter>(router: T) {
export default function connectorRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get(
'/connectors',
koaGuard({

View file

@ -17,14 +17,14 @@ import { findDefaultSignInExperience } from '#src/queries/sign-in-experience.js'
import assertThat from '#src/utils/assert-that.js';
import { isStrictlyPartial } from '#src/utils/translation.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
const cleanDeepTranslation = (translation: Translation) =>
// Since `Translation` type actually equals `Partial<Translation>`, force to cast it back to `Translation`.
// eslint-disable-next-line no-restricted-syntax
cleanDeep(translation) as Translation;
export default function customPhraseRoutes<T extends AuthedRouter>(router: T) {
export default function customPhraseRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get(
'/custom-phrases',
koaGuard({

View file

@ -9,7 +9,7 @@ import {
} from '#src/queries/log.js';
import { countUsers, getDailyNewUserCountsByTimeInterval } from '#src/queries/user.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
const getDateString = (date: Date | number) => format(date, 'yyyy-MM-dd');
@ -17,7 +17,7 @@ const indices = (length: number) => [...Array.from({ length }).keys()];
const getEndOfDayTimestamp = (date: Date | number) => endOfDay(date).valueOf();
export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
export default function dashboardRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get('/dashboard/users/total', async (ctx, next) => {
const { count: totalUserCount } = await countUsers();
ctx.body = { totalUserCount };

View file

@ -5,7 +5,7 @@ import koaBody from 'koa-body';
import LogtoRequestError from '#src/errors/RequestError/index.js';
import modelRouters from '#src/model-routers/index.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
// Organize this function if we decide to adopt withtyped eventually
const errorHandler: MiddlewareType = async (_, next) => {
@ -23,6 +23,6 @@ const errorHandler: MiddlewareType = async (_, next) => {
}
};
export default function hookRoutes<T extends AuthedRouter>(router: T) {
export default function hookRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.all('/hooks/(.*)?', koaBody(), errorHandler, koaAdapter(modelRouters.hook.routes()));
}

View file

@ -1,10 +1,9 @@
import { UserRole } from '@logto/schemas';
import Koa from 'koa';
import mount from 'koa-mount';
import Router from 'koa-router';
import type { Provider } from 'oidc-provider';
import koaAuditLogLegacy from '#src/middleware/koa-audit-log-legacy.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import koaAuth from '../middleware/koa-auth.js';
import koaLogSessionLegacy from '../middleware/koa-log-session-legacy.js';
@ -30,37 +29,37 @@ import swaggerRoutes from './swagger.js';
import type { AnonymousRouter, AnonymousRouterLegacy, AuthedRouter } from './types.js';
import wellKnownRoutes from './well-known.js';
const createRouters = (provider: Provider) => {
const createRouters = (tenant: TenantContext) => {
const sessionRouter: AnonymousRouterLegacy = new Router();
sessionRouter.use(koaAuditLogLegacy(), koaLogSessionLegacy(provider));
sessionRoutes(sessionRouter, provider);
sessionRouter.use(koaAuditLogLegacy(), koaLogSessionLegacy(tenant.provider));
sessionRoutes(sessionRouter, tenant);
const interactionRouter: AnonymousRouter = new Router();
interactionRoutes(interactionRouter, provider);
interactionRoutes(interactionRouter, tenant);
const managementRouter: AuthedRouter = new Router();
managementRouter.use(koaAuth(UserRole.Admin));
applicationRoutes(managementRouter);
settingRoutes(managementRouter);
connectorRoutes(managementRouter);
resourceRoutes(managementRouter);
signInExperiencesRoutes(managementRouter);
adminUserRoutes(managementRouter);
adminUserRoleRoutes(managementRouter);
logRoutes(managementRouter);
roleRoutes(managementRouter);
dashboardRoutes(managementRouter);
customPhraseRoutes(managementRouter);
hookRoutes(managementRouter);
applicationRoutes(managementRouter, tenant);
settingRoutes(managementRouter, tenant);
connectorRoutes(managementRouter, tenant);
resourceRoutes(managementRouter, tenant);
signInExperiencesRoutes(managementRouter, tenant);
adminUserRoutes(managementRouter, tenant);
adminUserRoleRoutes(managementRouter, tenant);
logRoutes(managementRouter, tenant);
roleRoutes(managementRouter, tenant);
dashboardRoutes(managementRouter, tenant);
customPhraseRoutes(managementRouter, tenant);
hookRoutes(managementRouter, tenant);
const profileRouter: AnonymousRouter = new Router();
profileRoutes(profileRouter, provider);
profileRoutes(profileRouter, tenant);
const anonymousRouter: AnonymousRouter = new Router();
phraseRoutes(anonymousRouter, provider);
wellKnownRoutes(anonymousRouter, provider);
statusRoutes(anonymousRouter);
authnRoutes(anonymousRouter);
phraseRoutes(anonymousRouter, tenant);
wellKnownRoutes(anonymousRouter, tenant);
statusRoutes(anonymousRouter, tenant);
authnRoutes(anonymousRouter, tenant);
// The swagger.json should contain all API routers.
swaggerRoutes(anonymousRouter, [
sessionRouter,
@ -73,13 +72,13 @@ const createRouters = (provider: Provider) => {
return [sessionRouter, interactionRouter, profileRouter, managementRouter, anonymousRouter];
};
export default function initRouter(app: Koa, provider: Provider) {
export default function initRouter(tenant: TenantContext): Koa {
const apisApp = new Koa();
for (const router of createRouters(provider)) {
for (const router of createRouters(tenant)) {
// @ts-expect-error will remove once interaction refactor finished
apisApp.use(router.routes()).use(router.allowedMethods());
}
app.use(mount('/api', apisApp));
return apisApp;
}

View file

@ -38,7 +38,7 @@ const { encryptUserPassword, generateUserId, insertUser } = mockEsm(
})
);
const { hasActiveUsers } = mockEsm('#src/queries/user.js', () => ({
const { hasActiveUsers, updateUserById } = mockEsm('#src/queries/user.js', () => ({
findUserById: jest
.fn()
.mockResolvedValue({ identities: { google: { userId: 'googleId', details: {} } } }),
@ -46,7 +46,6 @@ const { hasActiveUsers } = mockEsm('#src/queries/user.js', () => ({
hasActiveUsers: jest.fn().mockResolvedValue(true),
}));
const { updateUserById } = await import('#src/queries/user.js');
const submitInteraction = await pickDefault(import('./submit-interaction.js'));
const now = Date.now();

View file

@ -6,7 +6,7 @@ import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
import RequestError from '#src/errors/RequestError/index.js';
import type koaAuditLog from '#src/middleware/koa-audit-log.js';
import { createMockLogContext } from '#src/test-utils/koa-audit-log.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { createMockTenantWithInteraction } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
@ -120,7 +120,7 @@ describe('session -> interactionRoutes', () => {
};
const sessionRequest = createRequester({
anonymousRoutes: interactionRoutes,
provider: createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
tenantContext: createMockTenantWithInteraction(jest.fn().mockResolvedValue(baseProviderMock)),
});
afterEach(() => {
@ -224,7 +224,7 @@ describe('session -> interactionRoutes', () => {
const path = `${interactionPrefix}/profile`;
const sessionRequest = createRequester({
anonymousRoutes: interactionRoutes,
provider: createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
tenantContext: createMockTenantWithInteraction(jest.fn().mockResolvedValue(baseProviderMock)),
});
it('PUT /interaction/profile', async () => {

View file

@ -1,7 +1,6 @@
import type { LogtoErrorCode } from '@logto/phrases';
import { InteractionEvent, eventGuard, identifierPayloadGuard, profileGuard } from '@logto/schemas';
import type Router from 'koa-router';
import type { Provider } from 'oidc-provider';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
@ -10,7 +9,7 @@ import koaAuditLog from '#src/middleware/koa-audit-log.js';
import koaGuard from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.js';
import type { AnonymousRouter } from '../types.js';
import type { AnonymousRouter, RouterInitArgs } from '../types.js';
import submitInteraction from './actions/submit-interaction.js';
import koaInteractionDetails from './middleware/koa-interaction-details.js';
import type { WithInteractionDetailsContext } from './middleware/koa-interaction-details.js';
@ -45,8 +44,7 @@ export const verificationPath = 'verification';
type RouterContext<T> = T extends Router<unknown, infer Context> ? Context : never;
export default function interactionRoutes<T extends AnonymousRouter>(
anonymousRouter: T,
provider: Provider
...[anonymousRouter, { provider }]: RouterInitArgs<T>
) {
const router =
// @ts-expect-error for good koa types

View file

@ -5,9 +5,9 @@ import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import { countLogs, findLogById, findLogs } from '#src/queries/log.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function logRoutes<T extends AuthedRouter>(router: T) {
export default function logRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get(
'/logs',
koaPagination(),

View file

@ -3,7 +3,6 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm';
import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js';
import { mockSignInExperience } from '#src/__mocks__/index.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
@ -37,7 +36,6 @@ const phraseRoutes = await pickDefault(import('./phrase.js'));
const phraseRequest = createRequester({
anonymousRoutes: phraseRoutes,
provider: createMockProvider(),
});
afterEach(() => {

View file

@ -5,7 +5,7 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm';
import { zhCnTag } from '#src/__mocks__/custom-phrase.js';
import { mockSignInExperience } from '#src/__mocks__/index.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { createMockTenantWithInteraction } from '#src/test-utils/tenant.js';
const { jest } = import.meta;
@ -44,7 +44,7 @@ const phraseRoutes = await pickDefault(import('./phrase.js'));
const { createRequester } = await import('#src/utils/test-utils.js');
const phraseRequest = createRequester({
anonymousRoutes: phraseRoutes,
provider: createMockProvider(interactionDetails),
tenantContext: createMockTenantWithInteraction(interactionDetails),
});
describe('when the application is admin-console', () => {

View file

@ -1,13 +1,12 @@
import { isBuiltInLanguageTag } from '@logto/phrases-ui';
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas';
import type { Provider } from 'oidc-provider';
import detectLanguage from '#src/i18n/detect-language.js';
import { getPhrase } from '#src/libraries/phrase.js';
import { findAllCustomLanguageTags } from '#src/queries/custom-phrase.js';
import { findDefaultSignInExperience } from '#src/queries/sign-in-experience.js';
import type { AnonymousRouter } from './types.js';
import type { AnonymousRouter, RouterInitArgs } from './types.js';
const getLanguageInfo = async (applicationId: unknown) => {
if (applicationId === adminConsoleApplicationId) {
@ -19,7 +18,9 @@ const getLanguageInfo = async (applicationId: unknown) => {
return languageInfo;
};
export default function phraseRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
export default function phraseRoutes<T extends AnonymousRouter>(
...[router, { provider }]: RouterInitArgs<T>
) {
router.get('/phrase', async (ctx, next) => {
const interaction = await provider
.interactionDetails(ctx.req, ctx.res)

View file

@ -11,6 +11,7 @@ import {
mockUserResponse,
} from '#src/__mocks__/index.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
@ -85,7 +86,7 @@ describe('session -> profileRoutes', () => {
const mockGetSession: jest.Mock = jest.spyOn(provider.Session, 'get');
const sessionRequest = createRequester({
anonymousRoutes: profileRoutes,
provider,
tenantContext: new MockTenant(provider),
middlewares: [
async (ctx, next) => {
ctx.addLogContext = jest.fn();

View file

@ -2,7 +2,6 @@ import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/cor
import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas';
import { has, pick } from '@silverhand/essentials';
import { argon2Verify } from 'hash-wasm';
import type { Provider } from 'oidc-provider';
import { object, string, unknown } from 'zod';
import { getLogtoConnectorById } from '#src/connectors/index.js';
@ -15,11 +14,13 @@ import { deleteUserIdentity, findUserById, updateUserById } from '#src/queries/u
import assertThat from '#src/utils/assert-that.js';
import { verificationTimeout } from './consts.js';
import type { AnonymousRouter } from './types.js';
import type { AnonymousRouter, RouterInitArgs } from './types.js';
export const profileRoute = '/profile';
export default function profileRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
export default function profileRoutes<T extends AnonymousRouter>(
...[router, { provider }]: RouterInitArgs<T>
) {
router.get(profileRoute, async (ctx, next) => {
const { accountId: userId } = await provider.Session.get(ctx);

View file

@ -25,12 +25,12 @@ import {
} from '#src/queries/scope.js';
import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
const resourceId = buildIdGenerator(21);
const scopeId = resourceId;
export default function resourceRoutes<T extends AuthedRouter>(router: T) {
export default function resourceRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get(
'/resources',
koaPagination({ isOptional: true }),

View file

@ -34,11 +34,11 @@ import {
import assertThat from '#src/utils/assert-that.js';
import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
const roleId = buildIdGenerator(21);
export default function roleRoutes<T extends AuthedRouter>(router: T) {
export default function roleRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get('/roles', koaPagination({ isOptional: true }), async (ctx, next) => {
const { limit, offset, disabled } = ctx.pagination;
const { searchParams } = ctx.request.URL;

View file

@ -3,7 +3,6 @@ import path from 'path';
import type { LogtoErrorCode } from '@logto/phrases';
import { UserRole, adminConsoleApplicationId } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import type { Provider } from 'oidc-provider';
import { object, string } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
@ -11,7 +10,7 @@ import { assignInteractionResults, saveUserFirstConsentedAppId } from '#src/libr
import { findUserById } from '#src/queries/user.js';
import assertThat from '#src/utils/assert-that.js';
import type { AnonymousRouterLegacy } from '../types.js';
import type { AnonymousRouterLegacy, RouterInitArgs } from '../types.js';
import continueRoutes from './continue.js';
import forgotPasswordRoutes from './forgot-password.js';
import koaGuardSessionAction from './middleware/koa-guard-session-action.js';
@ -21,8 +20,7 @@ import socialRoutes from './social.js';
import { getRoutePrefix } from './utils.js';
export default function sessionRoutes<T extends AnonymousRouterLegacy>(
router: T,
provider: Provider
...[router, { provider }]: RouterInitArgs<T>
) {
router.use(getRoutePrefix('sign-in'), koaGuardSessionAction(provider, 'sign-in'));
router.use(getRoutePrefix('register'), koaGuardSessionAction(provider, 'register'));

View file

@ -3,9 +3,9 @@ import { Settings } from '@logto/schemas';
import koaGuard from '#src/middleware/koa-guard.js';
import { getSetting, updateSetting } from '#src/queries/setting.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function settingRoutes<T extends AuthedRouter>(router: T) {
export default function settingRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get('/settings', async (ctx, next) => {
const { id, ...rest } = await getSetting();
ctx.body = rest;

View file

@ -14,9 +14,11 @@ import {
updateDefaultSignInExperience,
} from '#src/queries/sign-in-experience.js';
import type { AuthedRouter } from './types.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function signInExperiencesRoutes<T extends AuthedRouter>(router: T) {
export default function signInExperiencesRoutes<T extends AuthedRouter>(
...[router]: RouterInitArgs<T>
) {
/**
* As we only support single signInExperience settings for V1
* always return the default settings in DB for the /sign-in-exp get method

View file

@ -1,8 +1,8 @@
import koaGuard from '#src/middleware/koa-guard.js';
import type { AnonymousRouter } from './types.js';
import type { AnonymousRouter, RouterInitArgs } from './types.js';
export default function statusRoutes<T extends AnonymousRouter>(router: T) {
export default function statusRoutes<T extends AnonymousRouter>(...[router]: RouterInitArgs<T>) {
router.get('/status', koaGuard({ status: 204 }), async (ctx, next) => {
ctx.status = 204;

View file

@ -5,6 +5,7 @@ import type { WithLogContextLegacy } from '#src/middleware/koa-audit-log-legacy.
import type { WithLogContext } from '#src/middleware/koa-audit-log.js';
import type { WithAuthContext } from '#src/middleware/koa-auth.js';
import type { WithI18nContext } from '#src/middleware/koa-i18next.js';
import type TenantContext from '#src/tenants/TenantContext.js';
export type AnonymousRouter = Router<unknown, WithLogContext & WithI18nContext>;
@ -15,3 +16,6 @@ export type AuthedRouter = Router<
unknown,
WithAuthContext & WithLogContext & WithI18nContext & ExtendableContext
>;
export type RouterInit<T> = (router: T, tenant: TenantContext) => void;
export type RouterInitArgs<T> = Parameters<RouterInit<T>>;

View file

@ -16,6 +16,7 @@ import {
mockWechatNativeConnector,
} from '#src/__mocks__/index.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
@ -58,7 +59,7 @@ describe('GET /.well-known/sign-in-exp', () => {
const provider = createMockProvider();
const sessionRequest = createRequester({
anonymousRoutes: wellKnownRoutes,
provider,
tenantContext: new MockTenant(provider),
middlewares: [
async (ctx, next) => {
ctx.addLogContext = jest.fn();

View file

@ -2,15 +2,16 @@ import type { ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorType } from '@logto/connector-kit';
import { adminConsoleApplicationId } from '@logto/schemas';
import etag from 'etag';
import type { Provider } from 'oidc-provider';
import { getLogtoConnectors } from '#src/connectors/index.js';
import { getApplicationIdFromInteraction } from '#src/libraries/session.js';
import { getSignInExperienceForApplication } from '#src/libraries/sign-in-experience/index.js';
import type { AnonymousRouter } from './types.js';
import type { AnonymousRouter, RouterInitArgs } from './types.js';
export default function wellKnownRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
export default function wellKnownRoutes<T extends AnonymousRouter>(
...[router, { provider }]: RouterInitArgs<T>
) {
router.get(
'/.well-known/sign-in-exp',
async (ctx, next) => {

View file

@ -0,0 +1,35 @@
import type { CommonQueryMethods } from 'slonik';
import { createApplicationQueries } from '#src/queries/application.js';
import { createConnectorQueries } from '#src/queries/connector.js';
import { createCustomPhraseQueries } from '#src/queries/custom-phrase.js';
import { createLogQueries } from '#src/queries/log.js';
import { createOidcModelInstanceQueries } from '#src/queries/oidc-model-instance.js';
import { createPasscodeQueries } from '#src/queries/passcode.js';
import { createResourceQueries } from '#src/queries/resource.js';
import { createRolesScopesQueries } from '#src/queries/roles-scopes.js';
import { createRolesQueries } from '#src/queries/roles.js';
import { createScopeQueries } from '#src/queries/scope.js';
import { createSettingQueries } from '#src/queries/setting.js';
import { createSignInExperienceQueries } from '#src/queries/sign-in-experience.js';
import { createUserQueries } from '#src/queries/user.js';
import { createUsersRolesQueries } from '#src/queries/users-roles.js';
export default class Queries {
applications = createApplicationQueries(this.pool);
connectors = createConnectorQueries(this.pool);
customPhrases = createCustomPhraseQueries(this.pool);
logs = createLogQueries(this.pool);
oidcModelInstances = createOidcModelInstanceQueries(this.pool);
passcodes = createPasscodeQueries(this.pool);
resources = createResourceQueries(this.pool);
rolesScopes = createRolesScopesQueries(this.pool);
roles = createRolesQueries(this.pool);
scopes = createScopeQueries(this.pool);
settings = createSettingQueries(this.pool);
signInExperiences = createSignInExperienceQueries(this.pool);
users = createUserQueries(this.pool);
usersRoles = createUsersRolesQueries(this.pool);
constructor(public readonly pool: CommonQueryMethods) {}
}

View file

@ -24,7 +24,7 @@ const middlewareList = [
});
// eslint-disable-next-line unicorn/consistent-function-scoping
mockEsmDefault('#src/oidc/init.js', () => () => createMockProvider);
mockEsmDefault('#src/oidc/init.js', () => () => createMockProvider());
const Tenant = await pickDefault(import('./Tenant.js'));

View file

@ -5,7 +5,7 @@ import koaLogger from 'koa-logger';
import mount from 'koa-mount';
import type { Provider } from 'oidc-provider';
import { MountedApps } from '#src/env-set/index.js';
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 koaErrorHandler from '#src/middleware/koa-error-handler.js';
@ -19,18 +19,29 @@ import koaWelcomeProxy from '#src/middleware/koa-welcome-proxy.js';
import initOidc from '#src/oidc/init.js';
import initRouter from '#src/routes/init.js';
export default class Tenant {
public readonly provider: Provider;
import Queries from './Queries.js';
import type TenantContext from './TenantContext.js';
protected readonly app: Koa;
export default class Tenant implements TenantContext {
public readonly provider: Provider;
public readonly queries: Queries;
public readonly app: Koa;
get run(): MiddlewareType {
return mount(this.app);
}
constructor(public id: string) {
const queries = new Queries(envSet.pool);
this.queries = queries;
// Init app
const app = new Koa();
const provider = initOidc(app);
const provider = initOidc();
app.use(mount('/oidc', provider.app));
app.use(koaLogger());
app.use(koaErrorHandler());
@ -39,7 +50,8 @@ export default class Tenant {
app.use(koaConnectorErrorHandler());
app.use(koaI18next());
initRouter(app, provider);
const apisApp = initRouter({ provider, queries });
app.use(mount('/api', apisApp));
app.use(mount('/', koaRootProxy()));

View file

@ -0,0 +1,8 @@
import type { Provider } from 'oidc-provider';
import type Queries from './Queries.js';
export default abstract class TenantContext {
public abstract readonly provider: Provider;
public abstract readonly queries: Queries;
}

View file

@ -0,0 +1 @@
export const defaultTenant = 'default';

View file

@ -20,3 +20,5 @@ class TenantPool {
}
export const tenantPool = new TenantPool();
export * from './consts.js';

View file

@ -0,0 +1,30 @@
import type Queries from '#src/tenants/Queries.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import { createMockProvider } from './oidc-provider.js';
const { jest } = import.meta;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const proxy: Queries = new Proxy<any>(
{},
{
get() {
return new Proxy(
{},
{
get() {
return jest.fn();
},
}
);
},
}
);
export class MockTenant implements TenantContext {
constructor(public provider = createMockProvider(), public queries = proxy) {}
}
export const createMockTenantWithInteraction = (interactionDetails?: jest.Mock) =>
new MockTenant(createMockProvider(interactionDetails));

View file

@ -2,7 +2,6 @@ import type { MiddlewareType, Context, Middleware } from 'koa';
import Koa from 'koa';
import type { IRouterParamContext } from 'koa-router';
import Router from 'koa-router';
import type { Provider } from 'oidc-provider';
import type { QueryResult, QueryResultRow } from 'slonik';
import { createMockPool, createMockQueryResult } from 'slonik';
import type {
@ -12,8 +11,10 @@ import type {
import request from 'supertest';
import type { AuthedRouter, AnonymousRouter } from '#src/routes/types.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import type { Options } from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
import { MockTenant } from '#src/test-utils/tenant.js';
/**
* Slonik Query Mock Utils
@ -103,46 +104,24 @@ export const createContextWithRouteParameters = (
/**
* Supertest Request Mock Utils
**/
type RouteLauncher<T extends AuthedRouter | AnonymousRouter> = (router: T) => void;
type ProviderRouteLauncher<T extends AuthedRouter | AnonymousRouter> = (
type RouteLauncher<T extends AuthedRouter | AnonymousRouter> = (
router: T,
provider: Provider
tenant: TenantContext
) => void;
export function createRequester(
payload:
| {
anonymousRoutes?: RouteLauncher<AnonymousRouter> | Array<RouteLauncher<AnonymousRouter>>;
authedRoutes?: RouteLauncher<AuthedRouter> | Array<RouteLauncher<AuthedRouter>>;
middlewares?: Middleware[];
}
| {
anonymousRoutes?:
| ProviderRouteLauncher<AnonymousRouter>
| Array<ProviderRouteLauncher<AnonymousRouter>>;
authedRoutes?: RouteLauncher<AuthedRouter> | Array<RouteLauncher<AuthedRouter>>;
middlewares?: Middleware[];
provider: Provider;
}
): request.SuperTest<request.Test>;
export function createRequester({
anonymousRoutes,
authedRoutes,
provider,
middlewares,
tenantContext,
}: {
anonymousRoutes?:
| RouteLauncher<AnonymousRouter>
| Array<RouteLauncher<AnonymousRouter>>
| ProviderRouteLauncher<AnonymousRouter>
| Array<ProviderRouteLauncher<AnonymousRouter>>;
anonymousRoutes?: RouteLauncher<AnonymousRouter> | Array<RouteLauncher<AnonymousRouter>>;
authedRoutes?: RouteLauncher<AuthedRouter> | Array<RouteLauncher<AuthedRouter>>;
provider?: Provider;
middlewares?: Middleware[];
tenantContext?: TenantContext;
}): request.SuperTest<request.Test> {
const app = new Koa();
const tenant = tenantContext ?? new MockTenant();
if (middlewares) {
for (const middleware of middlewares) {
@ -154,13 +133,7 @@ export function createRequester({
const anonymousRouter: AnonymousRouter = new Router();
for (const route of Array.isArray(anonymousRoutes) ? anonymousRoutes : [anonymousRoutes]) {
if (provider) {
route(anonymousRouter, provider);
} else {
// For test use only
// eslint-disable-next-line no-restricted-syntax
(route as RouteLauncher<AnonymousRouter>)(anonymousRouter);
}
route(anonymousRouter, tenant);
}
app.use(anonymousRouter.routes()).use(anonymousRouter.allowedMethods());
@ -176,7 +149,7 @@ export function createRequester({
});
for (const route of Array.isArray(authedRoutes) ? authedRoutes : [authedRoutes]) {
route(authRouter);
route(authRouter, tenant);
}
app.use(authRouter.routes()).use(authRouter.allowedMethods());