mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(cloud): init service route (#3397)
This commit is contained in:
parent
0978db5790
commit
2bb570da4b
8 changed files with 114 additions and 10 deletions
16
packages/cloud/src/libraries/services.ts
Normal file
16
packages/cloud/src/libraries/services.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { adminTenantId } from '@logto/schemas';
|
||||||
|
|
||||||
|
import type { Queries } from '#src/queries/index.js';
|
||||||
|
|
||||||
|
export class ServicesLibrary {
|
||||||
|
constructor(public readonly queries: Queries) {}
|
||||||
|
|
||||||
|
async getTenantIdFromApplicationId(applicationId: string) {
|
||||||
|
const application = await this.queries.applications.findApplicationById(
|
||||||
|
applicationId,
|
||||||
|
adminTenantId
|
||||||
|
);
|
||||||
|
|
||||||
|
return application.customClientMetadata.tenantId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import type { CreateApplication, CreateApplicationsRole } from '@logto/schemas';
|
import type { Application, CreateApplication, CreateApplicationsRole } from '@logto/schemas';
|
||||||
import type { PostgreSql } from '@withtyped/postgres';
|
import type { PostgreSql } from '@withtyped/postgres';
|
||||||
|
import { sql } from '@withtyped/postgres';
|
||||||
import type { Queryable } from '@withtyped/server';
|
import type { Queryable } from '@withtyped/server';
|
||||||
|
|
||||||
import { insertInto } from '#src/utils/query.js';
|
import { insertInto } from '#src/utils/query.js';
|
||||||
|
@ -13,5 +14,24 @@ export const createApplicationsQueries = (client: Queryable<PostgreSql>) => {
|
||||||
const assignRoleToApplication = async (data: CreateApplicationsRole) =>
|
const assignRoleToApplication = async (data: CreateApplicationsRole) =>
|
||||||
client.query(insertInto(data, 'applications_roles'));
|
client.query(insertInto(data, 'applications_roles'));
|
||||||
|
|
||||||
return { insertApplication, assignRoleToApplication };
|
const findApplicationById = async (id: string, tenantId: string) => {
|
||||||
|
// TODO implement "buildFindById" in withTyped
|
||||||
|
const { rows } = await client.query<Application>(sql`
|
||||||
|
select id, name, secret, description,
|
||||||
|
custom_client_metadata as "customClientMetadata",
|
||||||
|
created_at as "createdAt",
|
||||||
|
oidc_client_metadata as "oidcClientMetadata"
|
||||||
|
from applications
|
||||||
|
where id=${id}
|
||||||
|
and tenant_id=${tenantId}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!rows[0]) {
|
||||||
|
throw new Error(`Application ${id} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
return { insertApplication, assignRoleToApplication, findApplicationById };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { createRouter } from '@withtyped/server';
|
import { createRouter } from '@withtyped/server';
|
||||||
|
|
||||||
|
import { ServicesLibrary } from '#src/libraries/services.js';
|
||||||
import { TenantsLibrary } from '#src/libraries/tenants.js';
|
import { TenantsLibrary } from '#src/libraries/tenants.js';
|
||||||
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
||||||
import { Queries } from '#src/queries/index.js';
|
import { Queries } from '#src/queries/index.js';
|
||||||
|
|
||||||
import { createTenants } from './tenants.js';
|
import { servicesRoutes } from './services.js';
|
||||||
|
import { tenantsRoutes } from './tenants.js';
|
||||||
|
|
||||||
const router = createRouter<WithAuthContext, '/api'>('/api').pack(
|
const router = createRouter<WithAuthContext, '/api'>('/api')
|
||||||
createTenants(new TenantsLibrary(Queries.default))
|
.pack(tenantsRoutes(new TenantsLibrary(Queries.default)))
|
||||||
);
|
.pack(servicesRoutes(new ServicesLibrary(Queries.default)));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
34
packages/cloud/src/routes/service.test.ts
Normal file
34
packages/cloud/src/routes/service.test.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { CloudScope } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { buildRequestAuthContext, createHttpContext } from '#src/test-utils/context.js';
|
||||||
|
import { noop } from '#src/test-utils/function.js';
|
||||||
|
import { MockServicesLibrary } from '#src/test-utils/libraries.js';
|
||||||
|
|
||||||
|
import { servicesRoutes } from './services.js';
|
||||||
|
|
||||||
|
describe('POST /api/services/send-email', () => {
|
||||||
|
const library = new MockServicesLibrary();
|
||||||
|
const router = servicesRoutes(library);
|
||||||
|
|
||||||
|
it('should throw 403 when lack of permission', async () => {
|
||||||
|
await expect(
|
||||||
|
router.routes()(
|
||||||
|
buildRequestAuthContext('POST /services/send-email')(),
|
||||||
|
noop,
|
||||||
|
createHttpContext()
|
||||||
|
)
|
||||||
|
).rejects.toMatchObject({ status: 403 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 201', async () => {
|
||||||
|
library.getTenantIdFromApplicationId.mockResolvedValueOnce('tenantId');
|
||||||
|
|
||||||
|
await router.routes()(
|
||||||
|
buildRequestAuthContext('POST /services/send-email')([CloudScope.SendEmail]),
|
||||||
|
async ({ status }) => {
|
||||||
|
expect(status).toBe(201);
|
||||||
|
},
|
||||||
|
createHttpContext()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
23
packages/cloud/src/routes/services.ts
Normal file
23
packages/cloud/src/routes/services.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { CloudScope } from '@logto/schemas';
|
||||||
|
import { createRouter, RequestError } from '@withtyped/server';
|
||||||
|
|
||||||
|
import type { ServicesLibrary } from '#src/libraries/services.js';
|
||||||
|
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
||||||
|
|
||||||
|
export const servicesRoutes = (library: ServicesLibrary) =>
|
||||||
|
createRouter<WithAuthContext, '/services'>('/services').post(
|
||||||
|
'/send-email',
|
||||||
|
{},
|
||||||
|
async (context, next) => {
|
||||||
|
if (![CloudScope.SendEmail].some((scope) => context.auth.scopes.includes(scope))) {
|
||||||
|
throw new RequestError('Forbidden due to lack of permission.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantId = await library.getTenantIdFromApplicationId(context.auth.id);
|
||||||
|
|
||||||
|
// TODO send email
|
||||||
|
console.log(tenantId);
|
||||||
|
|
||||||
|
return next({ ...context, status: 201 });
|
||||||
|
}
|
||||||
|
);
|
|
@ -5,11 +5,11 @@ import { buildRequestAuthContext, createHttpContext } from '#src/test-utils/cont
|
||||||
import { noop } from '#src/test-utils/function.js';
|
import { noop } from '#src/test-utils/function.js';
|
||||||
import { MockTenantsLibrary } from '#src/test-utils/libraries.js';
|
import { MockTenantsLibrary } from '#src/test-utils/libraries.js';
|
||||||
|
|
||||||
import { createTenants } from './tenants.js';
|
import { tenantsRoutes } from './tenants.js';
|
||||||
|
|
||||||
describe('GET /api/tenants', () => {
|
describe('GET /api/tenants', () => {
|
||||||
const library = new MockTenantsLibrary();
|
const library = new MockTenantsLibrary();
|
||||||
const router = createTenants(library);
|
const router = tenantsRoutes(library);
|
||||||
|
|
||||||
it('should return whatever the library returns', async () => {
|
it('should return whatever the library returns', async () => {
|
||||||
const tenants: TenantInfo[] = [{ id: 'tenant_a', indicator: 'https://foo.bar' }];
|
const tenants: TenantInfo[] = [{ id: 'tenant_a', indicator: 'https://foo.bar' }];
|
||||||
|
@ -27,7 +27,7 @@ describe('GET /api/tenants', () => {
|
||||||
|
|
||||||
describe('POST /api/tenants', () => {
|
describe('POST /api/tenants', () => {
|
||||||
const library = new MockTenantsLibrary();
|
const library = new MockTenantsLibrary();
|
||||||
const router = createTenants(library);
|
const router = tenantsRoutes(library);
|
||||||
|
|
||||||
it('should throw 403 when lack of permission', async () => {
|
it('should throw 403 when lack of permission', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { TenantsLibrary } from '#src/libraries/tenants.js';
|
||||||
import { tenantInfoGuard } from '#src/libraries/tenants.js';
|
import { tenantInfoGuard } from '#src/libraries/tenants.js';
|
||||||
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
||||||
|
|
||||||
export const createTenants = (library: TenantsLibrary) =>
|
export const tenantsRoutes = (library: TenantsLibrary) =>
|
||||||
createRouter<WithAuthContext, '/tenants'>('/tenants')
|
createRouter<WithAuthContext, '/tenants'>('/tenants')
|
||||||
.get('/', { response: tenantInfoGuard.array() }, async (context, next) => {
|
.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) });
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { TenantInfo } from '@logto/schemas';
|
import type { TenantInfo } from '@logto/schemas';
|
||||||
|
|
||||||
|
import type { ServicesLibrary } from '#src/libraries/services.js';
|
||||||
import type { TenantsLibrary } from '#src/libraries/tenants.js';
|
import type { TenantsLibrary } from '#src/libraries/tenants.js';
|
||||||
import type { Queries } from '#src/queries/index.js';
|
import type { Queries } from '#src/queries/index.js';
|
||||||
|
|
||||||
|
@ -13,3 +14,11 @@ export class MockTenantsLibrary implements TenantsLibrary {
|
||||||
public getAvailableTenants = jest.fn<Promise<TenantInfo[]>, [string]>();
|
public getAvailableTenants = jest.fn<Promise<TenantInfo[]>, [string]>();
|
||||||
public createNewTenant = jest.fn<Promise<TenantInfo>, [string]>();
|
public createNewTenant = jest.fn<Promise<TenantInfo>, [string]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MockServicesLibrary implements ServicesLibrary {
|
||||||
|
public get queries(): Queries {
|
||||||
|
throw new Error('Not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTenantIdFromApplicationId = jest.fn<Promise<string>, [string]>();
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue