mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor: decouple admin tenant and user tenant
This commit is contained in:
parent
dd5b3037a8
commit
0481a450be
21 changed files with 163 additions and 89 deletions
|
@ -1,7 +1,7 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import type { Application } from '@logto/schemas';
|
||||
import { AppearanceMode, demoAppApplicationId } from '@logto/schemas';
|
||||
import { useMemo } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
|
@ -18,6 +18,7 @@ import Passwordless from '@/assets/images/passwordless.svg';
|
|||
import SocialDark from '@/assets/images/social-dark.svg';
|
||||
import Social from '@/assets/images/social.svg';
|
||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
|
@ -37,6 +38,7 @@ type GetStartedMetadata = {
|
|||
const useGetStartedMetadata = () => {
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const { configs, updateConfigs } = useConfigs();
|
||||
const { app } = useContext(AppEndpointsContext);
|
||||
const theme = useTheme();
|
||||
const isLightMode = theme === AppearanceMode.LightMode;
|
||||
const { data: demoApp, error } = useSWR<Application, RequestError>(
|
||||
|
@ -67,7 +69,7 @@ const useGetStartedMetadata = () => {
|
|||
isHidden: hideDemo,
|
||||
onClick: async () => {
|
||||
void updateConfigs({ demoChecked: true });
|
||||
window.open('/demo-app', '_blank');
|
||||
window.open(new URL('/demo-app', app), '_blank');
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -23,6 +23,9 @@ mockEsm('#src/env-set/check-alteration-state.js', () => ({
|
|||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
mockEsmDefault('#src/env-set/oidc.js', () => () => ({
|
||||
issuer: 'https://logto.test/oidc',
|
||||
cookieKeys: [],
|
||||
privateJwks: [],
|
||||
publicJwks: [],
|
||||
}));
|
||||
/* End */
|
||||
|
||||
|
|
|
@ -9,18 +9,24 @@ import RequestError from '#src/errors/RequestError/index.js';
|
|||
import { mockEnvSet } from '#src/test-utils/env-set.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { WithAuthContext } from './koa-auth.js';
|
||||
import type { WithAuthContext } from './index.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const { mockEsm } = createMockUtils(jest);
|
||||
|
||||
mockEsm('./utils.js', () => ({
|
||||
getAdminTenantTokenValidationSet: jest.fn().mockResolvedValue({ keys: [], issuer: [] }),
|
||||
}));
|
||||
|
||||
const { jwtVerify } = mockEsm('jose', () => ({
|
||||
createLocalJWKSet: jest.fn(),
|
||||
jwtVerify: jest
|
||||
.fn()
|
||||
.mockReturnValue({ payload: { sub: 'fooUser', scope: defaultManagementApi.scope.name } }),
|
||||
}));
|
||||
|
||||
const koaAuth = await pickDefault(import('./koa-auth.js'));
|
||||
const audience = defaultManagementApi.resource.indicator;
|
||||
const koaAuth = await pickDefault(import('./index.js'));
|
||||
|
||||
describe('koaAuth middleware', () => {
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
|
@ -64,7 +70,7 @@ describe('koaAuth middleware', () => {
|
|||
developmentUserId: 'foo',
|
||||
});
|
||||
|
||||
await koaAuth(mockEnvSet)(ctx, next);
|
||||
await koaAuth(mockEnvSet, audience)(ctx, next);
|
||||
expect(ctx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||
|
||||
stub.restore();
|
||||
|
@ -79,7 +85,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await koaAuth(mockEnvSet)(mockCtx, next);
|
||||
await koaAuth(mockEnvSet, audience)(mockCtx, next);
|
||||
expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||
});
|
||||
|
||||
|
@ -91,7 +97,7 @@ describe('koaAuth middleware', () => {
|
|||
isIntegrationTest: true,
|
||||
});
|
||||
|
||||
await koaAuth(mockEnvSet)(ctx, next);
|
||||
await koaAuth(mockEnvSet, audience)(ctx, next);
|
||||
expect(ctx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||
|
||||
stub.restore();
|
||||
|
@ -112,7 +118,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await koaAuth(mockEnvSet)(mockCtx, next);
|
||||
await koaAuth(mockEnvSet, audience)(mockCtx, next);
|
||||
expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||
|
||||
stub.restore();
|
||||
|
@ -125,12 +131,14 @@ describe('koaAuth middleware', () => {
|
|||
authorization: 'Bearer access_token',
|
||||
},
|
||||
};
|
||||
await koaAuth(mockEnvSet)(ctx, next);
|
||||
await koaAuth(mockEnvSet, audience)(ctx, next);
|
||||
expect(ctx.auth).toEqual({ type: 'user', id: 'fooUser' });
|
||||
});
|
||||
|
||||
it('expect to throw if authorization header is missing', async () => {
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(authHeaderMissingError);
|
||||
await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(
|
||||
authHeaderMissingError
|
||||
);
|
||||
});
|
||||
|
||||
it('expect to throw if authorization header token type not recognized ', async () => {
|
||||
|
@ -141,7 +149,9 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(tokenNotSupportedError);
|
||||
await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(
|
||||
tokenNotSupportedError
|
||||
);
|
||||
});
|
||||
|
||||
it('expect to throw if jwt sub is missing', async () => {
|
||||
|
@ -154,11 +164,13 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(jwtSubMissingError);
|
||||
await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(jwtSubMissingError);
|
||||
});
|
||||
|
||||
it('expect to have `client` type per jwt verify result', async () => {
|
||||
jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'bar', client_id: 'bar' } }));
|
||||
jwtVerify.mockImplementationOnce(() => ({
|
||||
payload: { sub: 'bar', client_id: 'bar', scope: 'all' },
|
||||
}));
|
||||
|
||||
ctx.request = {
|
||||
...ctx.request,
|
||||
|
@ -167,7 +179,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await koaAuth(mockEnvSet)(ctx, next);
|
||||
await koaAuth(mockEnvSet, audience)(ctx, next);
|
||||
expect(ctx.auth).toEqual({ type: 'app', id: 'bar' });
|
||||
});
|
||||
|
||||
|
@ -181,7 +193,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(forbiddenError);
|
||||
await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(forbiddenError);
|
||||
});
|
||||
|
||||
it('expect to throw if jwt scope does not include management resource scope', async () => {
|
||||
|
@ -196,7 +208,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(forbiddenError);
|
||||
await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(forbiddenError);
|
||||
});
|
||||
|
||||
it('expect to throw unauthorized error if unknown error occurs', async () => {
|
||||
|
@ -210,7 +222,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(
|
||||
await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(
|
||||
new RequestError({ code: 'auth.unauthorized', status: 401 }, new Error('unknown error'))
|
||||
);
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import type { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import { adminTenantId, defaultManagementApi } from '@logto/schemas';
|
||||
import { adminTenantId, defaultManagementApi, PredefinedScope } from '@logto/schemas';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import type { JWK } from 'jose';
|
||||
import { createLocalJWKSet, jwtVerify } from 'jose';
|
||||
|
@ -10,9 +10,10 @@ import { z } from 'zod';
|
|||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { tenantPool } from '#src/tenants/index.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { getAdminTenantTokenValidationSet } from './utils.js';
|
||||
|
||||
export type Auth = {
|
||||
type: 'user' | 'app';
|
||||
id: string;
|
||||
|
@ -68,13 +69,11 @@ export const verifyBearerTokenFromRequest = async (
|
|||
return [publicJwks, [issuer]];
|
||||
}
|
||||
|
||||
const {
|
||||
envSet: { oidc: adminOidc },
|
||||
} = await tenantPool.get(adminTenantId);
|
||||
const adminSet = await getAdminTenantTokenValidationSet();
|
||||
|
||||
return [
|
||||
[...publicJwks, ...adminOidc.publicJwks],
|
||||
[issuer, adminOidc.issuer],
|
||||
[...publicJwks, ...adminSet.keys],
|
||||
[issuer, ...adminSet.issuer],
|
||||
];
|
||||
};
|
||||
|
||||
|
@ -105,8 +104,7 @@ export const verifyBearerTokenFromRequest = async (
|
|||
|
||||
export default function koaAuth<StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
|
||||
envSet: EnvSet,
|
||||
audience: string,
|
||||
expectScopes = [defaultManagementApi.scope.name]
|
||||
audience: string
|
||||
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
const { sub, clientId, scopes } = await verifyBearerTokenFromRequest(
|
||||
|
@ -116,7 +114,7 @@ export default function koaAuth<StateT, ContextT extends IRouterParamContext, Re
|
|||
);
|
||||
|
||||
assertThat(
|
||||
expectScopes.every((scope) => scopes.includes(scope)),
|
||||
scopes.includes(PredefinedScope.All),
|
||||
new RequestError({ code: 'auth.forbidden', status: 403 })
|
||||
);
|
||||
|
54
packages/core/src/middleware/koa-auth/utils.ts
Normal file
54
packages/core/src/middleware/koa-auth/utils.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import crypto from 'crypto';
|
||||
|
||||
import type { LogtoConfig } from '@logto/schemas';
|
||||
import {
|
||||
logtoOidcConfigGuard,
|
||||
adminTenantId,
|
||||
LogtoOidcConfigKey,
|
||||
LogtoConfigs,
|
||||
} from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import type { JWK } from 'jose';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { exportJWK } from '#src/utils/jwks.js';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
|
||||
/**
|
||||
* This function is to fetch OIDC public signing keys and the issuer from the admin tenant
|
||||
* in order to let user tenants recognize Access Tokens issued by the admin tenant.
|
||||
*
|
||||
* Usually you don't mean to call this function.
|
||||
*/
|
||||
export const getAdminTenantTokenValidationSet = async (): Promise<{
|
||||
keys: JWK[];
|
||||
issuer: string[];
|
||||
}> => {
|
||||
const { isDomainBasedMultiTenancy, urlSet, adminUrlSet } = EnvSet.values;
|
||||
|
||||
if (!isDomainBasedMultiTenancy && adminUrlSet.deduplicated().length === 0) {
|
||||
return { keys: [], issuer: [] };
|
||||
}
|
||||
|
||||
const pool = await EnvSet.pool;
|
||||
const { value } = await pool.one<LogtoConfig>(sql`
|
||||
select ${sql.join([fields.key, fields.value], sql`,`)} from ${table}
|
||||
where ${fields.tenantId} = ${adminTenantId}
|
||||
and ${fields.key} = ${LogtoOidcConfigKey.PrivateKeys}
|
||||
`);
|
||||
const privateKeys = logtoOidcConfigGuard['oidc.privateKeys']
|
||||
.parse(value)
|
||||
.map((key) => crypto.createPrivateKey(key));
|
||||
const publicKeys = privateKeys.map((key) => crypto.createPublicKey(key));
|
||||
|
||||
return {
|
||||
keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))),
|
||||
issuer: [
|
||||
(isDomainBasedMultiTenancy
|
||||
? urlSet.endpoint.replace('*', adminTenantId)
|
||||
: adminUrlSet.endpoint) + '/oidc',
|
||||
],
|
||||
};
|
||||
};
|
|
@ -2,14 +2,13 @@ import {
|
|||
adminTenantId,
|
||||
arbitraryObjectGuard,
|
||||
getManagementApiResourceIndicator,
|
||||
PredefinedScope,
|
||||
} from '@logto/schemas';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type { WithAuthContext } from '#src/middleware/koa-auth.js';
|
||||
import koaAuth from '#src/middleware/koa-auth.js';
|
||||
import type { WithAuthContext } from '#src/middleware/koa-auth/index.js';
|
||||
import koaAuth from '#src/middleware/koa-auth/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
@ -25,9 +24,7 @@ export default function initMeApis(tenant: TenantContext): Koa {
|
|||
console.log('????', getManagementApiResourceIndicator(adminTenantId, 'me'));
|
||||
|
||||
meRouter.use(
|
||||
koaAuth(tenant.envSet, getManagementApiResourceIndicator(adminTenantId, 'me'), [
|
||||
PredefinedScope.All,
|
||||
]),
|
||||
koaAuth(tenant.envSet, getManagementApiResourceIndicator(adminTenantId, 'me')),
|
||||
async (ctx, next) => {
|
||||
assertThat(
|
||||
ctx.auth.type === 'user',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { verifyBearerTokenFromRequest } from '#src/middleware/koa-auth.js';
|
||||
import { verifyBearerTokenFromRequest } from '#src/middleware/koa-auth/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import cors from '@koa/cors';
|
||||
import { getManagementApiResourceIndicator, PredefinedScope } from '@logto/schemas';
|
||||
import { getManagementApiResourceIndicator } from '@logto/schemas';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
||||
import koaAuth from '../middleware/koa-auth.js';
|
||||
import koaAuth from '../middleware/koa-auth/index.js';
|
||||
import adminUserRoleRoutes from './admin-user-role.js';
|
||||
import adminUserRoutes from './admin-user.js';
|
||||
import applicationRoutes from './application.js';
|
||||
|
@ -35,9 +35,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
interactionRoutes(interactionRouter, tenant);
|
||||
|
||||
const managementRouter: AuthedRouter = new Router();
|
||||
managementRouter.use(
|
||||
koaAuth(tenant.envSet, getManagementApiResourceIndicator(tenant.id), [PredefinedScope.All])
|
||||
);
|
||||
managementRouter.use(koaAuth(tenant.envSet, getManagementApiResourceIndicator(tenant.id)));
|
||||
applicationRoutes(managementRouter, tenant);
|
||||
logtoConfigRoutes(managementRouter, tenant);
|
||||
connectorRoutes(managementRouter, tenant);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { InteractionEvent, adminConsoleApplicationId } from '@logto/schemas';
|
||||
import { InteractionEvent, adminConsoleApplicationId, adminTenantId } from '@logto/schemas';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
import type Provider from 'oidc-provider';
|
||||
|
||||
|
@ -31,6 +31,10 @@ const { encryptUserPassword } = mockEsm('#src/libraries/user.js', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
mockEsm('#src/utils/tenant.js', () => ({
|
||||
getTenantId: () => adminTenantId,
|
||||
}));
|
||||
|
||||
const userQueries = {
|
||||
findUserById: jest
|
||||
.fn()
|
||||
|
@ -115,7 +119,7 @@ describe('submit action', () => {
|
|||
id: 'uid',
|
||||
...upsertProfile,
|
||||
},
|
||||
false
|
||||
[]
|
||||
);
|
||||
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
|
||||
login: { accountId: 'uid' },
|
||||
|
@ -153,7 +157,7 @@ describe('submit action', () => {
|
|||
id: 'uid',
|
||||
...upsertProfile,
|
||||
},
|
||||
true
|
||||
['user', 'default:admin']
|
||||
);
|
||||
expect(assignInteractionResults).toBeCalledWith(adminConsoleCtx, tenant.provider, {
|
||||
login: { accountId: 'uid' },
|
||||
|
|
|
@ -174,7 +174,7 @@ export default async function submitInteraction(
|
|||
id,
|
||||
...upsertProfile,
|
||||
},
|
||||
createAdminUser ? [getManagementApiAdminName(defaultTenantId), UserRole.User] : []
|
||||
createAdminUser ? [UserRole.User, getManagementApiAdminName(defaultTenantId)] : []
|
||||
);
|
||||
|
||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { ExtendableContext } from 'koa';
|
|||
import type Router from 'koa-router';
|
||||
|
||||
import type { WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import type { WithAuthContext } from '#src/middleware/koa-auth.js';
|
||||
import type { WithAuthContext } from '#src/middleware/koa-auth/index.js';
|
||||
import type { WithI18nContext } from '#src/middleware/koa-i18next.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
||||
|
|
|
@ -111,11 +111,12 @@ describe('GET /.well-known/sign-in-exp', () => {
|
|||
|
||||
expect(response.body).toMatchObject({
|
||||
...adminConsoleSignInExperience,
|
||||
tenantId: 'admin',
|
||||
branding: {
|
||||
...adminConsoleSignInExperience.branding,
|
||||
slogan: 'admin_console.welcome.title',
|
||||
},
|
||||
termsOfUseUrl: mockSignInExperience.termsOfUseUrl,
|
||||
termsOfUseUrl: null,
|
||||
languageInfo: mockSignInExperience.languageInfo,
|
||||
socialConnectors: [],
|
||||
signInMode: SignInMode.SignIn,
|
||||
|
|
|
@ -79,20 +79,20 @@ export default class Tenant implements TenantContext {
|
|||
// Mount APIs
|
||||
app.use(mount('/api', initApis(tenantContext)));
|
||||
|
||||
// Mount `/me` APIs for admin tenant
|
||||
// Mount admin tenant APIs and app
|
||||
if (id === adminTenantId) {
|
||||
console.log('111111111111122221');
|
||||
// Mount `/me` APIs for admin tenant
|
||||
app.use(mount('/me', initMeApis(tenantContext)));
|
||||
}
|
||||
|
||||
// Mount Admin Console
|
||||
app.use(koaConsoleRedirectProxy(queries));
|
||||
app.use(
|
||||
mount(
|
||||
'/' + UserApps.Console,
|
||||
koaSpaProxy(mountedApps, UserApps.Console, 5002, UserApps.Console)
|
||||
)
|
||||
);
|
||||
// Mount Admin Console
|
||||
app.use(koaConsoleRedirectProxy(queries));
|
||||
app.use(
|
||||
mount(
|
||||
'/' + UserApps.Console,
|
||||
koaSpaProxy(mountedApps, UserApps.Console, 5002, UserApps.Console)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Mount demo app
|
||||
app.use(
|
||||
|
|
5
packages/core/src/tenants/TenantPoolContext.ts
Normal file
5
packages/core/src/tenants/TenantPoolContext.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type TenantContext from './TenantContext.js';
|
||||
|
||||
export default abstract class TenantPoolContext<Tenant extends TenantContext = TenantContext> {
|
||||
public abstract get(tenantId: string): Promise<Tenant>;
|
||||
}
|
|
@ -2,7 +2,7 @@ import LRUCache from 'lru-cache';
|
|||
|
||||
import Tenant from './Tenant.js';
|
||||
|
||||
class TenantPool {
|
||||
export class TenantPool {
|
||||
protected cache = new LRUCache<string, Tenant>({ max: 500 });
|
||||
|
||||
async get(tenantId: string): Promise<Tenant> {
|
||||
|
|
|
@ -5,7 +5,6 @@ export * from './sign-in-experience.js';
|
|||
export * from './admin-user.js';
|
||||
export * from './logs.js';
|
||||
export * from './dashboard.js';
|
||||
export * from './me.js';
|
||||
export * from './wellknown.js';
|
||||
export * from './interaction.js';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { adminRoleId } from '@logto/schemas';
|
||||
import { defaultManagementApi } from '@logto/schemas';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { assignRolesToUser, getUserRoles, deleteRoleFromUser } from '#src/api/index.js';
|
||||
|
@ -25,8 +25,8 @@ describe('admin console user management (roles)', () => {
|
|||
it('should delete role from user successfully', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
|
||||
await assignRolesToUser(user.id, [adminRoleId]);
|
||||
await deleteRoleFromUser(user.id, adminRoleId);
|
||||
await assignRolesToUser(user.id, [defaultManagementApi.role.id]);
|
||||
await deleteRoleFromUser(user.id, defaultManagementApi.role.id);
|
||||
|
||||
const roles = await getUserRoles(user.id);
|
||||
expect(roles.length).toBe(0);
|
||||
|
@ -35,7 +35,7 @@ describe('admin console user management (roles)', () => {
|
|||
it('should delete non-exist-role from user failed', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
|
||||
const response = await deleteRoleFromUser(user.id, adminRoleId).catch(
|
||||
const response = await deleteRoleFromUser(user.id, defaultManagementApi.role.id).catch(
|
||||
(error: unknown) => error
|
||||
);
|
||||
expect(response instanceof HTTPError && response.response.statusCode === 404).toBe(true);
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import path from 'path';
|
||||
|
||||
import { fetchTokenByRefreshToken } from '@logto/js';
|
||||
import {
|
||||
managementResource,
|
||||
InteractionEvent,
|
||||
adminRoleId,
|
||||
managementResourceScope,
|
||||
} from '@logto/schemas';
|
||||
import { defaultManagementApi, InteractionEvent } from '@logto/schemas';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
|
@ -27,14 +22,14 @@ describe('get access token', () => {
|
|||
beforeAll(async () => {
|
||||
await createUserByAdmin(guestUsername, password);
|
||||
const user = await createUserByAdmin(username, password);
|
||||
await assignUsersToRole([user.id], adminRoleId);
|
||||
await assignUsersToRole([user.id], defaultManagementApi.role.id);
|
||||
await enableAllPasswordSignInMethods();
|
||||
});
|
||||
|
||||
it('sign-in and getAccessToken with admin user', async () => {
|
||||
const client = new MockClient({
|
||||
resources: [managementResource.indicator],
|
||||
scopes: [managementResourceScope.name],
|
||||
resources: [defaultManagementApi.resource.indicator],
|
||||
scopes: [defaultManagementApi.scope.name],
|
||||
});
|
||||
await client.initSession();
|
||||
await client.successSend(putInteraction, {
|
||||
|
@ -43,11 +38,11 @@ describe('get access token', () => {
|
|||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
const accessToken = await client.getAccessToken(managementResource.indicator);
|
||||
const accessToken = await client.getAccessToken(defaultManagementApi.resource.indicator);
|
||||
expect(accessToken).not.toBeNull();
|
||||
expect(getAccessTokenPayload(accessToken)).toHaveProperty(
|
||||
'scope',
|
||||
managementResourceScope.name
|
||||
defaultManagementApi.scope.name
|
||||
);
|
||||
|
||||
// Request for invalid resource should throw
|
||||
|
@ -56,8 +51,8 @@ describe('get access token', () => {
|
|||
|
||||
it('sign-in and getAccessToken with guest user', async () => {
|
||||
const client = new MockClient({
|
||||
resources: [managementResource.indicator],
|
||||
scopes: [managementResourceScope.name],
|
||||
resources: [defaultManagementApi.resource.indicator],
|
||||
scopes: [defaultManagementApi.scope.name],
|
||||
});
|
||||
await client.initSession();
|
||||
await client.successSend(putInteraction, {
|
||||
|
@ -66,16 +61,16 @@ describe('get access token', () => {
|
|||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
const accessToken = await client.getAccessToken(managementResource.indicator);
|
||||
const accessToken = await client.getAccessToken(defaultManagementApi.resource.indicator);
|
||||
|
||||
expect(getAccessTokenPayload(accessToken)).not.toHaveProperty(
|
||||
'scope',
|
||||
managementResourceScope.name
|
||||
defaultManagementApi.scope.name
|
||||
);
|
||||
});
|
||||
|
||||
it('sign-in and get multiple Access Token by the same Refresh Token within refreshTokenReuseInterval', async () => {
|
||||
const client = new MockClient({ resources: [managementResource.indicator] });
|
||||
const client = new MockClient({ resources: [defaultManagementApi.resource.indicator] });
|
||||
|
||||
await client.initSession();
|
||||
|
||||
|
@ -98,7 +93,7 @@ describe('get access token', () => {
|
|||
clientId: defaultConfig.appId,
|
||||
tokenEndpoint: path.join(logtoUrl, '/oidc/token'),
|
||||
refreshToken,
|
||||
resource: managementResource.indicator,
|
||||
resource: defaultManagementApi.resource.indicator,
|
||||
},
|
||||
async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
|
||||
const response = await fetch(...args);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { managementResource, managementResourceScope } from '@logto/schemas';
|
||||
import { defaultManagementApi } from '@logto/schemas';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { createResource } from '#src/api/index.js';
|
||||
|
@ -7,9 +7,9 @@ import { generateScopeName } from '#src/utils.js';
|
|||
|
||||
describe('scopes', () => {
|
||||
it('should get management api resource scopes successfully', async () => {
|
||||
const scopes = await getScopes(managementResource.id);
|
||||
const scopes = await getScopes(defaultManagementApi.resource.id);
|
||||
|
||||
expect(scopes[0]).toMatchObject(managementResourceScope);
|
||||
expect(scopes[0]).toMatchObject(defaultManagementApi.scope);
|
||||
});
|
||||
|
||||
it('should create scope successfully', async () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { managementResource } from '@logto/schemas';
|
||||
import { defaultManagementApi } from '@logto/schemas';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { createResource, getResource, updateResource, deleteResource } from '#src/api/index.js';
|
||||
|
@ -6,9 +6,9 @@ import { generateResourceIndicator, generateResourceName } from '#src/utils.js';
|
|||
|
||||
describe('admin console api resources', () => {
|
||||
it('should get management api resource details successfully', async () => {
|
||||
const fetchedManagementApiResource = await getResource(managementResource.id);
|
||||
const fetchedManagementApiResource = await getResource(defaultManagementApi.resource.id);
|
||||
|
||||
expect(fetchedManagementApiResource).toMatchObject(managementResource);
|
||||
expect(fetchedManagementApiResource).toMatchObject(defaultManagementApi.resource);
|
||||
});
|
||||
|
||||
it('should create api resource successfully', async () => {
|
||||
|
|
|
@ -6,9 +6,9 @@ create table users (
|
|||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
id varchar(12) not null,
|
||||
username varchar(128) unique,
|
||||
primary_email varchar(128) unique,
|
||||
primary_phone varchar(128) unique,
|
||||
username varchar(128),
|
||||
primary_email varchar(128),
|
||||
primary_phone varchar(128),
|
||||
password_encrypted varchar(128),
|
||||
password_encryption_method users_password_encryption_method,
|
||||
name varchar(128),
|
||||
|
@ -19,7 +19,13 @@ create table users (
|
|||
is_suspended boolean not null default false,
|
||||
last_sign_in_at timestamptz,
|
||||
created_at timestamptz not null default (now()),
|
||||
primary key (id)
|
||||
primary key (id),
|
||||
constraint users__username
|
||||
unique (tenant_id, username),
|
||||
constraint users__primary_email
|
||||
unique (tenant_id, primary_email),
|
||||
constraint users__primary_phone
|
||||
unique (tenant_id, primary_phone)
|
||||
);
|
||||
|
||||
create index users__id
|
||||
|
|
Loading…
Add table
Reference in a new issue