From 26f8511f937d8aabd035b1b72e050a853cb84d02 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 9 Jan 2023 16:58:02 +0800 Subject: [PATCH] refactor(core): use tenant context for route inits --- packages/core/package.json | 12 ++++- packages/core/src/app/init.ts | 4 +- packages/core/src/oidc/init.test.ts | 5 +- packages/core/src/oidc/init.ts | 7 +-- packages/core/src/queries/passcode.ts | 2 +- packages/core/src/queries/user.ts | 2 +- packages/core/src/routes/admin-user-role.ts | 6 ++- packages/core/src/routes/admin-user.ts | 4 +- packages/core/src/routes/application.ts | 4 +- packages/core/src/routes/authn.ts | 4 +- packages/core/src/routes/connector.ts | 4 +- packages/core/src/routes/custom-phrase.ts | 4 +- packages/core/src/routes/dashboard.ts | 4 +- packages/core/src/routes/hook.ts | 4 +- packages/core/src/routes/init.ts | 51 +++++++++---------- .../actions/submit-interaction.test.ts | 3 +- .../core/src/routes/interaction/index.test.ts | 6 +-- packages/core/src/routes/interaction/index.ts | 6 +-- packages/core/src/routes/log.ts | 4 +- .../routes/phrase.content-language.test.ts | 2 - packages/core/src/routes/phrase.test.ts | 4 +- packages/core/src/routes/phrase.ts | 7 +-- packages/core/src/routes/profile.test.ts | 3 +- packages/core/src/routes/profile.ts | 7 +-- packages/core/src/routes/resource.ts | 4 +- packages/core/src/routes/role.ts | 4 +- packages/core/src/routes/session/index.ts | 6 +-- packages/core/src/routes/setting.ts | 4 +- .../core/src/routes/sign-in-experience.ts | 6 ++- packages/core/src/routes/status.ts | 4 +- packages/core/src/routes/types.ts | 4 ++ packages/core/src/routes/well-known.test.ts | 3 +- packages/core/src/routes/well-known.ts | 7 +-- packages/core/src/tenants/Queries.ts | 35 +++++++++++++ packages/core/src/tenants/Tenant.test.ts | 2 +- packages/core/src/tenants/Tenant.ts | 24 ++++++--- packages/core/src/tenants/TenantContext.ts | 8 +++ packages/core/src/tenants/consts.ts | 1 + packages/core/src/tenants/index.ts | 2 + packages/core/src/test-utils/tenant.ts | 30 +++++++++++ packages/core/src/utils/test-utils.ts | 47 ++++------------- 41 files changed, 209 insertions(+), 141 deletions(-) create mode 100644 packages/core/src/tenants/Queries.ts create mode 100644 packages/core/src/tenants/TenantContext.ts create mode 100644 packages/core/src/tenants/consts.ts create mode 100644 packages/core/src/test-utils/tenant.ts diff --git a/packages/core/package.json b/packages/core/package.json index cf9cdab03..04bd590aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" } diff --git a/packages/core/src/app/init.ts b/packages/core/src/app/init.ts index 17fd5aae6..47f9e2d06 100644 --- a/packages/core/src/app/init.ts +++ b/packages/core/src/app/init.ts @@ -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 { app.use(async (ctx, next) => { // TODO: add multi-tenancy logic diff --git a/packages/core/src/oidc/init.test.ts b/packages/core/src/oidc/init.test.ts index b16a2949f..ed096df50 100644 --- a/packages/core/src/oidc/init.test.ts +++ b/packages/core/src/oidc/init.test.ts @@ -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(); }); }); diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 47070199f..51e017282 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -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; } diff --git a/packages/core/src/queries/passcode.ts b/packages/core/src/queries/passcode.ts index b336a374a..b79740933 100644 --- a/packages/core/src/queries/passcode.ts +++ b/packages/core/src/queries/passcode.ts @@ -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(sql` select ${sql.join(Object.values(fields), sql`, `)} diff --git a/packages/core/src/queries/user.ts b/packages/core/src/queries/user.ts index e87dc3be4..8654d0721 100644 --- a/packages/core/src/queries/user.ts +++ b/packages/core/src/queries/user.ts @@ -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(sql` select ${sql.join(Object.values(fields), sql`,`)} diff --git a/packages/core/src/routes/admin-user-role.ts b/packages/core/src/routes/admin-user-role.ts index be841b1e6..c80ae8e14 100644 --- a/packages/core/src/routes/admin-user-role.ts +++ b/packages/core/src/routes/admin-user-role.ts @@ -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(router: T) { +export default function adminUserRoleRoutes( + ...[router]: RouterInitArgs +) { router.get( '/users/:userId/roles', koaGuard({ diff --git a/packages/core/src/routes/admin-user.ts b/packages/core/src/routes/admin-user.ts index ccce47cc8..a958e5cdb 100644 --- a/packages/core/src/routes/admin-user.ts +++ b/packages/core/src/routes/admin-user.ts @@ -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(router: T) { +export default function adminUserRoutes(...[router]: RouterInitArgs) { router.get('/users', koaPagination(), async (ctx, next) => { const { limit, offset } = ctx.pagination; const { searchParams } = ctx.request.URL; diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts index cc75a5726..2c9633735 100644 --- a/packages/core/src/routes/application.ts +++ b/packages/core/src/routes/application.ts @@ -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(router: T) { +export default function applicationRoutes(...[router]: RouterInitArgs) { router.get('/applications', koaPagination(), async (ctx, next) => { const { limit, offset } = ctx.pagination; diff --git a/packages/core/src/routes/authn.ts b/packages/core/src/routes/authn.ts index 03f64d6df..922314707 100644 --- a/packages/core/src/routes/authn.ts +++ b/packages/core/src/routes/authn.ts @@ -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(router: T) { +export default function authnRoutes(...[router]: RouterInitArgs) { router.get( '/authn/hasura', koaGuard({ diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index a3ac7e7df..bc02178db 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -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(router: T) { +export default function connectorRoutes(...[router]: RouterInitArgs) { router.get( '/connectors', koaGuard({ diff --git a/packages/core/src/routes/custom-phrase.ts b/packages/core/src/routes/custom-phrase.ts index f446b2f6e..a04199abc 100644 --- a/packages/core/src/routes/custom-phrase.ts +++ b/packages/core/src/routes/custom-phrase.ts @@ -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`, force to cast it back to `Translation`. // eslint-disable-next-line no-restricted-syntax cleanDeep(translation) as Translation; -export default function customPhraseRoutes(router: T) { +export default function customPhraseRoutes(...[router]: RouterInitArgs) { router.get( '/custom-phrases', koaGuard({ diff --git a/packages/core/src/routes/dashboard.ts b/packages/core/src/routes/dashboard.ts index 4b4ee5a00..449c9b182 100644 --- a/packages/core/src/routes/dashboard.ts +++ b/packages/core/src/routes/dashboard.ts @@ -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(router: T) { +export default function dashboardRoutes(...[router]: RouterInitArgs) { router.get('/dashboard/users/total', async (ctx, next) => { const { count: totalUserCount } = await countUsers(); ctx.body = { totalUserCount }; diff --git a/packages/core/src/routes/hook.ts b/packages/core/src/routes/hook.ts index 534ea9e31..d19fbbee7 100644 --- a/packages/core/src/routes/hook.ts +++ b/packages/core/src/routes/hook.ts @@ -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(router: T) { +export default function hookRoutes(...[router]: RouterInitArgs) { router.all('/hooks/(.*)?', koaBody(), errorHandler, koaAdapter(modelRouters.hook.routes())); } diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 52b331fad..1faa7b771 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -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; } diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index 0b7a06113..75b815468 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -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(); diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index d1eb44b53..9437c251f 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -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 () => { diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index 3111eefee..a568b0678 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -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 extends Router ? Context : never; export default function interactionRoutes( - anonymousRouter: T, - provider: Provider + ...[anonymousRouter, { provider }]: RouterInitArgs ) { const router = // @ts-expect-error for good koa types diff --git a/packages/core/src/routes/log.ts b/packages/core/src/routes/log.ts index 09f0103b9..dae86a430 100644 --- a/packages/core/src/routes/log.ts +++ b/packages/core/src/routes/log.ts @@ -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(router: T) { +export default function logRoutes(...[router]: RouterInitArgs) { router.get( '/logs', koaPagination(), diff --git a/packages/core/src/routes/phrase.content-language.test.ts b/packages/core/src/routes/phrase.content-language.test.ts index 068c7a4e9..3368f93cd 100644 --- a/packages/core/src/routes/phrase.content-language.test.ts +++ b/packages/core/src/routes/phrase.content-language.test.ts @@ -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(() => { diff --git a/packages/core/src/routes/phrase.test.ts b/packages/core/src/routes/phrase.test.ts index d30e72ff0..802724b1e 100644 --- a/packages/core/src/routes/phrase.test.ts +++ b/packages/core/src/routes/phrase.test.ts @@ -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', () => { diff --git a/packages/core/src/routes/phrase.ts b/packages/core/src/routes/phrase.ts index 40036ffcb..93294b9e7 100644 --- a/packages/core/src/routes/phrase.ts +++ b/packages/core/src/routes/phrase.ts @@ -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(router: T, provider: Provider) { +export default function phraseRoutes( + ...[router, { provider }]: RouterInitArgs +) { router.get('/phrase', async (ctx, next) => { const interaction = await provider .interactionDetails(ctx.req, ctx.res) diff --git a/packages/core/src/routes/profile.test.ts b/packages/core/src/routes/profile.test.ts index 0812035ec..60561f092 100644 --- a/packages/core/src/routes/profile.test.ts +++ b/packages/core/src/routes/profile.test.ts @@ -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(); diff --git a/packages/core/src/routes/profile.ts b/packages/core/src/routes/profile.ts index 88cc15ee3..17fa7cc20 100644 --- a/packages/core/src/routes/profile.ts +++ b/packages/core/src/routes/profile.ts @@ -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(router: T, provider: Provider) { +export default function profileRoutes( + ...[router, { provider }]: RouterInitArgs +) { router.get(profileRoute, async (ctx, next) => { const { accountId: userId } = await provider.Session.get(ctx); diff --git a/packages/core/src/routes/resource.ts b/packages/core/src/routes/resource.ts index b74a3c796..291759c47 100644 --- a/packages/core/src/routes/resource.ts +++ b/packages/core/src/routes/resource.ts @@ -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(router: T) { +export default function resourceRoutes(...[router]: RouterInitArgs) { router.get( '/resources', koaPagination({ isOptional: true }), diff --git a/packages/core/src/routes/role.ts b/packages/core/src/routes/role.ts index c02d416b2..0de0ec6c3 100644 --- a/packages/core/src/routes/role.ts +++ b/packages/core/src/routes/role.ts @@ -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(router: T) { +export default function roleRoutes(...[router]: RouterInitArgs) { router.get('/roles', koaPagination({ isOptional: true }), async (ctx, next) => { const { limit, offset, disabled } = ctx.pagination; const { searchParams } = ctx.request.URL; diff --git a/packages/core/src/routes/session/index.ts b/packages/core/src/routes/session/index.ts index a85cc5e3f..920c2470b 100644 --- a/packages/core/src/routes/session/index.ts +++ b/packages/core/src/routes/session/index.ts @@ -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( - router: T, - provider: Provider + ...[router, { provider }]: RouterInitArgs ) { router.use(getRoutePrefix('sign-in'), koaGuardSessionAction(provider, 'sign-in')); router.use(getRoutePrefix('register'), koaGuardSessionAction(provider, 'register')); diff --git a/packages/core/src/routes/setting.ts b/packages/core/src/routes/setting.ts index b79c03982..213452751 100644 --- a/packages/core/src/routes/setting.ts +++ b/packages/core/src/routes/setting.ts @@ -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(router: T) { +export default function settingRoutes(...[router]: RouterInitArgs) { router.get('/settings', async (ctx, next) => { const { id, ...rest } = await getSetting(); ctx.body = rest; diff --git a/packages/core/src/routes/sign-in-experience.ts b/packages/core/src/routes/sign-in-experience.ts index ea7b421c5..607178c5d 100644 --- a/packages/core/src/routes/sign-in-experience.ts +++ b/packages/core/src/routes/sign-in-experience.ts @@ -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(router: T) { +export default function signInExperiencesRoutes( + ...[router]: RouterInitArgs +) { /** * As we only support single signInExperience settings for V1 * always return the default settings in DB for the /sign-in-exp get method diff --git a/packages/core/src/routes/status.ts b/packages/core/src/routes/status.ts index 916c002eb..f4f1a4792 100644 --- a/packages/core/src/routes/status.ts +++ b/packages/core/src/routes/status.ts @@ -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(router: T) { +export default function statusRoutes(...[router]: RouterInitArgs) { router.get('/status', koaGuard({ status: 204 }), async (ctx, next) => { ctx.status = 204; diff --git a/packages/core/src/routes/types.ts b/packages/core/src/routes/types.ts index 8005e5d79..2382e68ab 100644 --- a/packages/core/src/routes/types.ts +++ b/packages/core/src/routes/types.ts @@ -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; @@ -15,3 +16,6 @@ export type AuthedRouter = Router< unknown, WithAuthContext & WithLogContext & WithI18nContext & ExtendableContext >; + +export type RouterInit = (router: T, tenant: TenantContext) => void; +export type RouterInitArgs = Parameters>; diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 15d32133e..0c2c488e6 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -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(); diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts index e06ea47d0..2c4c75308 100644 --- a/packages/core/src/routes/well-known.ts +++ b/packages/core/src/routes/well-known.ts @@ -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(router: T, provider: Provider) { +export default function wellKnownRoutes( + ...[router, { provider }]: RouterInitArgs +) { router.get( '/.well-known/sign-in-exp', async (ctx, next) => { diff --git a/packages/core/src/tenants/Queries.ts b/packages/core/src/tenants/Queries.ts new file mode 100644 index 000000000..07d2f0b60 --- /dev/null +++ b/packages/core/src/tenants/Queries.ts @@ -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) {} +} diff --git a/packages/core/src/tenants/Tenant.test.ts b/packages/core/src/tenants/Tenant.test.ts index b6771649c..aa2b2e248 100644 --- a/packages/core/src/tenants/Tenant.test.ts +++ b/packages/core/src/tenants/Tenant.test.ts @@ -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')); diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index 2268fb89a..e37fa9b3c 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -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())); diff --git a/packages/core/src/tenants/TenantContext.ts b/packages/core/src/tenants/TenantContext.ts new file mode 100644 index 000000000..fa95edc7b --- /dev/null +++ b/packages/core/src/tenants/TenantContext.ts @@ -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; +} diff --git a/packages/core/src/tenants/consts.ts b/packages/core/src/tenants/consts.ts new file mode 100644 index 000000000..c8fad436a --- /dev/null +++ b/packages/core/src/tenants/consts.ts @@ -0,0 +1 @@ +export const defaultTenant = 'default'; diff --git a/packages/core/src/tenants/index.ts b/packages/core/src/tenants/index.ts index 13a85bff9..271343603 100644 --- a/packages/core/src/tenants/index.ts +++ b/packages/core/src/tenants/index.ts @@ -20,3 +20,5 @@ class TenantPool { } export const tenantPool = new TenantPool(); + +export * from './consts.js'; diff --git a/packages/core/src/test-utils/tenant.ts b/packages/core/src/test-utils/tenant.ts new file mode 100644 index 000000000..89d5c4109 --- /dev/null +++ b/packages/core/src/test-utils/tenant.ts @@ -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( + {}, + { + 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)); diff --git a/packages/core/src/utils/test-utils.ts b/packages/core/src/utils/test-utils.ts index 1f9720731..a8371a5bd 100644 --- a/packages/core/src/utils/test-utils.ts +++ b/packages/core/src/utils/test-utils.ts @@ -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 = (router: T) => void; - -type ProviderRouteLauncher = ( +type RouteLauncher = ( router: T, - provider: Provider + tenant: TenantContext ) => void; -export function createRequester( - payload: - | { - anonymousRoutes?: RouteLauncher | Array>; - authedRoutes?: RouteLauncher | Array>; - middlewares?: Middleware[]; - } - | { - anonymousRoutes?: - | ProviderRouteLauncher - | Array>; - authedRoutes?: RouteLauncher | Array>; - middlewares?: Middleware[]; - provider: Provider; - } -): request.SuperTest; - export function createRequester({ anonymousRoutes, authedRoutes, - provider, middlewares, + tenantContext, }: { - anonymousRoutes?: - | RouteLauncher - | Array> - | ProviderRouteLauncher - | Array>; + anonymousRoutes?: RouteLauncher | Array>; authedRoutes?: RouteLauncher | Array>; - provider?: Provider; middlewares?: Middleware[]; + tenantContext?: TenantContext; }): request.SuperTest { 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); - } + 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());