mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(cloud,schemas,cli): update cloud service tenant APIs (#3875)
This commit is contained in:
parent
708303349e
commit
62b1ee6b72
11 changed files with 265 additions and 91 deletions
|
@ -4,8 +4,10 @@ import {
|
|||
} from '@logto/cli/lib/commands/database/utils.js';
|
||||
import { DemoConnector } from '@logto/connector-kit';
|
||||
import { createTenantMetadata } from '@logto/core-kit';
|
||||
import type { LogtoOidcConfigType, TenantInfo, CreateTenant } from '@logto/schemas';
|
||||
import {
|
||||
type LogtoOidcConfigType,
|
||||
type TenantInfo,
|
||||
type CreateTenant,
|
||||
createAdminTenantApplicationRole,
|
||||
AdminTenantRole,
|
||||
createTenantMachineToMachineApplication,
|
||||
|
@ -17,11 +19,10 @@ import {
|
|||
adminTenantId,
|
||||
createAdminData,
|
||||
createAdminDataInAdminTenant,
|
||||
getManagementApiResourceIndicator,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { appendPath } from '@silverhand/essentials';
|
||||
import type { ZodType } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { createApplicationsQueries } from '#src/queries/application.js';
|
||||
|
@ -38,11 +39,6 @@ import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
|
|||
|
||||
const demoSocialConnectorId = 'logto-social-demo';
|
||||
|
||||
export const tenantInfoGuard: ZodType<TenantInfo> = z.object({
|
||||
id: z.string(),
|
||||
indicator: z.string(),
|
||||
});
|
||||
|
||||
const oidcConfigBuilders: {
|
||||
[key in LogtoOidcConfigKey]: () => Promise<LogtoOidcConfigType[key]>;
|
||||
} = {
|
||||
|
@ -54,23 +50,40 @@ export class TenantsLibrary {
|
|||
constructor(public readonly queries: Queries) {}
|
||||
|
||||
async getAvailableTenants(userId: string): Promise<TenantInfo[]> {
|
||||
const { getManagementApiLikeIndicatorsForUser } = this.queries.tenants;
|
||||
const { getManagementApiLikeIndicatorsForUser, getTenantsByIds } = this.queries.tenants;
|
||||
const { rows } = await getManagementApiLikeIndicatorsForUser(userId);
|
||||
|
||||
return rows
|
||||
.map(({ indicator }) => ({
|
||||
id: getTenantIdFromManagementApiIndicator(indicator),
|
||||
indicator,
|
||||
}))
|
||||
.filter((tenant): tenant is TenantInfo => Boolean(tenant.id));
|
||||
const tenantIds = rows
|
||||
.map(({ indicator }) => getTenantIdFromManagementApiIndicator(indicator))
|
||||
.filter((id): id is string => typeof id === 'string');
|
||||
|
||||
if (tenantIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async createNewTenant(forUserId: string): Promise<TenantInfo> {
|
||||
const rawTenantInfos = await getTenantsByIds(tenantIds);
|
||||
return rawTenantInfos.map(({ id, name, tag }) => ({
|
||||
id,
|
||||
name,
|
||||
tag,
|
||||
indicator: getManagementApiResourceIndicator(id),
|
||||
}));
|
||||
}
|
||||
|
||||
async createNewTenant(
|
||||
forUserId: string,
|
||||
payload: Pick<CreateTenant, 'name' | 'tag'>
|
||||
): Promise<TenantInfo> {
|
||||
const databaseName = await getDatabaseName(this.queries.client);
|
||||
const { id: tenantId, parentRole, role, password } = createTenantMetadata(databaseName);
|
||||
|
||||
// Init tenant
|
||||
const createTenant: CreateTenant = { id: tenantId, dbUser: role, dbUserPassword: password };
|
||||
const createTenant: CreateTenant = {
|
||||
id: tenantId,
|
||||
dbUser: role,
|
||||
dbUserPassword: password,
|
||||
...payload,
|
||||
};
|
||||
const transaction = await this.queries.client.transaction();
|
||||
const tenants = createTenantsQueries(transaction);
|
||||
const users = createUsersQueries(transaction);
|
||||
|
@ -84,6 +97,7 @@ export class TenantsLibrary {
|
|||
|
||||
// Init tenant
|
||||
await tenants.insertTenant(createTenant);
|
||||
const insertedTenant = await tenants.getTenantById(tenantId);
|
||||
await tenants.createTenantRole(parentRole, role, password);
|
||||
|
||||
// Create admin data set (resource, roles, etc.)
|
||||
|
@ -168,6 +182,11 @@ export class TenantsLibrary {
|
|||
await transaction.end();
|
||||
/* === End === */
|
||||
|
||||
return { id: tenantId, indicator: adminDataInAdminTenant.resource.indicator };
|
||||
return {
|
||||
id: tenantId,
|
||||
name: insertedTenant.name,
|
||||
tag: insertedTenant.tag,
|
||||
indicator: adminDataInAdminTenant.resource.indicator,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,14 @@ import {
|
|||
adminTenantId,
|
||||
getManagementApiResourceIndicator,
|
||||
PredefinedScope,
|
||||
type AdminData,
|
||||
type CreateTenant,
|
||||
type CreateRolesScope,
|
||||
type TenantInfo,
|
||||
} from '@logto/schemas';
|
||||
import type { AdminData, CreateTenant, CreateRolesScope } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import { jsonb, dangerousRaw, id, sql } from '@withtyped/postgres';
|
||||
import { jsonb, dangerousRaw, id, sql, jsonIfNeeded } from '@withtyped/postgres';
|
||||
import type { Queryable } from '@withtyped/server';
|
||||
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
|
@ -34,7 +37,14 @@ export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
|
|||
where roles.tenant_id = ${adminTenantId};
|
||||
`);
|
||||
|
||||
const insertTenant = async (tenant: CreateTenant) => client.query(insertInto(tenant, 'tenants'));
|
||||
const insertTenant = async (tenant: CreateTenant) =>
|
||||
client.query(
|
||||
insertInto(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
Object.fromEntries(Object.entries(tenant).filter(([_, value]) => value !== undefined)),
|
||||
'tenants'
|
||||
)
|
||||
);
|
||||
|
||||
const createTenantRole = async (parentRole: string, role: string, password: string) =>
|
||||
client.query(sql`
|
||||
|
@ -80,6 +90,25 @@ export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
|
|||
);
|
||||
};
|
||||
|
||||
const getTenantById = async (id: string): Promise<Pick<TenantInfo, 'name' | 'tag'>> => {
|
||||
return client.one<Pick<TenantInfo, 'name' | 'tag'>>(sql`
|
||||
select name, tag from tenants
|
||||
where id = ${id}
|
||||
`);
|
||||
};
|
||||
|
||||
const getTenantsByIds = async (
|
||||
tenantIds: string[]
|
||||
): Promise<Array<Pick<TenantInfo, 'id' | 'name' | 'tag'>>> => {
|
||||
const { rows } = await client.query<Pick<TenantInfo, 'id' | 'name' | 'tag'>>(sql`
|
||||
select id, name, tag from tenants
|
||||
where id in (${tenantIds.map((tenantId) => jsonIfNeeded(tenantId))})
|
||||
order by created_at desc, name desc;
|
||||
`);
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const appendAdminConsoleRedirectUris = async (...urls: URL[]) => {
|
||||
const metadataKey = id('oidc_client_metadata');
|
||||
|
||||
|
@ -102,6 +131,8 @@ export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
|
|||
insertTenant,
|
||||
createTenantRole,
|
||||
insertAdminData,
|
||||
getTenantById,
|
||||
getTenantsByIds,
|
||||
appendAdminConsoleRedirectUris,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { TenantInfo } from '@logto/schemas';
|
||||
import { CloudScope } from '@logto/schemas';
|
||||
import { CloudScope, TenantTag } from '@logto/schemas';
|
||||
|
||||
import { buildRequestAuthContext, createHttpContext } from '#src/test-utils/context.js';
|
||||
import { noop } from '#src/test-utils/function.js';
|
||||
|
@ -12,13 +12,21 @@ describe('GET /api/tenants', () => {
|
|||
const router = tenantsRoutes(library);
|
||||
|
||||
it('should return whatever the library returns', async () => {
|
||||
const tenants: TenantInfo[] = [{ id: 'tenant_a', indicator: 'https://foo.bar' }];
|
||||
const tenants: TenantInfo[] = [
|
||||
{
|
||||
id: 'tenant_a',
|
||||
name: 'tenant_a',
|
||||
tag: TenantTag.Development,
|
||||
indicator: 'https://foo.bar',
|
||||
},
|
||||
];
|
||||
library.getAvailableTenants.mockResolvedValueOnce(tenants);
|
||||
|
||||
await router.routes()(
|
||||
buildRequestAuthContext('GET /tenants')(),
|
||||
async ({ json }) => {
|
||||
async ({ json, status }) => {
|
||||
expect(json).toBe(tenants);
|
||||
expect(status).toBe(200);
|
||||
},
|
||||
createHttpContext()
|
||||
);
|
||||
|
@ -30,48 +38,90 @@ describe('POST /api/tenants', () => {
|
|||
const router = tenantsRoutes(library);
|
||||
|
||||
it('should throw 403 when lack of permission', async () => {
|
||||
await expect(
|
||||
router.routes()(buildRequestAuthContext('POST /tenants')(), noop, createHttpContext())
|
||||
).rejects.toMatchObject({ status: 403 });
|
||||
});
|
||||
|
||||
it('should throw 409 when user has a tenant', async () => {
|
||||
const tenant: TenantInfo = { id: 'tenant_a', indicator: 'https://foo.bar' };
|
||||
library.getAvailableTenants.mockResolvedValueOnce([tenant]);
|
||||
|
||||
await expect(
|
||||
router.routes()(
|
||||
buildRequestAuthContext('POST /tenants')([CloudScope.CreateTenant]),
|
||||
buildRequestAuthContext('POST /tenants', {
|
||||
body: { name: 'tenant', tag: TenantTag.Development },
|
||||
})(),
|
||||
noop,
|
||||
createHttpContext()
|
||||
)
|
||||
).rejects.toMatchObject({ status: 409 });
|
||||
).rejects.toMatchObject({ status: 403 });
|
||||
});
|
||||
|
||||
it('should be able to create a new tenant', async () => {
|
||||
const tenant: TenantInfo = { id: 'tenant_a', indicator: 'https://foo.bar' };
|
||||
const tenant: TenantInfo = {
|
||||
id: 'tenant_a',
|
||||
name: 'tenant_a',
|
||||
tag: TenantTag.Development,
|
||||
indicator: 'https://foo.bar',
|
||||
};
|
||||
library.getAvailableTenants.mockResolvedValueOnce([]);
|
||||
library.createNewTenant.mockResolvedValueOnce(tenant);
|
||||
|
||||
await router.routes()(
|
||||
buildRequestAuthContext('POST /tenants')([CloudScope.CreateTenant]),
|
||||
async ({ json }) => {
|
||||
buildRequestAuthContext('POST /tenants', {
|
||||
body: { name: 'tenant_a', tag: TenantTag.Development },
|
||||
})([CloudScope.CreateTenant]),
|
||||
async ({ json, status }) => {
|
||||
expect(json).toBe(tenant);
|
||||
expect(status).toBe(201);
|
||||
},
|
||||
createHttpContext()
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to create a new tenant with `create:tenant` scope even if user has a tenant', async () => {
|
||||
const tenantA: TenantInfo = {
|
||||
id: 'tenant_a',
|
||||
name: 'tenant_a',
|
||||
tag: TenantTag.Development,
|
||||
indicator: 'https://foo.bar',
|
||||
};
|
||||
const tenantB: TenantInfo = {
|
||||
id: 'tenant_b',
|
||||
name: 'tenant_b',
|
||||
tag: TenantTag.Development,
|
||||
indicator: 'https://foo.baz',
|
||||
};
|
||||
library.getAvailableTenants.mockResolvedValueOnce([tenantA]);
|
||||
library.createNewTenant.mockResolvedValueOnce(tenantB);
|
||||
|
||||
await router.routes()(
|
||||
buildRequestAuthContext('POST /tenants', {
|
||||
body: { name: 'tenant_b', tag: TenantTag.Development },
|
||||
})([CloudScope.CreateTenant]),
|
||||
async ({ json, status }) => {
|
||||
expect(json).toBe(tenantB);
|
||||
expect(status).toBe(201);
|
||||
},
|
||||
createHttpContext()
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to create a new tenant with `manage:tenant` scope even if user has a tenant', async () => {
|
||||
const tenantA: TenantInfo = { id: 'tenant_a', indicator: 'https://foo.bar' };
|
||||
const tenantB: TenantInfo = { id: 'tenant_b', indicator: 'https://foo.baz' };
|
||||
const tenantA: TenantInfo = {
|
||||
id: 'tenant_a',
|
||||
name: 'tenant_a',
|
||||
tag: TenantTag.Development,
|
||||
indicator: 'https://foo.bar',
|
||||
};
|
||||
const tenantB: TenantInfo = {
|
||||
id: 'tenant_b',
|
||||
name: 'tenant_b',
|
||||
tag: TenantTag.Development,
|
||||
indicator: 'https://foo.baz',
|
||||
};
|
||||
library.getAvailableTenants.mockResolvedValueOnce([tenantA]);
|
||||
library.createNewTenant.mockResolvedValueOnce(tenantB);
|
||||
|
||||
await router.routes()(
|
||||
buildRequestAuthContext('POST /tenants')([CloudScope.ManageTenant]),
|
||||
async ({ json }) => {
|
||||
buildRequestAuthContext('POST /tenants', {
|
||||
body: { name: 'tenant_b', tag: TenantTag.Development },
|
||||
})([CloudScope.ManageTenant]),
|
||||
async ({ json, status }) => {
|
||||
expect(json).toBe(tenantB);
|
||||
expect(status).toBe(201);
|
||||
},
|
||||
createHttpContext()
|
||||
);
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import { CloudScope } from '@logto/schemas';
|
||||
import { CloudScope, tenantInfoGuard, createTenantGuard } from '@logto/schemas';
|
||||
import { createRouter, RequestError } from '@withtyped/server';
|
||||
|
||||
import type { TenantsLibrary } from '#src/libraries/tenants.js';
|
||||
import { tenantInfoGuard } from '#src/libraries/tenants.js';
|
||||
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
||||
|
||||
export const tenantsRoutes = (library: TenantsLibrary) =>
|
||||
createRouter<WithAuthContext, '/tenants'>('/tenants')
|
||||
.get('/', { response: tenantInfoGuard.array() }, async (context, next) => {
|
||||
return next({ ...context, json: await library.getAvailableTenants(context.auth.id) });
|
||||
return next({
|
||||
...context,
|
||||
json: await library.getAvailableTenants(context.auth.id),
|
||||
status: 200,
|
||||
});
|
||||
})
|
||||
.post('/', { response: tenantInfoGuard }, async (context, next) => {
|
||||
.post(
|
||||
'/',
|
||||
{
|
||||
body: createTenantGuard.pick({ name: true, tag: true }).required(),
|
||||
response: tenantInfoGuard,
|
||||
},
|
||||
async (context, next) => {
|
||||
if (
|
||||
![CloudScope.CreateTenant, CloudScope.ManageTenant].some((scope) =>
|
||||
context.auth.scopes.includes(scope)
|
||||
|
@ -19,11 +28,10 @@ export const tenantsRoutes = (library: TenantsLibrary) =>
|
|||
throw new RequestError('Forbidden due to lack of permission.', 403);
|
||||
}
|
||||
|
||||
const tenants = await library.getAvailableTenants(context.auth.id);
|
||||
|
||||
if (!context.auth.scopes.includes(CloudScope.ManageTenant) && tenants.length > 0) {
|
||||
throw new RequestError('The user already has a tenant.', 409);
|
||||
}
|
||||
|
||||
return next({ ...context, json: await library.createNewTenant(context.auth.id) });
|
||||
return next({
|
||||
...context,
|
||||
json: await library.createNewTenant(context.auth.id, context.guarded.body),
|
||||
status: 201,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,7 +12,7 @@ export class MockTenantsLibrary implements TenantsLibrary {
|
|||
}
|
||||
|
||||
public getAvailableTenants = jest.fn<Promise<TenantInfo[]>, [string]>();
|
||||
public createNewTenant = jest.fn<Promise<TenantInfo>, [string]>();
|
||||
public createNewTenant = jest.fn<Promise<TenantInfo>, [string, Record<string, unknown>]>();
|
||||
}
|
||||
|
||||
export class MockServicesLibrary implements ServicesLibrary {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { TenantInfo } from '@logto/schemas';
|
||||
import { type TenantInfo, TenantTag } from '@logto/schemas';
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
|
@ -19,7 +19,15 @@ function Tenants({ data, onAdd }: Props) {
|
|||
const { navigate } = useContext(TenantsContext);
|
||||
|
||||
const createTenant = useCallback(async () => {
|
||||
onAdd(await api.post('api/tenants').json<TenantInfo>());
|
||||
onAdd(
|
||||
/**
|
||||
* `name` and `tag` are required for POST /tenants API, add fixed value to avoid throwing error.
|
||||
* This page page will be removed in upcoming changes on multi-tenancy cloud console.
|
||||
*/
|
||||
await api
|
||||
.post('api/tenants', { json: { name: 'My Project', tag: TenantTag.Development } })
|
||||
.json<TenantInfo>()
|
||||
);
|
||||
}, [api, onAdd]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { TenantInfo } from '@logto/schemas';
|
||||
import { defaultManagementApi } from '@logto/schemas';
|
||||
import { type TenantInfo, TenantTag, defaultManagementApi } from '@logto/schemas';
|
||||
import { conditional, noop } from '@silverhand/essentials';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, createContext, useState } from 'react';
|
||||
|
@ -22,7 +21,11 @@ export type Tenants = {
|
|||
};
|
||||
|
||||
const { tenantId, indicator } = defaultManagementApi.resource;
|
||||
const initialTenants = conditional(!isCloud && [{ id: tenantId, indicator }]);
|
||||
const initialTenants = conditional(
|
||||
!isCloud && [
|
||||
{ id: tenantId, name: `tenant_${tenantId}`, tag: `${TenantTag.Development}`, indicator }, // Make `tag` value to be string type.
|
||||
]
|
||||
);
|
||||
|
||||
export const TenantsContext = createContext<Tenants>({
|
||||
tenants: initialTenants,
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import type { TenantInfo } from '@logto/schemas';
|
||||
import type { CreateTenant, TenantInfo } from '@logto/schemas';
|
||||
|
||||
import { cloudApi } from './api.js';
|
||||
|
||||
export const createTenant = async (accessToken: string) => {
|
||||
export const createTenant = async (
|
||||
accessToken: string,
|
||||
payload: Required<Pick<CreateTenant, 'name' | 'tag'>>
|
||||
) => {
|
||||
return cloudApi
|
||||
.extend({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
.post('tenants')
|
||||
.post('tenants', { json: payload })
|
||||
.json<TenantInfo>();
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,9 @@ import {
|
|||
type Resource,
|
||||
type Scope,
|
||||
type Role,
|
||||
TenantTag,
|
||||
type TenantInfo,
|
||||
type CreateTenant,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { authedAdminTenantApi } from '#src/api/api.js';
|
||||
|
@ -15,25 +18,55 @@ describe('Tenant APIs', () => {
|
|||
it('should be able to create multiple tenants for `admin` role', async () => {
|
||||
const { client } = await createUserAndSignInToCloudClient(AdminTenantRole.Admin);
|
||||
const accessToken = await client.getAccessToken(cloudApiIndicator);
|
||||
const tenant1 = await createTenant(accessToken);
|
||||
const tenant2 = await createTenant(accessToken);
|
||||
expect(tenant1).toHaveProperty('id');
|
||||
expect(tenant2).toHaveProperty('id');
|
||||
const payload1 = {
|
||||
name: 'tenant1',
|
||||
tag: TenantTag.Staging,
|
||||
};
|
||||
const tenant1 = await createTenant(accessToken, payload1);
|
||||
const payload2 = {
|
||||
name: 'tenant2',
|
||||
tag: TenantTag.Production,
|
||||
};
|
||||
const tenant2 = await createTenant(accessToken, payload2);
|
||||
for (const [payload, tenant] of [
|
||||
[payload1, tenant1],
|
||||
[payload2, tenant2],
|
||||
] as Array<[Required<Pick<CreateTenant, 'name' | 'tag'>>, TenantInfo]>) {
|
||||
expect(tenant).toHaveProperty('id');
|
||||
expect(tenant).toHaveProperty('tag', payload.tag);
|
||||
expect(tenant).toHaveProperty('name', payload.name);
|
||||
}
|
||||
const tenants = await getTenants(accessToken);
|
||||
expect(tenants.length).toBeGreaterThan(2);
|
||||
expect(tenants.find((tenant) => tenant.id === tenant1.id)).toBeDefined();
|
||||
expect(tenants.find((tenant) => tenant.id === tenant2.id)).toBeDefined();
|
||||
expect(tenants.find((tenant) => tenant.id === tenant1.id)).toStrictEqual(tenant1);
|
||||
expect(tenants.find((tenant) => tenant.id === tenant2.id)).toStrictEqual(tenant2);
|
||||
});
|
||||
|
||||
it('should create only one tenant for `user` role', async () => {
|
||||
it('should be able to create multiple tenants for `user` role', async () => {
|
||||
const { client } = await createUserAndSignInToCloudClient(AdminTenantRole.User);
|
||||
const accessToken = await client.getAccessToken(cloudApiIndicator);
|
||||
const tenant1 = await createTenant(accessToken);
|
||||
await expect(createTenant(accessToken)).rejects.toThrow();
|
||||
expect(tenant1).toHaveProperty('id');
|
||||
const payload1 = {
|
||||
name: 'tenant1',
|
||||
tag: TenantTag.Staging,
|
||||
};
|
||||
const tenant1 = await createTenant(accessToken, payload1);
|
||||
const payload2 = {
|
||||
name: 'tenant2',
|
||||
tag: TenantTag.Development,
|
||||
};
|
||||
const tenant2 = await createTenant(accessToken, payload2);
|
||||
for (const [payload, tenant] of [
|
||||
[payload1, tenant1],
|
||||
[payload2, tenant2],
|
||||
] as Array<[Required<Pick<CreateTenant, 'name' | 'tag'>>, TenantInfo]>) {
|
||||
expect(tenant).toHaveProperty('id');
|
||||
expect(tenant).toHaveProperty('tag', payload.tag);
|
||||
expect(tenant).toHaveProperty('name', payload.name);
|
||||
}
|
||||
const tenants = await getTenants(accessToken);
|
||||
expect(tenants.length).toEqual(1);
|
||||
expect(tenants.find((tenant) => tenant.id === tenant1.id)).toBeDefined();
|
||||
expect(tenants.length).toEqual(2);
|
||||
expect(tenants.find((tenant) => tenant.id === tenant1.id)).toStrictEqual(tenant1);
|
||||
expect(tenants.find((tenant) => tenant.id === tenant2.id)).toStrictEqual(tenant2);
|
||||
});
|
||||
|
||||
it('`user` role should have `CloudScope.ManageTenantSelf` scope', async () => {
|
||||
|
@ -43,14 +76,16 @@ describe('Tenant APIs', () => {
|
|||
const scopes = await authedAdminTenantApi
|
||||
.get(`resources/${cloudApiResource!.id}/scopes`)
|
||||
.json<Scope[]>();
|
||||
const manageOwnTenantScope = scopes.find((scope) => scope.name === CloudScope.ManageTenantSelf);
|
||||
expect(manageOwnTenantScope).toBeDefined();
|
||||
const manageTenantSelfScope = scopes.find(
|
||||
(scope) => scope.name === CloudScope.ManageTenantSelf
|
||||
);
|
||||
expect(manageTenantSelfScope).toBeDefined();
|
||||
const roles = await authedAdminTenantApi.get('roles').json<Role[]>();
|
||||
const userRole = roles.find(({ name }) => name === 'user');
|
||||
expect(userRole).toBeDefined();
|
||||
const roleScopes = await authedAdminTenantApi
|
||||
.get(`roles/${userRole!.id}/scopes`)
|
||||
.json<Scope[]>();
|
||||
expect(roleScopes.find(({ id }) => id === manageOwnTenantScope!.id)).toBeDefined();
|
||||
expect(roleScopes.find(({ id }) => id === manageTenantSelfScope!.id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,5 +10,3 @@ export const adminTenantId = 'admin';
|
|||
* type, manually define it here for now.
|
||||
*/
|
||||
export type TenantModel = InferModelType<typeof Tenants>;
|
||||
export type CreateTenant = Pick<TenantModel, 'id' | 'dbUser' | 'dbUserPassword'> &
|
||||
Partial<Pick<TenantModel, 'name'>>;
|
||||
|
|
|
@ -1,10 +1,29 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { type TenantModel } from '../seeds/tenant.js';
|
||||
|
||||
export enum TenantTag {
|
||||
Development = 'development',
|
||||
Staging = 'staging',
|
||||
Production = 'production',
|
||||
}
|
||||
|
||||
export type TenantInfo = {
|
||||
id: string;
|
||||
indicator: string;
|
||||
};
|
||||
export type PatchTenant = Partial<Pick<TenantModel, 'name' | 'tag'>>;
|
||||
export type CreateTenant = Pick<TenantModel, 'id' | 'dbUser' | 'dbUserPassword'> &
|
||||
PatchTenant & { createdAt?: number };
|
||||
|
||||
export const createTenantGuard = z.object({
|
||||
id: z.string(),
|
||||
dbUser: z.string(),
|
||||
dbUserPassword: z.string(),
|
||||
name: z.string().optional(),
|
||||
tag: z.nativeEnum(TenantTag).optional(),
|
||||
createdAt: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TenantInfo = Pick<TenantModel, 'id' | 'name' | 'tag'> & { indicator: string };
|
||||
|
||||
export const tenantInfoGuard: z.ZodType<TenantInfo> = createTenantGuard
|
||||
.pick({ id: true, name: true, tag: true })
|
||||
.extend({ indicator: z.string() })
|
||||
.required();
|
||||
|
|
Loading…
Reference in a new issue