@@ -3,9 +3,12 @@ import {
33 type CreateOrganization ,
44 type Organization ,
55 userWithOrganizationRolesGuard ,
6+ Users ,
67} from '@logto/schemas' ;
78import { z } from 'zod' ;
89
10+ import { buildManagementApiContext } from '#src/libraries/hook/utils.js' ;
11+ import { type QuotaLibrary } from '#src/libraries/quota.js' ;
912import koaGuard from '#src/middleware/koa-guard.js' ;
1013import koaPagination from '#src/middleware/koa-pagination.js' ;
1114import type OrganizationQueries from '#src/queries/organization/index.js' ;
@@ -18,13 +21,9 @@ import userRoleRelationRoutes from './role-relations.js';
1821/** Mounts the user-related routes on the organization router. */
1922export default function userRoutes (
2023 router : SchemaRouter < OrganizationKeys , CreateOrganization , Organization > ,
21- organizations : OrganizationQueries
24+ organizations : OrganizationQueries ,
25+ quota : QuotaLibrary
2226) {
23- router . addRelationRoutes ( organizations . relations . users , undefined , {
24- disabled : { get : true } ,
25- hookEvent : 'Organization.Membership.Updated' ,
26- } ) ;
27-
2827 router . get (
2928 '/:id/users' ,
3029 koaPagination ( ) ,
@@ -50,6 +49,107 @@ export default function userRoutes(
5049 }
5150 ) ;
5251
52+ router . post (
53+ '/:id/users' ,
54+ koaGuard ( {
55+ params : z . object ( { id : z . string ( ) . min ( 1 ) } ) ,
56+ body : z . object ( { userIds : z . string ( ) . min ( 1 ) . array ( ) . nonempty ( ) } ) ,
57+ status : [ 201 , 403 , 422 ] ,
58+ } ) ,
59+ async ( ctx , next ) => {
60+ const {
61+ params : { id } ,
62+ body : { userIds } ,
63+ } = ctx . guard ;
64+
65+ // Check quota limit before adding users
66+ await quota . guardTenantUsageByKey ( 'usersPerOrganizationLimit' , {
67+ entityId : id ,
68+ consumeUsageCount : userIds . length ,
69+ } ) ;
70+
71+ await organizations . relations . users . insert (
72+ ...userIds . map ( ( userId ) => ( { organizationId : id , userId } ) )
73+ ) ;
74+
75+ ctx . status = 201 ;
76+
77+ // Trigger hook event
78+ ctx . appendDataHookContext ( 'Organization.Membership.Updated' , {
79+ ...buildManagementApiContext ( ctx ) ,
80+ organizationId : id ,
81+ } ) ;
82+
83+ return next ( ) ;
84+ }
85+ ) ;
86+
87+ router . put (
88+ '/:id/users' ,
89+ koaGuard ( {
90+ params : z . object ( { id : z . string ( ) . min ( 1 ) } ) ,
91+ body : z . object ( { userIds : z . string ( ) . min ( 1 ) . array ( ) } ) ,
92+ status : [ 204 , 403 , 422 ] ,
93+ } ) ,
94+ async ( ctx , next ) => {
95+ const {
96+ params : { id } ,
97+ body : { userIds } ,
98+ } = ctx . guard ;
99+
100+ // For replace operation, calculate the delta (new count - current count)
101+ // Only check quota if we're adding more users than currently exist
102+ const [ currentCount ] = await organizations . relations . users . getEntities ( Users , {
103+ organizationId : id ,
104+ } ) ;
105+ const delta = userIds . length - currentCount ;
106+
107+ if ( delta > 0 ) {
108+ await quota . guardTenantUsageByKey ( 'usersPerOrganizationLimit' , {
109+ entityId : id ,
110+ consumeUsageCount : delta ,
111+ } ) ;
112+ }
113+
114+ await organizations . relations . users . replace ( id , userIds ) ;
115+
116+ ctx . status = 204 ;
117+
118+ // Trigger hook event
119+ ctx . appendDataHookContext ( 'Organization.Membership.Updated' , {
120+ ...buildManagementApiContext ( ctx ) ,
121+ organizationId : id ,
122+ } ) ;
123+
124+ return next ( ) ;
125+ }
126+ ) ;
127+
128+ router . delete (
129+ '/:id/users/:userId' ,
130+ koaGuard ( {
131+ params : z . object ( { id : z . string ( ) . min ( 1 ) , userId : z . string ( ) . min ( 1 ) } ) ,
132+ status : [ 204 , 422 ] ,
133+ } ) ,
134+ async ( ctx , next ) => {
135+ const {
136+ params : { id, userId } ,
137+ } = ctx . guard ;
138+
139+ await organizations . relations . users . delete ( { organizationId : id , userId } ) ;
140+
141+ ctx . status = 204 ;
142+
143+ // Trigger hook event
144+ ctx . appendDataHookContext ( 'Organization.Membership.Updated' , {
145+ ...buildManagementApiContext ( ctx ) ,
146+ organizationId : id ,
147+ } ) ;
148+
149+ return next ( ) ;
150+ }
151+ ) ;
152+
53153 router . post (
54154 '/:id/users/roles' ,
55155 koaGuard ( {
0 commit comments