1
+ import { Request , Response , NextFunction } from "express" ;
2
+ import { z } from "zod" ;
3
+ import { db } from "@server/db" ;
4
+ import {
5
+ resources ,
6
+ orgs ,
7
+ userOrgs ,
8
+ userResources ,
9
+ roleResources ,
10
+ roles ,
11
+ users
12
+ } from "@server/db" ;
13
+ import { eq , and } from "drizzle-orm" ;
14
+ import response from "@server/lib/response" ;
15
+ import HttpCode from "@server/types/HttpCode" ;
16
+ import createHttpError from "http-errors" ;
17
+ import logger from "@server/logger" ;
18
+ import { fromError } from "zod-validation-error" ;
19
+ import { registry , OpenAPITags } from "@server/openApi" ;
20
+
21
+ const getMoveImpactParamsSchema = z . object ( {
22
+ resourceId : z . string ( ) . transform ( Number ) . pipe ( z . number ( ) . int ( ) . positive ( ) ) ,
23
+ } ) ;
24
+
25
+ const getMoveImpactQuerySchema = z . object ( {
26
+ targetOrgId : z . string ( ) . min ( 1 )
27
+ } ) ;
28
+
29
+ registry . registerPath ( {
30
+ method : "get" ,
31
+ path : "/resource/{resourceId}/move-impact" ,
32
+ description : "Get the impact analysis of moving a resource to a different org" ,
33
+ tags : [ OpenAPITags . Resource ] ,
34
+ request : {
35
+ params : getMoveImpactParamsSchema ,
36
+ query : getMoveImpactQuerySchema
37
+ } ,
38
+ responses : {
39
+ 200 : {
40
+ description : "Move impact analysis" ,
41
+ content : {
42
+ "application/json" : {
43
+ schema : {
44
+ type : "object" ,
45
+ properties : {
46
+ data : {
47
+ type : "object" ,
48
+ properties : {
49
+ resourceId : { type : "number" } ,
50
+ resourceName : { type : "string" } ,
51
+ currentOrgId : { type : "string" } ,
52
+ currentOrgName : { type : "string" } ,
53
+ targetOrgId : { type : "string" } ,
54
+ targetOrgName : { type : "string" } ,
55
+ impact : {
56
+ type : "object" ,
57
+ properties : {
58
+ rolePermissions : {
59
+ type : "object" ,
60
+ properties : {
61
+ count : { type : "number" } ,
62
+ details : {
63
+ type : "array" ,
64
+ items : {
65
+ type : "object" ,
66
+ properties : {
67
+ roleId : { type : "number" } ,
68
+ roleName : { type : "string" }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ } ,
74
+ userPermissions : {
75
+ type : "object" ,
76
+ properties : {
77
+ count : { type : "number" } ,
78
+ details : {
79
+ type : "array" ,
80
+ items : {
81
+ type : "object" ,
82
+ properties : {
83
+ userId : { type : "string" } ,
84
+ username : { type : "string" } ,
85
+ email : { type : "string" }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ } ,
91
+ totalImpactedPermissions : { type : "number" } ,
92
+ authenticationPreserved : { type : "boolean" } ,
93
+ movingUserRetainsAccess : { type : "boolean" }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ } ) ;
105
+
106
+ export async function getMoveImpact (
107
+ req : Request ,
108
+ res : Response ,
109
+ next : NextFunction
110
+ ) {
111
+ try {
112
+ const parsedParams = getMoveImpactParamsSchema . safeParse ( req . params ) ;
113
+ if ( ! parsedParams . success ) {
114
+ return next ( createHttpError ( HttpCode . BAD_REQUEST , fromError ( parsedParams . error ) . toString ( ) ) ) ;
115
+ }
116
+
117
+ const parsedQuery = getMoveImpactQuerySchema . safeParse ( req . query ) ;
118
+ if ( ! parsedQuery . success ) {
119
+ return next ( createHttpError ( HttpCode . BAD_REQUEST , fromError ( parsedQuery . error ) . toString ( ) ) ) ;
120
+ }
121
+
122
+ const { resourceId } = parsedParams . data ;
123
+ const { targetOrgId } = parsedQuery . data ;
124
+ const user = req . user ;
125
+
126
+ if ( ! user ) {
127
+ return next ( createHttpError ( HttpCode . UNAUTHORIZED , "User not authenticated" ) ) ;
128
+ }
129
+
130
+ const [ resource ] = await db
131
+ . select ( )
132
+ . from ( resources )
133
+ . where ( eq ( resources . resourceId , resourceId ) )
134
+ . limit ( 1 ) ;
135
+
136
+ if ( ! resource ) {
137
+ return next (
138
+ createHttpError ( HttpCode . NOT_FOUND , `Resource with ID ${ resourceId } not found` )
139
+ ) ;
140
+ }
141
+
142
+ // set req.userOrgId to source org for permission check
143
+ req . userOrgId = resource . orgId ;
144
+
145
+ if ( resource . orgId === targetOrgId ) {
146
+ return next (
147
+ createHttpError ( HttpCode . BAD_REQUEST , `Resource is already in this organization` )
148
+ ) ;
149
+ }
150
+
151
+ const [ currentOrg ] = await db
152
+ . select ( )
153
+ . from ( orgs )
154
+ . where ( eq ( orgs . orgId , resource . orgId ) )
155
+ . limit ( 1 ) ;
156
+
157
+ const [ targetOrg ] = await db
158
+ . select ( )
159
+ . from ( orgs )
160
+ . where ( eq ( orgs . orgId , targetOrgId ) )
161
+ . limit ( 1 ) ;
162
+
163
+ if ( ! targetOrg ) {
164
+ return next (
165
+ createHttpError ( HttpCode . NOT_FOUND , `Target organization with ID ${ targetOrgId } not found` )
166
+ ) ;
167
+ }
168
+
169
+ const [ userOrgAccess ] = await db
170
+ . select ( )
171
+ . from ( userOrgs )
172
+ . where ( and (
173
+ eq ( userOrgs . userId , user . userId ) ,
174
+ eq ( userOrgs . orgId , targetOrgId )
175
+ ) )
176
+ . limit ( 1 ) ;
177
+
178
+ if ( ! userOrgAccess ) {
179
+ return next (
180
+ createHttpError ( HttpCode . FORBIDDEN , `You don't have access to the target organization` )
181
+ ) ;
182
+ }
183
+
184
+ // get role-based permissions that will be affected
185
+ const rolePermissionsQuery = await db
186
+ . select ( {
187
+ roleId : roleResources . roleId ,
188
+ roleName : roles . name ,
189
+ roleDescription : roles . description
190
+ } )
191
+ . from ( roleResources )
192
+ . innerJoin ( roles , eq ( roleResources . roleId , roles . roleId ) )
193
+ . where ( eq ( roleResources . resourceId , resourceId ) ) ;
194
+
195
+ // get user permissions that will be affected (excluding moving user)
196
+ const userPermissionsQuery = await db
197
+ . select ( {
198
+ userId : userResources . userId ,
199
+ username : users . username ,
200
+ email : users . email ,
201
+ name : users . name
202
+ } )
203
+ . from ( userResources )
204
+ . innerJoin ( users , eq ( userResources . userId , users . userId ) )
205
+ . where ( and (
206
+ eq ( userResources . resourceId , resourceId ) ,
207
+ // exclude the moving user from the impact as they'll retain access
208
+ // Note: We'll show this in the UI but they won't "lose" access
209
+ ) ) ;
210
+
211
+ // Separate moving user from others who will lose access
212
+ const movingUserPermission = userPermissionsQuery . find ( up => up . userId === user . userId ) ;
213
+ const otherUserPermissions = userPermissionsQuery . filter ( up => up . userId !== user . userId ) ;
214
+
215
+ const totalImpactedPermissions = rolePermissionsQuery . length + otherUserPermissions . length ;
216
+
217
+ const impactData = {
218
+ resourceId : resource . resourceId ,
219
+ resourceName : resource . name ,
220
+ currentOrgId : resource . orgId ,
221
+ currentOrgName : currentOrg ?. name || 'Unknown' ,
222
+ targetOrgId,
223
+ targetOrgName : targetOrg . name ,
224
+ impact : {
225
+ rolePermissions : {
226
+ count : rolePermissionsQuery . length ,
227
+ details : rolePermissionsQuery . map ( rp => ( {
228
+ roleId : rp . roleId ,
229
+ roleName : rp . roleName ,
230
+ roleDescription : rp . roleDescription
231
+ } ) )
232
+ } ,
233
+ userPermissions : {
234
+ count : otherUserPermissions . length ,
235
+ details : otherUserPermissions . map ( up => ( {
236
+ userId : up . userId ,
237
+ username : up . username ,
238
+ email : up . email || '' ,
239
+ name : up . name || ''
240
+ } ) )
241
+ } ,
242
+ movingUser : movingUserPermission ? {
243
+ userId : movingUserPermission . userId ,
244
+ username : movingUserPermission . username ,
245
+ email : movingUserPermission . email || '' ,
246
+ name : movingUserPermission . name || '' ,
247
+ retainsAccess : true
248
+ } : null ,
249
+ totalImpactedPermissions,
250
+ authenticationPreserved : true , // Passwords, pins, etc. are preserved
251
+ movingUserRetainsAccess : true
252
+ }
253
+ } ;
254
+
255
+ logger . info ( `Move impact calculated for resource ${ resourceId } ` , {
256
+ resourceId,
257
+ currentOrgId : resource . orgId ,
258
+ targetOrgId,
259
+ userId : user . userId ,
260
+ rolePermissionsAffected : rolePermissionsQuery . length ,
261
+ userPermissionsAffected : otherUserPermissions . length ,
262
+ totalImpact : totalImpactedPermissions
263
+ } ) ;
264
+
265
+ return response ( res , {
266
+ data : impactData ,
267
+ success : true ,
268
+ error : false ,
269
+ message : "Move impact analysis completed successfully" ,
270
+ status : HttpCode . OK ,
271
+ } ) ;
272
+
273
+ } catch ( err ) {
274
+ logger . error ( "Error calculating move impact:" , err ) ;
275
+ return next (
276
+ createHttpError ( HttpCode . INTERNAL_SERVER_ERROR , "An error occurred while calculating move impact" )
277
+ ) ;
278
+ }
279
+ }
0 commit comments