generated from MattHalloran/ReactGraphQLTemplate
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
customer.ts
469 lines (453 loc) · 21.5 KB
/
customer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
import { gql } from 'apollo-server-express';
import bcrypt from 'bcrypt';
import { CODE, COOKIE } from '@shared/consts';
import { CustomError, validateArgs } from '../error';
import { generateToken } from '../auth';
import { customerNotifyAdmin, sendResetPasswordLink, sendVerificationLink } from '../worker/email/queue';
import { HASHING_ROUNDS } from '../consts';
import { PrismaSelect } from '@paljs/plugins';
import { customerFromEmail, getCart, getCustomerSelect, upsertCustomer } from '../db/models/customer';
import { logInSchema, passwordSchema, requestPasswordChangeSchema, signUpSchema } from '@shared/validation';
import { IWrap, RecursivePartial } from '../types';
import { Context } from '../context';
import { GraphQLResolveInfo } from 'graphql';
import { AccountStatus, AddCustomerRoleInput, ChangeCustomerStatusInput, Count, Customer, CustomerInput, DeleteCustomerInput, LoginInput, RemoveCustomerRoleInput, RequestPasswordChangeInput, ResetPasswordInput, SignUpInput, UpdateCustomerInput } from './types';
import { logger, LogLevel } from '../logger';
const LOGIN_ATTEMPTS_TO_SOFT_LOCKOUT = 3;
const SOFT_LOCKOUT_DURATION = 15 * 60 * 1000;
const REQUEST_PASSWORD_RESET_DURATION = 2 * 24 * 3600 * 1000;
const LOGIN_ATTEMPTS_TO_HARD_LOCKOUT = 10;
export const typeDef = gql`
enum AccountStatus {
Deleted
Unlocked
SoftLock
HardLock
}
input CustomerInput {
id: ID
firstName: String
lastName: String
pronouns: String
emails: [EmailInput!]
phones: [PhoneInput!]
business: BusinessInput
theme: String
status: AccountStatus
accountApproved: Boolean
}
input LoginInput {
email: String
password: String,
verificationCode: String
}
input SignUpInput {
firstName: String!
lastName: String!
pronouns: String
business: String!
email: String!
phone: String!
accountApproved: Boolean!
theme: String!
marketingEmails: Boolean!
password: String!
}
input UpdateCustomerInput {
input: CustomerInput!
currentPassword: String
newPassword: String
}
input DeleteCustomerInput {
id: ID!
password: String
}
input RequestPasswordChangeInput {
email: String!
}
input ResetPasswordInput {
id: ID!
code: String!
newPassword: String!
}
input ChangeCustomerStatusInput {
id: ID!
status: AccountStatus!
}
input AddCustomerRoleInput {
id: ID!
roleId: ID!
}
input RemoveCustomerRoleInput {
id: ID!
roleId: ID!
}
type Customer {
id: ID!
firstName: String!
lastName: String!
pronouns: String!
emails: [Email!]!
phones: [Phone!]!
business: Business
theme: String!
accountApproved: Boolean!
emailVerified: Boolean!
status: AccountStatus!
cart: Order
orders: [Order!]!
roles: [CustomerRole!]!
feedback: [Feedback!]!
}
extend type Query {
customers: [Customer!]!
profile: Customer!
}
extend type Mutation {
login(input: LoginInput!): Customer!
logout: Boolean
signUp(input: SignUpInput!): Customer!
addCustomer(input: CustomerInput!): Customer!
updateCustomer(input: UpdateCustomerInput!): Customer!
deleteCustomer(input: DeleteCustomerInput!): Boolean
requestPasswordChange(input: RequestPasswordChangeInput!): Boolean
resetPassword(input: ResetPasswordInput!): Customer!
changeCustomerStatus(input: ChangeCustomerStatusInput!): Boolean
addCustomerRole(input: AddCustomerRoleInput!): Customer!
removeCustomerRole(input: RemoveCustomerRoleInput!): Boolean
}
`
export const resolvers = {
AccountStatus: AccountStatus,
Query: {
customers: async (_parent: undefined, _input: undefined, { prisma, req }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>[]> => {
// Must be admin
if (!req.isAdmin) throw new CustomError(CODE.Unauthorized);
return await prisma.customer.findMany({
orderBy: { lastName: 'asc', },
...(new PrismaSelect(info).value)
}) as any[];
},
profile: async (_parent: undefined, _input: undefined, { prisma, req }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>> => {
// Can only query your own profile
const customerId = req.customerId;
if (customerId === null || customerId === undefined) throw new CustomError(CODE.Unauthorized);
return await prisma.customer.findUnique({ where: { id: customerId }, ...(new PrismaSelect(info).value) }) as any;
}
},
Mutation: {
login: async (_parent: undefined, { input }: IWrap<LoginInput>, { prisma, req, res }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>> => {
logger.log(LogLevel.info, 'Logging in user a...', input);
const prismaInfo = getCustomerSelect(info);
logger.log(LogLevel.info, 'Logging in user b...');
// If username and password wasn't passed, then use the session cookie data to validate
if (!input.email || !input.password) {
if (req.customerId && req.roles && req.roles.length > 0) {
const cart = await getCart(prisma, info, req.customerId);
let userData: any = await prisma.customer.findUnique({ where: { id: req.customerId }, ...prismaInfo });
if (userData) {
if (cart) userData.cart = cart;
return userData;
}
res.clearCookie(COOKIE.Jwt);
}
logger.log(LogLevel.info, 'Logging in user failed c...');
throw new CustomError(CODE.BadCredentials);
}
// Validate input format
const validateError = await validateArgs(logInSchema, input);
if (validateError) return validateError;
// Get customer
let customer = await customerFromEmail(input.email, prisma);
// Check for password in database, if doesn't exist, send a password reset link
if (!customer.password) {
// Generate new code
const requestCode = bcrypt.genSaltSync(HASHING_ROUNDS).replace('/', '');
// Store code and request time in customer row
await prisma.customer.update({
where: { id: customer.id },
data: { resetPasswordCode: requestCode, lastResetPasswordReqestAttempt: new Date().toISOString() }
})
// Send new verification email
sendResetPasswordLink(input.email, customer.id, requestCode);
logger.log(LogLevel.info, 'Logging in user failed d...');
throw new CustomError(CODE.MustResetPassword);
}
// Validate verification code, if supplied
if (input.verificationCode === customer.id && customer.emailVerified === false) {
customer = await prisma.customer.update({
where: { id: customer.id },
data: { status: AccountStatus.Unlocked, emailVerified: true }
})
}
// Reset login attempts after 15 minutes
const unable_to_reset = [AccountStatus.HardLock, AccountStatus.Deleted];
if (!unable_to_reset.includes(customer.status as any) && Date.now() - new Date(customer.lastLoginAttempt).getTime() > SOFT_LOCKOUT_DURATION) {
customer = await prisma.customer.update({
where: { id: customer.id },
data: { loginAttempts: 0 }
})
}
// Before validating password, let's check to make sure the account is unlocked
const status_to_code = {
[AccountStatus.Deleted]: CODE.NoCustomer,
[AccountStatus.SoftLock]: CODE.SoftLockout,
[AccountStatus.HardLock]: CODE.HardLockout
}
if (customer.status in status_to_code) {
logger.log(LogLevel.info, 'Logging in user failed e...', customer.status);
throw new CustomError((status_to_code as any)[customer.status]);
}
// Now we can validate the password
const validPassword = customer.password && bcrypt.compareSync(input.password, customer.password);
if (validPassword) {
await generateToken(res, customer.id, customer.businessId ?? '');
await prisma.customer.update({
where: { id: customer.id },
data: {
loginAttempts: 0,
lastLoginAttempt: new Date().toISOString(),
resetPasswordCode: null,
lastResetPasswordReqestAttempt: null
},
...prismaInfo
})
// Return cart, along with user data
const cart = await getCart(prisma, info, customer.id);
const userData: any = await prisma.customer.findUnique({ where: { id: customer.id }, ...prismaInfo });
if (cart) userData.cart = cart;
logger.log(LogLevel.info, 'Logging in user returning data...', userData);
return userData;
} else {
let new_status = AccountStatus.Unlocked;
let login_attempts = customer.loginAttempts + 1;
if (login_attempts >= LOGIN_ATTEMPTS_TO_SOFT_LOCKOUT) {
new_status = AccountStatus.SoftLock;
} else if (login_attempts > LOGIN_ATTEMPTS_TO_HARD_LOCKOUT) {
new_status = AccountStatus.HardLock;
}
await prisma.customer.update({
where: { id: customer.id },
data: { status: new_status, loginAttempts: login_attempts, lastLoginAttempt: new Date().toISOString() }
})
logger.log(LogLevel.info, 'Logging in user failed f...');
throw new CustomError(CODE.BadCredentials);
}
},
logout: async (_parent: undefined, _input: undefined, { prisma, req, res }: Context): Promise<boolean> => {
res.clearCookie(COOKIE.Jwt);
return true;
},
signUp: async (_parent: undefined, { input }: IWrap<SignUpInput>, { prisma, req, res }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>> => {
const prismaInfo = getCustomerSelect(info);
// Validate input format
const validateError = await validateArgs(signUpSchema, input);
if (validateError) return validateError;
// Find customer role to give to new user
const customerRole = await prisma.role.findUnique({ where: { title: 'Customer' } });
if (!customerRole) throw new CustomError(CODE.ErrorUnknown);
const customer = await upsertCustomer({
prisma: prisma,
info,
data: {
firstName: input.firstName,
lastName: input.lastName,
pronouns: input.pronouns,
business: { name: input.business },
password: bcrypt.hashSync(input.password, HASHING_ROUNDS),
accountApproved: input.accountApproved,
theme: input.theme,
status: AccountStatus.Unlocked,
emails: [{ emailAddress: input.email }],
phones: [{ number: input.phone }],
roles: [customerRole]
}
})
await generateToken(res, customer.id, customer.businessId);
// Send verification email
sendVerificationLink(input.email, customer.id);
// Send email to business owner
customerNotifyAdmin(`${input.firstName} ${input.lastName}`);
// Return cart, along with user data
const cart = await getCart(prisma, info, customer.id);
const userData: any = await prisma.customer.findUnique({ where: { id: customer.id }, ...prismaInfo });
if (userData && cart) userData.cart = cart;
return userData;
},
addCustomer: async (_parent: undefined, { input }: IWrap<CustomerInput>, { prisma, req }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>> => {
// Must be admin to add a customer directly
if (!req.isAdmin) throw new CustomError(CODE.Unauthorized);
const prismaInfo = getCustomerSelect(info);
// Find customer role to give to new user
const customerRole = await prisma.role.findUnique({ where: { title: 'Customer' } });
if (!customerRole) throw new CustomError(CODE.ErrorUnknown);
const customer = await upsertCustomer({
prisma: prisma,
info,
data: {
firstName: input.firstName,
lastName: input.lastName,
pronouns: input.pronouns,
business: input.business,
accountApproved: true,
theme: 'light',
status: AccountStatus.Unlocked,
emails: input.emails,
phones: input.phones,
roles: [customerRole]
}
});
// Return cart, along with user data
const cart = await getCart(prisma, info, customer.id);
const userData: any = await prisma.customer.findUnique({ where: { id: customer.id }, ...prismaInfo });
if (userData && cart) userData.cart = cart;
return userData;
},
updateCustomer: async (_parent: undefined, { input }: IWrap<UpdateCustomerInput>, { prisma, req }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>> => {
// Must be admin, or updating your own
if (!req.isAdmin && !input.input.id || (req.customerId !== input.input.id)) throw new CustomError(CODE.Unauthorized);
if (!req.isAdmin) {
// Check for correct password
let customer = await prisma.customer.findUnique({
where: { id: input.input.id as string },
select: {
id: true,
password: true,
business: { select: { id: true } }
}
});
if (!customer?.password || !input.currentPassword || !bcrypt.compareSync(input.currentPassword, customer.password)) throw new CustomError(CODE.BadCredentials);
}
const user = await upsertCustomer({
prisma: prisma,
info,
data: input.input
})
return user;
},
deleteCustomer: async (_parent: undefined, { input }: IWrap<DeleteCustomerInput>, { prisma, req }: Context): Promise<boolean> => {
// Must be admin, or deleting your own
if (!req.isAdmin && (req.customerId !== input.id)) throw new CustomError(CODE.Unauthorized);
// Check for correct password
let customer = await prisma.customer.findUnique({
where: { id: input.id },
select: {
id: true,
password: true
}
});
if (!customer) throw new CustomError(CODE.ErrorUnknown);
// If admin, make sure you are not deleting yourself
if (req.isAdmin) {
if (customer.id === req.customerId) throw new CustomError(CODE.CannotDeleteYourself);
}
// If not admin, make sure correct password is entered
else if (!req.isAdmin) {
if (!customer.password || !input.password || !bcrypt.compareSync(input.password, customer.password)) throw new CustomError(CODE.BadCredentials);
}
// Delete account
await prisma.customer.delete({ where: { id: customer.id } });
return true;
},
requestPasswordChange: async (_parent: undefined, { input }: IWrap<RequestPasswordChangeInput>, { prisma, req }: Context): Promise<boolean> => {
// Validate input format
const validateError = await validateArgs(requestPasswordChangeSchema, input);
if (validateError) return validateError;
// Find customer in database
const customer = await customerFromEmail(input.email, prisma);
// Generate request code
const requestCode = bcrypt.genSaltSync(HASHING_ROUNDS).replace('/', '');
// Store code and request time in customer row
await prisma.customer.update({
where: { id: customer.id },
data: { resetPasswordCode: requestCode, lastResetPasswordReqestAttempt: new Date().toISOString() }
})
// Send email with correct reset link
sendResetPasswordLink(input.email, customer.id, requestCode);
return true;
},
resetPassword: async (_parent: undefined, { input }: IWrap<ResetPasswordInput>, { prisma, req }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>> => {
// Validate input format
const validateError = await validateArgs(passwordSchema, input.newPassword);
if (validateError) return validateError;
// Find customer in database
const customer = await prisma.customer.findUnique({
where: { id: input.id },
select: {
id: true,
resetPasswordCode: true,
lastResetPasswordReqestAttempt: true,
emails: { select: { emailAddress: true } }
}
});
if (!customer) throw new CustomError(CODE.ErrorUnknown);
// Verify request code and that request was made within 48 hours
if (!customer.resetPasswordCode ||
customer.resetPasswordCode !== input.code ||
!customer.lastResetPasswordReqestAttempt ||
Date.now() - new Date(customer.lastResetPasswordReqestAttempt).getTime() > REQUEST_PASSWORD_RESET_DURATION) {
// Generate new code
const requestCode = bcrypt.genSaltSync(HASHING_ROUNDS).replace('/', '');
// Store code and request time in customer row
await prisma.customer.update({
where: { id: customer.id },
data: { resetPasswordCode: requestCode, lastResetPasswordReqestAttempt: new Date().toISOString() }
})
// Send new verification email
for (const email of customer.emails) {
sendResetPasswordLink(email.emailAddress, customer.id, requestCode);
}
// Return error
throw new CustomError(CODE.InvalidResetCode);
}
// Remove request data from customer, and set new password
await prisma.customer.update({
where: { id: customer.id },
data: {
resetPasswordCode: null,
lastResetPasswordReqestAttempt: null,
password: bcrypt.hashSync(input.newPassword, HASHING_ROUNDS)
}
})
// Return customer data
const prismaInfo = getCustomerSelect(info);
const cart = await getCart(prisma, info, customer.id);
const customerData: any = await prisma.customer.findUnique({ where: { id: customer.id }, ...prismaInfo });
if (cart) customerData.cart = cart;
return customerData;
},
changeCustomerStatus: async (_parent: undefined, { input }: IWrap<ChangeCustomerStatusInput>, { prisma, req }: Context): Promise<boolean> => {
// Must be admin
if (!req.isAdmin) throw new CustomError(CODE.Unauthorized);
await prisma.customer.update({
where: { id: input.id },
data: { status: input.status }
})
return true;
},
addCustomerRole: async (_parent: undefined, { input }: IWrap<AddCustomerRoleInput>, { prisma, req }: Context, info: GraphQLResolveInfo): Promise<RecursivePartial<Customer>> => {
// Must be admin
if (!req.isAdmin) throw new CustomError(CODE.Unauthorized);
await prisma.customer_roles.create({
data: {
customerId: input.id,
roleId: input.roleId
}
})
return await prisma.customer.findUnique({ where: { id: input.id }, ...(new PrismaSelect(info).value) }) as any;
},
removeCustomerRole: async (_parent: undefined, { input }: IWrap<RemoveCustomerRoleInput>, { prisma, req }: Context): Promise<boolean> => {
// Must be admin
if (!req.isAdmin) throw new CustomError(CODE.Unauthorized);
await prisma.customer_roles.delete({
where: {
customer_roles_customerid_roleid_unique: {
customerId: input.id,
roleId: input.roleId
}
}
})
return true;
},
}
}