diff --git a/packages/core/src/routes/organization/invitations.openapi.json b/packages/core/src/routes/organization-invitation/index.openapi.json similarity index 100% rename from packages/core/src/routes/organization/invitations.openapi.json rename to packages/core/src/routes/organization-invitation/index.openapi.json diff --git a/packages/core/src/routes/organization/invitations.ts b/packages/core/src/routes/organization-invitation/index.ts similarity index 98% rename from packages/core/src/routes/organization/invitations.ts rename to packages/core/src/routes/organization-invitation/index.ts index c7e886503ae..c988850a783 100644 --- a/packages/core/src/routes/organization/invitations.ts +++ b/packages/core/src/routes/organization-invitation/index.ts @@ -11,10 +11,9 @@ import koaGuard from '#src/middleware/koa-guard.js'; 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( ...[ originalRouter, diff --git a/packages/core/src/routes/organization/roles.openapi.json b/packages/core/src/routes/organization-role/index.openapi.json similarity index 100% rename from packages/core/src/routes/organization/roles.openapi.json rename to packages/core/src/routes/organization-role/index.openapi.json diff --git a/packages/core/src/routes/organization/roles.ts b/packages/core/src/routes/organization-role/index.ts similarity index 98% rename from packages/core/src/routes/organization/roles.ts rename to packages/core/src/routes/organization-role/index.ts index e53980a3cec..32951fa55af 100644 --- a/packages/core/src/routes/organization/roles.ts +++ b/packages/core/src/routes/organization-role/index.ts @@ -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( ...[ originalRouter, diff --git a/packages/core/src/routes/organization/scopes.openapi.json b/packages/core/src/routes/organization-scope/index.openapi.json similarity index 100% rename from packages/core/src/routes/organization/scopes.openapi.json rename to packages/core/src/routes/organization-scope/index.openapi.json diff --git a/packages/core/src/routes/organization/scopes.ts b/packages/core/src/routes/organization-scope/index.ts similarity index 92% rename from packages/core/src/routes/organization/scopes.ts rename to packages/core/src/routes/organization-scope/index.ts index 39f635fe357..f86f07ab521 100644 --- a/packages/core/src/routes/organization/scopes.ts +++ b/packages/core/src/routes/organization-scope/index.ts @@ -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( ...[ originalRouter, diff --git a/packages/core/src/routes/organization/index.applications.openapi.json b/packages/core/src/routes/organization/application/index.openapi.json similarity index 95% rename from packages/core/src/routes/organization/index.applications.openapi.json rename to packages/core/src/routes/organization/application/index.openapi.json index 23bebb126cf..2f75e22ad88 100644 --- a/packages/core/src/routes/organization/index.applications.openapi.json +++ b/packages/core/src/routes/organization/application/index.openapi.json @@ -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": { @@ -17,6 +18,7 @@ } }, "post": { + "tags": ["Dev feature"], "summary": "Add organization application", "description": "Add an application to the organization.", "requestBody": { @@ -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": { @@ -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": { @@ -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": { @@ -89,6 +94,7 @@ } }, "post": { + "tags": ["Dev feature"], "summary": "Add organization application role", "description": "Add a role to the application in the organization.", "requestBody": { @@ -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": { @@ -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": { diff --git a/packages/core/src/routes/organization/application/index.ts b/packages/core/src/routes/organization/application/index.ts new file mode 100644 index 00000000000..63da85806ae --- /dev/null +++ b/packages/core/src/routes/organization/application/index.ts @@ -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, + 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); + } +} diff --git a/packages/core/src/routes/organization/index.application-role-relations.ts b/packages/core/src/routes/organization/application/role-relations.ts similarity index 100% rename from packages/core/src/routes/organization/index.application-role-relations.ts rename to packages/core/src/routes/organization/application/role-relations.ts diff --git a/packages/core/src/routes/organization/index.ts b/packages/core/src/routes/organization/index.ts index 7b68a455f14..cf091681393 100644 --- a/packages/core/src/routes/organization/index.ts +++ b/packages/core/src/routes/organization/index.ts @@ -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( @@ -83,81 +76,9 @@ export default function organizationRoutes( } ); - // 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); diff --git a/packages/core/src/routes/organization/index.jit.email-domains.openapi.json b/packages/core/src/routes/organization/jit/email-domains.openapi.json similarity index 100% rename from packages/core/src/routes/organization/index.jit.email-domains.openapi.json rename to packages/core/src/routes/organization/jit/email-domains.openapi.json diff --git a/packages/core/src/routes/organization/index.jit.email-domains.ts b/packages/core/src/routes/organization/jit/email-domains.ts similarity index 100% rename from packages/core/src/routes/organization/index.jit.email-domains.ts rename to packages/core/src/routes/organization/jit/email-domains.ts diff --git a/packages/core/src/routes/organization/jit/index.ts b/packages/core/src/routes/organization/jit/index.ts new file mode 100644 index 00000000000..6c160f98c91 --- /dev/null +++ b/packages/core/src/routes/organization/jit/index.ts @@ -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, + organizations: OrganizationQueries +) { + emailDomainRoutes(router, organizations); + router.addRelationRoutes(organizations.jit.roles, 'jit/roles', { isPaginationOptional: true }); + router.addRelationRoutes(organizations.jit.ssoConnectors, 'jit/sso-connectors', { + isPaginationOptional: true, + }); +} diff --git a/packages/core/src/routes/organization/index.jit.roles.openapi.json b/packages/core/src/routes/organization/jit/roles.openapi.json similarity index 100% rename from packages/core/src/routes/organization/index.jit.roles.openapi.json rename to packages/core/src/routes/organization/jit/roles.openapi.json diff --git a/packages/core/src/routes/organization/index.jit.sso-connectors.openapi.json b/packages/core/src/routes/organization/jit/sso-connectors.openapi.json similarity index 100% rename from packages/core/src/routes/organization/index.jit.sso-connectors.openapi.json rename to packages/core/src/routes/organization/jit/sso-connectors.openapi.json diff --git a/packages/core/src/routes/organization/index.users.openapi.json b/packages/core/src/routes/organization/user/index.openapi.json similarity index 100% rename from packages/core/src/routes/organization/index.users.openapi.json rename to packages/core/src/routes/organization/user/index.openapi.json diff --git a/packages/core/src/routes/organization/user/index.ts b/packages/core/src/routes/organization/user/index.ts new file mode 100644 index 00000000000..9e90f3c9316 --- /dev/null +++ b/packages/core/src/routes/organization/user/index.ts @@ -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, + 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(); + } + ); + + 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); +} diff --git a/packages/core/src/routes/organization/index.user-role-relations.ts b/packages/core/src/routes/organization/user/role-relations.ts similarity index 93% rename from packages/core/src/routes/organization/index.user-role-relations.ts rename to packages/core/src/routes/organization/user/role-relations.ts index fa6898f9388..67ace52d176 100644 --- a/packages/core/src/routes/organization/index.user-role-relations.ts +++ b/packages/core/src/routes/organization/user/role-relations.ts @@ -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, + router: SchemaRouter, organizations: OrganizationQueries ) { const params = Object.freeze({ id: z.string().min(1), userId: z.string().min(1) } as const);