Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 420 lines 12 kB view raw
1import { AuthenticationError, ForbiddenError } from 'apollo-server-express'; 2import jwt from 'jsonwebtoken'; 3 4import { 5 type GQLGetDecisionCountSettings, 6 type GQLGetJobCreationCountSettings, 7 type GQLMutationResolvers, 8 type GQLQueryResolvers, 9 type GQLUserResolvers, 10} from '../generated.js'; 11import { gqlSuccessResult } from '../utils/gqlResult.js'; 12 13const typeDefs = /* GraphQL */ ` 14 enum UserRole { 15 ADMIN 16 RULES_MANAGER 17 ANALYST 18 MODERATOR_MANAGER 19 MODERATOR 20 CHILD_SAFETY_MODERATOR 21 EXTERNAL_MODERATOR 22 } 23 24 enum UserPermission { 25 MANAGE_ORG 26 MUTATE_LIVE_RULES 27 MUTATE_NON_LIVE_RULES 28 RUN_RETROACTION 29 RUN_BACKTEST 30 VIEW_INSIGHTS 31 MANUALLY_ACTION_CONTENT 32 VIEW_MRT 33 VIEW_MRT_DATA 34 EDIT_MRT_QUEUES 35 VIEW_CHILD_SAFETY_DATA 36 MANAGE_POLICIES 37 VIEW_INVESTIGATION 38 VIEW_RULES_DASHBOARD 39 } 40 41 enum UserPenaltySeverity { 42 NONE 43 LOW 44 MEDIUM 45 HIGH 46 SEVERE 47 } 48 49 # TODO: figure out if role can really be null. Also, squash approvedByAdmin 50 # and removedByAdmin into one field for simplicity and to prevent incoherent 51 # states (like being both approved and rejected). Figure out if that new field 52 # can be null. 53 type User { 54 id: ID! 55 email: String! 56 firstName: String! 57 lastName: String! 58 orgId: ID! 59 role: UserRole 60 permissions: [UserPermission!]! 61 createdAt: String! 62 approvedByAdmin: Boolean 63 rejectedByAdmin: Boolean 64 loginMethods: [String!]! 65 # Extra wrapper types here are so that we can eventually turn notifications 66 # into a proper Connection in a non-breaking way if we ever need pagination. 67 notifications: UserNotifications! 68 readMeJWT: String 69 favoriteRules: [Rule!]! 70 favoriteMRTQueues: [ManualReviewQueue!]! 71 interfacePreferences: UserInterfacePreferences! 72 reviewableQueues(queueIds: [ID!]): [ManualReviewQueue!]! 73 } 74 75 type UserInterfacePreferences { 76 moderatorSafetyMuteVideo: Boolean! 77 moderatorSafetyGrayscale: Boolean! 78 moderatorSafetyBlurLevel: Int! 79 mrtChartConfigurations: [ManualReviewChartSettings!]! 80 } 81 82 input ModeratorSafetySettingsInput { 83 moderatorSafetyMuteVideo: Boolean! 84 moderatorSafetyGrayscale: Boolean! 85 moderatorSafetyBlurLevel: Int! 86 } 87 88 input ManualReviewChartConfigurationsInput { 89 chartConfigurations: [ManualReviewChartSettingsInput!]! 90 } 91 92 type UserNotifications { 93 edges: [UserNotificationEdge!]! 94 } 95 96 type UserNotificationEdge { 97 node: Notification! 98 } 99 100 enum NotificationType { 101 RULE_PASS_RATE_INCREASE_ANOMALY_START 102 RULE_PASS_RATE_INCREASE_ANOMALY_END 103 } 104 105 type Notification { 106 id: ID! 107 type: NotificationType! 108 message: String! 109 data: JSONObject 110 readAt: DateTime 111 createdAt: DateTime! 112 } 113 114 type Query { 115 user(id: ID!): User 116 } 117 118 input ChangePasswordInput { 119 currentPassword: String! 120 newPassword: String! 121 } 122 123 type ChangePasswordSuccessResponse { 124 _: Boolean 125 } 126 127 type ChangePasswordError implements Error { 128 title: String! 129 status: Int! 130 type: [String!]! 131 pointer: String 132 detail: String 133 requestId: String 134 } 135 136 union ChangePasswordResponse = 137 ChangePasswordSuccessResponse 138 | ChangePasswordError 139 140 type Mutation { 141 deleteUser(id: ID!): Boolean 142 updateAccountInfo(firstName: String, lastName: String): Boolean 143 changePassword(input: ChangePasswordInput!): ChangePasswordResponse! 144 addFavoriteRule(ruleId: ID!): AddFavoriteRuleSuccessResponse! 145 removeFavoriteRule(ruleId: ID!): RemoveFavoriteRuleSuccessResponse! 146 addFavoriteMRTQueue(queueId: ID!): AddFavoriteMRTQueueSuccessResponse! 147 removeFavoriteMRTQueue(queueId: ID!): RemoveFavoriteMRTQueueSuccessResponse! 148 setModeratorSafetySettings( 149 moderatorSafetySettings: ModeratorSafetySettingsInput! 150 ): SetModeratorSafetySettingsSuccessResponse 151 setMrtChartConfigurationSettings( 152 mrtChartConfigurationSettings: ManualReviewChartConfigurationsInput! 153 ): SetMrtChartConfigurationSettingsSuccessResponse 154 } 155 156 union AddFavoriteRuleResponse = AddFavoriteRuleSuccessResponse 157 158 type AddFavoriteRuleSuccessResponse { 159 _: Boolean 160 } 161 162 type RemoveFavoriteRuleSuccessResponse { 163 _: Boolean 164 } 165 166 type SetModeratorSafetySettingsSuccessResponse { 167 _: Boolean 168 } 169 170 type SetMrtChartConfigurationSettingsSuccessResponse { 171 _: Boolean 172 } 173 174 type AddFavoriteMRTQueueSuccessResponse { 175 _: Boolean 176 } 177 178 type RemoveFavoriteMRTQueueSuccessResponse { 179 _: Boolean 180 } 181 182 union AddFavoriteRuleResponse = AddFavoriteRuleSuccessResponse 183 union RemoveFavoriteRuleResponse = RemoveFavoriteRuleSuccessResponse 184`; 185 186const Query: GQLQueryResolvers = { 187 async user(_, { id }, context) { 188 const user = context.getUser(); 189 if (user == null) { 190 throw new AuthenticationError('User required.'); 191 } 192 193 const { orgId } = user; 194 return context.dataSources.userAPI.getGraphQLUserFromId({ id, orgId }); 195 }, 196}; 197 198const Mutation: GQLMutationResolvers = { 199 async updateAccountInfo(_, params, context) { 200 const user = context.getUser(); 201 if (user == null) { 202 throw new AuthenticationError('Authenticated user required'); 203 } 204 await context.dataSources.userAPI.updateAccountInfo(user, params); 205 return true; // TODO: return the updated user instead. 206 }, 207 async changePassword(_, params, context) { 208 const user = context.getUser(); 209 if (user == null) { 210 throw new AuthenticationError('Authenticated user required'); 211 } 212 return context.dataSources.userAPI.changePassword(user, params.input); 213 }, 214 async deleteUser(_, params, context) { 215 const user = context.getUser(); 216 if (user == null) { 217 throw new AuthenticationError('Authenticated user required'); 218 } 219 220 return context.dataSources.userAPI.deleteUser({ 221 id: params.id, 222 orgId: user.orgId, 223 }); 224 }, 225 async addFavoriteRule(_, params, context) { 226 const user = context.getUser(); 227 if (user == null) { 228 throw new AuthenticationError('User required.'); 229 } 230 await context.dataSources.userAPI.addFavoriteRule( 231 user.id, 232 params.ruleId, 233 user.orgId, 234 ); 235 return gqlSuccessResult({}, 'AddFavoriteRuleSuccessResponse'); 236 }, 237 async removeFavoriteRule(_, params, context) { 238 const user = context.getUser(); 239 if (user == null) { 240 throw new AuthenticationError('User required.'); 241 } 242 await context.dataSources.userAPI.removeFavoriteRule( 243 user.id, 244 params.ruleId, 245 user.orgId, 246 ); 247 return gqlSuccessResult({}, 'RemoveFavoriteRuleSuccessResponse'); 248 }, 249 async addFavoriteMRTQueue(_, params, context) { 250 const user = context.getUser(); 251 if (user == null) { 252 throw new AuthenticationError('User required.'); 253 } 254 await context.services.ManualReviewToolService.addFavoriteQueueForUser({ 255 userId: user.id, 256 orgId: user.orgId, 257 queueId: params.queueId, 258 }); 259 return gqlSuccessResult({}, 'AddFavoriteMRTQueueSuccessResponse'); 260 }, 261 async removeFavoriteMRTQueue(_, params, context) { 262 const user = context.getUser(); 263 if (user == null) { 264 throw new AuthenticationError('User required.'); 265 } 266 await context.services.ManualReviewToolService.removeFavoriteQueueForUser({ 267 userId: user.id, 268 orgId: user.orgId, 269 queueId: params.queueId, 270 }); 271 return gqlSuccessResult({}, 'RemoveFavoriteMRTQueueSuccessResponse'); 272 }, 273 async setModeratorSafetySettings(_, params, context) { 274 const user = context.getUser(); 275 if (user == null) { 276 throw new AuthenticationError('User required.'); 277 } 278 await context.services.UserManagementService.upsertUserInterfaceSettings({ 279 userId: user.id, 280 userInterfaceSettings: { 281 moderatorSafetySettings: params.moderatorSafetySettings, 282 }, 283 }); 284 return gqlSuccessResult({}, 'SetModeratorSafetySettingsSuccessResponse'); 285 }, 286 287 async setMrtChartConfigurationSettings(_, params, context) { 288 const user = context.getUser(); 289 if (user == null) { 290 throw new AuthenticationError('User required.'); 291 } 292 await context.services.UserManagementService.upsertUserInterfaceSettings({ 293 userId: user.id, 294 userInterfaceSettings: { 295 mrtChartConfigurations: 296 params.mrtChartConfigurationSettings.chartConfigurations.map((it) => 297 it.decisionCountSettings 298 ? { 299 ...it.decisionCountSettings, 300 title: it.title, 301 metric: 'DECISIONS', 302 filterBy: { 303 ...it.decisionCountSettings.filterBy, 304 startDate: new Date( 305 it.decisionCountSettings.filterBy.startDate, 306 ), 307 endDate: new Date( 308 it.decisionCountSettings.filterBy.endDate, 309 ), 310 filteredDecisionActionType: it.decisionCountSettings 311 .filterBy.filteredDecisionActionType 312 ? it.decisionCountSettings.filterBy 313 .filteredDecisionActionType 314 : undefined, 315 }, 316 } 317 : { 318 ...it.jobCreationCountSettings!, 319 title: it.title, 320 metric: 'JOBS', 321 }, 322 ), 323 }, 324 }); 325 return gqlSuccessResult( 326 {}, 327 'SetMrtChartConfigurationSettingsSuccessResponse', 328 ); 329 }, 330}; 331 332const User: GQLUserResolvers = { 333 permissions(user) { 334 return user.getPermissions(); 335 }, 336 async notifications(user, _, context) { 337 const api = context.dataSources.notificationsAPI; 338 const notifications = await api.getNotificationsForUser(user.id); 339 return { edges: notifications.map((it) => ({ node: it })) }; 340 }, 341 async readMeJWT(user, __, { dataSources, getUser }) { 342 try { 343 const authedUser = getUser(); 344 if (!authedUser || user.id !== authedUser.id) { 345 throw new ForbiddenError('Must be signed in as this user to read JWT.'); 346 } 347 348 const { email, firstName, lastName, orgId } = user; 349 const name = `${firstName} ${lastName}`; 350 const [apiKeyRes, publicSigningKey] = await Promise.all([ 351 dataSources.orgAPI.getActivatedApiKeyForOrg(orgId), 352 dataSources.orgAPI.getPublicSigningKeyPem(orgId), 353 ]); 354 const apiKey = apiKeyRes === false ? null : apiKeyRes.key; 355 356 return jwt.sign( 357 { name, email, apiKey, publicSigningKey }, 358 process.env.READ_ME_JWT_SECRET!, 359 ); 360 } catch (e) { 361 return null; 362 } 363 }, 364 async favoriteRules(user, _, context) { 365 return context.dataSources.userAPI.getFavoriteRules(user.id, user.orgId); 366 }, 367 async interfacePreferences(user, _, context) { 368 const settings = 369 await context.services.UserManagementService.getUserInterfaceSettings({ 370 userId: user.id, 371 orgId: user.orgId, 372 }); 373 return { 374 ...settings, 375 mrtChartConfigurations: settings.mrtChartConfigurations.map((it) => { 376 if (!('metric' in it)) { 377 throw new Error('No metric found in MRT chart configuration'); 378 } 379 if (it.metric === 'DECISIONS') { 380 return it as GQLGetDecisionCountSettings; 381 } else { 382 return it as GQLGetJobCreationCountSettings; 383 } 384 }), 385 }; 386 }, 387 async favoriteMRTQueues(user, _, context) { 388 return context.services.ManualReviewToolService.getFavoriteQueuesForUser({ 389 userId: user.id, 390 orgId: user.orgId, 391 }); 392 }, 393 async reviewableQueues(_, { queueIds }, context) { 394 const user = context.getUser(); 395 if (user == null) { 396 throw new AuthenticationError('Authenticated user required'); 397 } 398 399 const queues = 400 await context.services.ManualReviewToolService.getReviewableQueuesForUser( 401 { 402 invoker: { 403 userId: user.id, 404 permissions: user.getPermissions(), 405 orgId: user.orgId, 406 }, 407 }, 408 ); 409 410 if (queueIds) { 411 return queues.filter((it) => queueIds.includes(it.id)); 412 } 413 414 return queues; 415 }, 416}; 417 418const resolvers = { Query, Mutation, User }; 419 420export { typeDefs, resolvers };