Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): reorg organization routes #6078

Merged
merged 2 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ jobs:
- uses: logto-io/actions-package-logto-artifact@v2
with:
artifact-name: dev-feature-disabled-integration-test-${{ github.sha }}
branch: ${{github.base_ref}}
pnpm-version: 9

run-logto:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
import SchemaRouter from '#src/utils/SchemaRouter.js';
import assertThat from '#src/utils/assert-that.js';

import { errorHandler } from '../organization/utils.js';
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';

import { errorHandler } from './utils.js';

export default function organizationInvitationRoutes<T extends ManagementApiRouter>(
...[
originalRouter,
Expand Down Expand Up @@ -136,7 +135,7 @@
return next();
}

// TODO: Error i18n

Check warning on line 138 in packages/core/src/routes/organization-invitation/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/core/src/routes/organization-invitation/index.ts#L138

[no-warning-comments] Unexpected 'todo' comment: 'TODO: Error i18n'.
assertThat(
acceptedUserId,
new RequestError({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ import { organizationRoleSearchKeys } from '#src/queries/organization/index.js';
import SchemaRouter from '#src/utils/SchemaRouter.js';
import { parseSearchOptions } from '#src/utils/search.js';

import { errorHandler } from '../organization/utils.js';
import {
type ManagementApiRouter,
type ManagementApiRouterContext,
type RouterInitArgs,
} from '../types.js';

import { errorHandler } from './utils.js';

export default function organizationRoleRoutes<T extends ManagementApiRouter>(
...[
originalRouter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { OrganizationScopes } from '@logto/schemas';
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
import SchemaRouter from '#src/utils/SchemaRouter.js';

import { errorHandler } from '../organization/utils.js';
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';

import { errorHandler } from './utils.js';

export default function organizationScopeRoutes<T extends ManagementApiRouter>(
...[
originalRouter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"paths": {
"/api/organizations/{id}/applications": {
"get": {
"tags": ["Dev feature"],
"summary": "Get organization applications",
"description": "Get applications associated with the organization.",
"responses": {
Expand All @@ -17,6 +18,7 @@
}
},
"post": {
"tags": ["Dev feature"],
"summary": "Add organization application",
"description": "Add an application to the organization.",
"requestBody": {
Expand All @@ -42,6 +44,7 @@
}
},
"put": {
"tags": ["Dev feature"],
"summary": "Replace organization applications",
"description": "Replace all applications associated with the organization with the given data.",
"requestBody": {
Expand Down Expand Up @@ -69,6 +72,7 @@
},
"/api/organizations/{id}/applications/{applicationId}": {
"delete": {
"tags": ["Dev feature"],
"summary": "Remove organization application",
"description": "Remove an application from the organization.",
"responses": {
Expand All @@ -80,6 +84,7 @@
},
"/api/organizations/{id}/applications/{applicationId}/roles": {
"get": {
"tags": ["Dev feature"],
"summary": "Get organization application roles",
"description": "Get roles associated with the application in the organization.",
"responses": {
Expand All @@ -89,6 +94,7 @@
}
},
"post": {
"tags": ["Dev feature"],
"summary": "Add organization application role",
"description": "Add a role to the application in the organization.",
"requestBody": {
Expand All @@ -114,6 +120,7 @@
}
},
"put": {
"tags": ["Dev feature"],
"summary": "Replace organization application roles",
"description": "Replace all roles associated with the application in the organization with the given data.",
"requestBody": {
Expand Down Expand Up @@ -141,6 +148,7 @@
},
"/api/organizations/{id}/applications/{applicationId}/roles/{organizationRoleId}": {
"delete": {
"tags": ["Dev feature"],
"summary": "Remove organization application role",
"description": "Remove a role from the application in the organization.",
"responses": {
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/routes/organization/application/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type OrganizationKeys, type CreateOrganization, type Organization } from '@logto/schemas';

import { EnvSet } from '#src/env-set/index.js';
import type OrganizationQueries from '#src/queries/organization/index.js';
import type SchemaRouter from '#src/utils/SchemaRouter.js';

import applicationRoleRelationRoutes from './role-relations.js';

/** Mounts the application-related routes on the organization router. */
export default function applicationRoutes(
router: SchemaRouter<OrganizationKeys, CreateOrganization, Organization>,
organizations: OrganizationQueries
) {
if (EnvSet.values.isDevFeaturesEnabled) {
// MARK: Organization - application relation routes
router.addRelationRoutes(organizations.relations.apps, undefined, {
hookEvent: 'Organization.Membership.Updated',
});

// MARK: Organization - application role relation routes
applicationRoleRelationRoutes(router, organizations);
}
}
99 changes: 10 additions & 89 deletions packages/core/src/routes/organization/index.ts
gao-sun marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import {
type OrganizationWithFeatured,
Organizations,
featuredUserGuard,
userWithOrganizationRolesGuard,
} from '@logto/schemas';
import { type OrganizationWithFeatured, Organizations, featuredUserGuard } from '@logto/schemas';
import { yes } from '@silverhand/essentials';
import { z } from 'zod';

import { EnvSet } from '#src/env-set/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
import { userSearchKeys } from '#src/queries/user.js';
import SchemaRouter from '#src/utils/SchemaRouter.js';
import { parseSearchOptions } from '#src/utils/search.js';

import organizationInvitationRoutes from '../organization-invitation/index.js';
import organizationRoleRoutes from '../organization-role/index.js';
import organizationScopeRoutes from '../organization-scope/index.js';
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';

import applicationRoleRelationRoutes from './index.application-role-relations.js';
import emailDomainRoutes from './index.jit.email-domains.js';
import userRoleRelationRoutes from './index.user-role-relations.js';
import organizationInvitationRoutes from './invitations.js';
import organizationRoleRoutes from './roles.js';
import organizationScopeRoutes from './scopes.js';
import applicationRoutes from './application/index.js';
import jitRoutes from './jit/index.js';
import userRoutes from './user/index.js';
import { errorHandler } from './utils.js';

export default function organizationRoutes<T extends ManagementApiRouter>(
Expand Down Expand Up @@ -83,81 +76,9 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
}
);

// MARK: Organization - user relation routes
router.addRelationRoutes(organizations.relations.users, undefined, {
disabled: { get: true },
hookEvent: 'Organization.Membership.Updated',
});

router.get(
'/:id/users',
koaPagination(),
koaGuard({
query: z.object({ q: z.string().optional() }),
params: z.object({ id: z.string().min(1) }),
response: userWithOrganizationRolesGuard.array(),
status: [200, 404],
}),
async (ctx, next) => {
const search = parseSearchOptions(userSearchKeys, ctx.guard.query);

const [totalCount, entities] = await organizations.relations.users.getUsersByOrganizationId(
ctx.guard.params.id,
ctx.pagination,
search
);

ctx.pagination.totalCount = totalCount;
ctx.body = entities;

return next();
}
);

// MARK: Organization - user role relation routes
router.post(
'/:id/users/roles',
koaGuard({
params: z.object({ id: z.string().min(1) }),
body: z.object({
userIds: z.string().min(1).array().nonempty(),
organizationRoleIds: z.string().min(1).array().nonempty(),
}),
status: [201, 422],
}),
async (ctx, next) => {
const { id } = ctx.guard.params;
const { userIds, organizationRoleIds } = ctx.guard.body;

await organizations.relations.usersRoles.insert(
...organizationRoleIds.flatMap((roleId) =>
userIds.map((userId) => ({ organizationId: id, organizationRoleId: roleId, userId }))
)
);

ctx.status = 201;
return next();
}
);

userRoleRelationRoutes(router, organizations);

if (EnvSet.values.isDevFeaturesEnabled) {
// MARK: Organization - application relation routes
router.addRelationRoutes(organizations.relations.apps, undefined, {
hookEvent: 'Organization.Membership.Updated',
});

// MARK: Organization - application role relation routes
applicationRoleRelationRoutes(router, organizations);
}

// MARK: Just-in-time provisioning
emailDomainRoutes(router, organizations);
router.addRelationRoutes(organizations.jit.roles, 'jit/roles', { isPaginationOptional: true });
router.addRelationRoutes(organizations.jit.ssoConnectors, 'jit/sso-connectors', {
isPaginationOptional: true,
});
userRoutes(router, organizations);
applicationRoutes(router, organizations);
jitRoutes(router, organizations);

// MARK: Mount sub-routes
organizationRoleRoutes(...args);
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/routes/organization/jit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type OrganizationKeys, type CreateOrganization, type Organization } from '@logto/schemas';

import type OrganizationQueries from '#src/queries/organization/index.js';
import type SchemaRouter from '#src/utils/SchemaRouter.js';

import emailDomainRoutes from './email-domains.js';

/** Mounts the jit-related routes on the organization router. */
export default function jitRoutes(
router: SchemaRouter<OrganizationKeys, CreateOrganization, Organization>,
organizations: OrganizationQueries
) {
emailDomainRoutes(router, organizations);
router.addRelationRoutes(organizations.jit.roles, 'jit/roles', { isPaginationOptional: true });
router.addRelationRoutes(organizations.jit.ssoConnectors, 'jit/sso-connectors', {
isPaginationOptional: true,
});
}
79 changes: 79 additions & 0 deletions packages/core/src/routes/organization/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
type OrganizationKeys,
type CreateOrganization,
type Organization,
userWithOrganizationRolesGuard,
} from '@logto/schemas';
import { z } from 'zod';

import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import type OrganizationQueries from '#src/queries/organization/index.js';
import { userSearchKeys } from '#src/queries/user.js';
import type SchemaRouter from '#src/utils/SchemaRouter.js';
import { parseSearchOptions } from '#src/utils/search.js';

import userRoleRelationRoutes from './role-relations.js';

/** Mounts the user-related routes on the organization router. */
export default function userRoutes(
router: SchemaRouter<OrganizationKeys, CreateOrganization, Organization>,
organizations: OrganizationQueries
) {
router.addRelationRoutes(organizations.relations.users, undefined, {
disabled: { get: true },
hookEvent: 'Organization.Membership.Updated',
});

router.get(
'/:id/users',
koaPagination(),
koaGuard({
query: z.object({ q: z.string().optional() }),
params: z.object({ id: z.string().min(1) }),
response: userWithOrganizationRolesGuard.array(),
status: [200, 404],
}),
async (ctx, next) => {
const search = parseSearchOptions(userSearchKeys, ctx.guard.query);

const [totalCount, entities] = await organizations.relations.users.getUsersByOrganizationId(
ctx.guard.params.id,
ctx.pagination,
search
);

ctx.pagination.totalCount = totalCount;
ctx.body = entities;

return next();
}

Check warning on line 50 in packages/core/src/routes/organization/user/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/user/index.ts#L38-L50

Added lines #L38 - L50 were not covered by tests
);

router.post(
'/:id/users/roles',
koaGuard({
params: z.object({ id: z.string().min(1) }),
body: z.object({
userIds: z.string().min(1).array().nonempty(),
organizationRoleIds: z.string().min(1).array().nonempty(),
}),
status: [201, 422],
}),
async (ctx, next) => {
const { id } = ctx.guard.params;
const { userIds, organizationRoleIds } = ctx.guard.body;

await organizations.relations.usersRoles.insert(
...organizationRoleIds.flatMap((roleId) =>
userIds.map((userId) => ({ organizationId: id, organizationRoleId: roleId, userId }))
)
);

ctx.status = 201;
return next();
}

Check warning on line 75 in packages/core/src/routes/organization/user/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/user/index.ts#L64-L75

Added lines #L64 - L75 were not covered by tests
);

userRoleRelationRoutes(router, organizations);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { OrganizationRoles, OrganizationScopes } from '@logto/schemas';
import type Router from 'koa-router';
import {
type CreateOrganization,
type Organization,
type OrganizationKeys,
OrganizationRoles,
OrganizationScopes,
} from '@logto/schemas';
import { z } from 'zod';

import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import { type WithHookContext } from '#src/middleware/koa-management-api-hooks.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import type OrganizationQueries from '#src/queries/organization/index.js';
import type SchemaRouter from '#src/utils/SchemaRouter.js';

// Manually add these routes since I don't want to over-engineer the `SchemaRouter`.
// Update: Now we also have "organization - organization role - application" relations. Consider
// extracting the common logic to a class once we have one more relation like this.
export default function userRoleRelationRoutes(
router: Router<unknown, WithHookContext>,
router: SchemaRouter<OrganizationKeys, CreateOrganization, Organization>,
organizations: OrganizationQueries
) {
const params = Object.freeze({ id: z.string().min(1), userId: z.string().min(1) } as const);
Expand Down
Loading