Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { mergeResolvers } from '@graphql-tools/merge';
2import { AuthenticationError } from 'apollo-server-core';
3import { type GraphQLFieldResolver } from 'graphql';
4import { type PassportContext } from 'graphql-passport';
5
6import { type GQLServices } from '../api.js';
7import { type DataSources } from '../iocContainer/index.js';
8import { type User } from '../models/UserModel.js';
9import { CoopError, isCoopErrorOfType } from '../utils/errors.js';
10import {
11 type GQLInviteUserToken,
12 type GQLMutationResolvers,
13 type GQLQueryResolvers,
14} from './generated.js';
15import { resolvers as actionResolvers } from './modules/action.js';
16import { resolvers as actionStatisticsResolvers } from './modules/actionStatistics.js';
17import { resolvers as apiKeyResolvers } from './modules/apiKey.js';
18import { resolvers as authenticationResolvers } from './modules/authentication.js';
19import { resolvers as backtestResolvers } from './modules/backtest.js';
20import { resolvers as contentTypeResolvers } from './modules/contentType.js';
21import { resolvers as genericResolvers } from './modules/generic.js';
22import { resolvers as insightsResolvers } from './modules/insights.js';
23import { resolvers as integrationResolvers } from './modules/integration.js';
24import { resolvers as investigationResolvers } from './modules/investigation.js';
25import { resolvers as itemTypeResolvers } from './modules/itemType.js';
26import { resolvers as locationBankResolvers } from './modules/locationBank.js';
27import { resolvers as manualReviewToolResolvers } from './modules/manualReviewTool.js';
28import { resolvers as hashBankResolvers } from './modules/hashBanks/resolvers.js';
29import { resolvers as ncmecResolvers } from './modules/ncmec.js';
30import { resolvers as orgResolvers } from './modules/org.js';
31import { resolvers as policyResolvers } from './modules/policy.js';
32import { resolvers as reportingResolvers } from './modules/reporting.js';
33import { resolvers as reportingRulesResolvers } from './modules/reportingRule.js';
34import { resolvers as retroactionResolvers } from './modules/retroaction.js';
35import { resolvers as routingRulesResolvers } from './modules/routingRule.js';
36import { resolvers as ruleResolvers } from './modules/rule.js';
37import { resolvers as signalResolvers } from './modules/signal.js';
38import { resolvers as spotTestResolvers } from './modules/spotTest.js';
39import { resolvers as textBankResolvers } from './modules/textBank.js';
40import { resolvers as userResolvers } from './modules/user.js';
41import { gqlErrorResult, gqlSuccessResult } from './utils/gqlResult.js';
42
43// eslint-disable-next-line @typescript-eslint/no-restricted-types
44export type Context = PassportContext<User, {}> & {
45 dataSources: DataSources;
46 services: GQLServices;
47};
48
49export type Resolver<Source = unknown, Args = unknown> = GraphQLFieldResolver<
50 Source,
51 Context,
52 Args
53>;
54
55export type ResolverMap<Source = unknown> = {
56 // The `any` here lets us use a different + arbitrary type for
57 // the args object in each resolver, which is what we need.
58 // eslint-disable-next-line @typescript-eslint/no-explicit-any
59 [key: string]: Resolver<Source, any> | ResolverMap<Source>;
60};
61
62/**
63 * GraphQL Query Resolvers
64 */
65const Query: GQLQueryResolvers = {
66 async myOrg(_, __, context) {
67 const user = context.getUser();
68 if (user == null) {
69 return null;
70 }
71 return context.dataSources.orgAPI.getGraphQLOrgFromId(user.orgId);
72 },
73 async inviteUserToken(_, { token }, { dataSources }) {
74 try {
75 const inviteUserToken = await dataSources.orgAPI.getInviteUserToken(
76 token,
77 );
78 return gqlSuccessResult(
79 { tokenData: inviteUserToken as unknown as GQLInviteUserToken },
80 'InviteUserTokenSuccessResponse',
81 );
82 } catch (e: unknown) {
83 if (
84 isCoopErrorOfType(e, [
85 'InviteUserTokenExpiredError',
86 'InviteUserTokenMissingError',
87 ])
88 ) {
89 return gqlErrorResult(e);
90 }
91
92 throw e;
93 }
94 },
95 async allRuleInsights(_, __, context) {
96 const user = context.getUser();
97 if (user == null) {
98 return null;
99 }
100
101 try {
102 // TODO: this response type actually isn't right; remove cast and fix errors.
103 return (await context.dataSources.ruleAPI.getAllRuleInsights(
104 user.orgId,
105 )) as any;
106 } catch (e) {
107 // eslint-disable-next-line no-console
108 console.error('allRuleInsights: warehouse query failed:', (e as Error).message);
109 return null;
110 }
111 },
112 async isWarehouseAvailable(_, __, context) {
113 try {
114 await context.services.DataWarehouse.query(
115 'SELECT 1',
116 context.services.Tracer,
117 );
118 return true;
119 } catch (e) {
120 // eslint-disable-next-line no-console
121 console.error('isWarehouseAvailable: warehouse health check failed:', (e as Error).message);
122 return false;
123 }
124 },
125};
126
127type TSignUpResponse = { data: User } | CoopError;
128const SignUpResponse: ResolverMap<TSignUpResponse> = {
129 __resolveType(response) {
130 if (response instanceof CoopError) {
131 return 'SignUpUserExistsError';
132 } else {
133 return 'SignUpSuccessResponse';
134 }
135 },
136};
137
138const Mutation: GQLMutationResolvers = {
139 async signUp(_, params, context) {
140 try {
141 const newUser = await context.dataSources.userAPI.signUp(params, context);
142 return { data: newUser };
143 } catch (e: unknown) {
144 if (isCoopErrorOfType(e, 'SignUpUserExistsError')) {
145 return gqlErrorResult(e);
146 }
147
148 throw e;
149 }
150 },
151 async sendPasswordReset(_, params, context) {
152 const { email } = params.input;
153 context.services.UserManagementService.sendPasswordResetEmail({
154 email,
155 }).catch(() => {});
156 return true;
157 },
158 async resetPassword(_, params, context) {
159 const { token, newPassword } = params.input;
160 await context.services.UserManagementService.resetPasswordForToken({
161 token,
162 newPassword,
163 });
164 return true;
165 },
166 async generatePasswordResetToken(_, { userId }, context) {
167 const user = context.getUser();
168 if (user == null) {
169 throw new AuthenticationError('Authenticated user required');
170 }
171
172 if (!user.getPermissions().includes('MANAGE_ORG')) {
173 throw new AuthenticationError(
174 'User does not have permission to generate password reset tokens',
175 );
176 }
177
178 const token =
179 await context.services.UserManagementService.generatePasswordResetTokenForUser(
180 { userId, invokerOrgId: user.orgId },
181 );
182 return token;
183 },
184 async updateRole(_, params, context) {
185 const user = context.getUser();
186 if (user == null) {
187 throw new AuthenticationError('Authenticated user required');
188 }
189
190 await context.services.UserManagementService.updateUserRole({
191 userId: params.input.id,
192 newRole: params.input.role,
193 orgId: user.orgId,
194 invoker: {
195 userId: user.id,
196 orgId: user.orgId,
197 permissions: user.getPermissions(),
198 },
199 });
200
201 return true;
202 },
203 async inviteUser(_, params, context) {
204 const user = context.getUser();
205 if (user == null) {
206 throw new AuthenticationError('Authenticated user required');
207 }
208
209 if (!user.getPermissions().includes('MANAGE_ORG')) {
210 throw new AuthenticationError(
211 'User does not have permission to invite users',
212 );
213 }
214
215 const token = await context.dataSources.orgAPI.inviteUser(
216 params.input,
217 user.orgId,
218 );
219 return token;
220 },
221 async deleteInvite(_, { id }, context) {
222 const user = context.getUser();
223 if (user == null) {
224 throw new AuthenticationError('Authenticated user required');
225 }
226
227 if (!user.getPermissions().includes('MANAGE_ORG')) {
228 throw new AuthenticationError(
229 'User does not have permission to delete invites',
230 );
231 }
232
233 return context.services.UserManagementService.deleteInvite(id, user.orgId);
234 },
235 async approveUser(_, { id }, context) {
236 const user = context.getUser();
237 if (user == null) {
238 throw new AuthenticationError('Authenticated user required');
239 }
240
241 if (!user.getPermissions().includes('MANAGE_ORG')) {
242 throw new AuthenticationError(
243 'User does not have permission to approve users',
244 );
245 }
246
247 return context.dataSources.userAPI.approveUser(id, user.orgId);
248 },
249 async rejectUser(_, { id }, context) {
250 const user = context.getUser();
251 if (user == null) {
252 throw new AuthenticationError('Authenticated user required');
253 }
254
255 if (!user.getPermissions().includes('MANAGE_ORG')) {
256 throw new AuthenticationError(
257 'User does not have permission to reject users',
258 );
259 }
260
261 return context.dataSources.userAPI.rejectUser(id, user.orgId);
262 },
263 async requestDemo(_, params, context) {
264 return context.dataSources.orgAPI.requestDemo(params.input);
265 },
266};
267
268export default mergeResolvers([
269 { Query, Mutation, SignUpResponse },
270 actionResolvers,
271 actionStatisticsResolvers,
272 apiKeyResolvers,
273 authenticationResolvers,
274 backtestResolvers,
275 contentTypeResolvers,
276 genericResolvers,
277 insightsResolvers,
278 integrationResolvers,
279 investigationResolvers,
280 itemTypeResolvers,
281 locationBankResolvers,
282 manualReviewToolResolvers,
283 hashBankResolvers,
284 ncmecResolvers,
285 orgResolvers,
286 policyResolvers,
287 reportingResolvers,
288 reportingRulesResolvers,
289 retroactionResolvers,
290 routingRulesResolvers,
291 ruleResolvers,
292 signalResolvers,
293 spotTestResolvers,
294 textBankResolvers,
295 userResolvers,
296]);