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:
parent
a68b34971a
commit
26f8511f93
41 changed files with 209 additions and 141 deletions
packages/core
package.json
src
app
oidc
queries
routes
admin-user-role.tsadmin-user.tsapplication.tsauthn.tsconnector.tscustom-phrase.tsdashboard.tshook.tsinit.ts
interaction
log.tsphrase.content-language.test.tsphrase.test.tsphrase.tsprofile.test.tsprofile.tsresource.tsrole.tssession
setting.tssign-in-experience.tsstatus.tstypes.tswell-known.test.tswell-known.tstenants
test-utils
utils
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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`, `)}
|
||||
|
|
|
@ -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`,`)}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
35
packages/core/src/tenants/Queries.ts
Normal file
35
packages/core/src/tenants/Queries.ts
Normal 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) {}
|
||||
}
|
|
@ -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'));
|
||||
|
||||
|
|
|
@ -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()));
|
||||
|
||||
|
|
8
packages/core/src/tenants/TenantContext.ts
Normal file
8
packages/core/src/tenants/TenantContext.ts
Normal 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;
|
||||
}
|
1
packages/core/src/tenants/consts.ts
Normal file
1
packages/core/src/tenants/consts.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const defaultTenant = 'default';
|
|
@ -20,3 +20,5 @@ class TenantPool {
|
|||
}
|
||||
|
||||
export const tenantPool = new TenantPool();
|
||||
|
||||
export * from './consts.js';
|
||||
|
|
30
packages/core/src/test-utils/tenant.ts
Normal file
30
packages/core/src/test-utils/tenant.ts
Normal 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));
|
|
@ -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());
|
||||
|
|
Loading…
Add table
Reference in a new issue