@things-factory/auth-base 8.0.0-alpha.8 → 8.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/client/actions/auth.ts +5 -4
  2. package/client/index.ts +1 -0
  3. package/client/verify-webauthn.ts +86 -0
  4. package/dist-client/actions/auth.d.ts +5 -4
  5. package/dist-client/actions/auth.js.map +1 -1
  6. package/dist-client/index.d.ts +1 -0
  7. package/dist-client/index.js +1 -0
  8. package/dist-client/index.js.map +1 -1
  9. package/dist-client/tsconfig.tsbuildinfo +1 -1
  10. package/dist-client/verify-webauthn.d.ts +13 -0
  11. package/dist-client/verify-webauthn.js +72 -0
  12. package/dist-client/verify-webauthn.js.map +1 -0
  13. package/dist-server/constants/error-code.d.ts +2 -0
  14. package/dist-server/constants/error-code.js +3 -1
  15. package/dist-server/constants/error-code.js.map +1 -1
  16. package/dist-server/controllers/change-pwd.js +2 -2
  17. package/dist-server/controllers/change-pwd.js.map +1 -1
  18. package/dist-server/controllers/delete-user.js +13 -12
  19. package/dist-server/controllers/delete-user.js.map +1 -1
  20. package/dist-server/controllers/invitation.d.ts +2 -1
  21. package/dist-server/controllers/invitation.js +30 -5
  22. package/dist-server/controllers/invitation.js.map +1 -1
  23. package/dist-server/controllers/profile.d.ts +4 -3
  24. package/dist-server/controllers/profile.js +20 -2
  25. package/dist-server/controllers/profile.js.map +1 -1
  26. package/dist-server/controllers/signin.d.ts +4 -1
  27. package/dist-server/controllers/signin.js +17 -1
  28. package/dist-server/controllers/signin.js.map +1 -1
  29. package/dist-server/controllers/signup.js +13 -4
  30. package/dist-server/controllers/signup.js.map +1 -1
  31. package/dist-server/controllers/unlock-user.js +1 -0
  32. package/dist-server/controllers/unlock-user.js.map +1 -1
  33. package/dist-server/controllers/verification.js +1 -0
  34. package/dist-server/controllers/verification.js.map +1 -1
  35. package/dist-server/index.d.ts +1 -0
  36. package/dist-server/index.js +1 -0
  37. package/dist-server/index.js.map +1 -1
  38. package/dist-server/middlewares/signin-middleware.js +3 -3
  39. package/dist-server/middlewares/signin-middleware.js.map +1 -1
  40. package/dist-server/middlewares/webauthn-middleware.js.map +1 -1
  41. package/dist-server/migrations/1548206416130-SeedUser.js +2 -1
  42. package/dist-server/migrations/1548206416130-SeedUser.js.map +1 -1
  43. package/dist-server/router/auth-checkin-router.js +8 -2
  44. package/dist-server/router/auth-checkin-router.js.map +1 -1
  45. package/dist-server/router/auth-private-process-router.js +12 -7
  46. package/dist-server/router/auth-private-process-router.js.map +1 -1
  47. package/dist-server/router/auth-public-process-router.js +20 -9
  48. package/dist-server/router/auth-public-process-router.js.map +1 -1
  49. package/dist-server/router/auth-signin-router.js +3 -3
  50. package/dist-server/router/auth-signin-router.js.map +1 -1
  51. package/dist-server/router/webauthn-router.js +51 -1
  52. package/dist-server/router/webauthn-router.js.map +1 -1
  53. package/dist-server/service/appliance/appliance.js +4 -1
  54. package/dist-server/service/appliance/appliance.js.map +1 -1
  55. package/dist-server/service/application/application.js +11 -3
  56. package/dist-server/service/application/application.js.map +1 -1
  57. package/dist-server/service/invitation/invitation-mutation.d.ts +3 -2
  58. package/dist-server/service/invitation/invitation-mutation.js +20 -8
  59. package/dist-server/service/invitation/invitation-mutation.js.map +1 -1
  60. package/dist-server/service/user/user-mutation.d.ts +10 -9
  61. package/dist-server/service/user/user-mutation.js +135 -61
  62. package/dist-server/service/user/user-mutation.js.map +1 -1
  63. package/dist-server/service/user/user-types.d.ts +1 -0
  64. package/dist-server/service/user/user-types.js +4 -0
  65. package/dist-server/service/user/user-types.js.map +1 -1
  66. package/dist-server/service/user/user.d.ts +1 -0
  67. package/dist-server/service/user/user.js +57 -17
  68. package/dist-server/service/user/user.js.map +1 -1
  69. package/dist-server/service/verification-token/verification-token.js +7 -2
  70. package/dist-server/service/verification-token/verification-token.js.map +1 -1
  71. package/dist-server/templates/account-unlock-email.d.ts +2 -1
  72. package/dist-server/templates/account-unlock-email.js +1 -1
  73. package/dist-server/templates/account-unlock-email.js.map +1 -1
  74. package/dist-server/templates/invitation-email.d.ts +2 -1
  75. package/dist-server/templates/invitation-email.js +1 -1
  76. package/dist-server/templates/invitation-email.js.map +1 -1
  77. package/dist-server/templates/verification-email.d.ts +2 -1
  78. package/dist-server/templates/verification-email.js +1 -1
  79. package/dist-server/templates/verification-email.js.map +1 -1
  80. package/dist-server/tsconfig.tsbuildinfo +1 -1
  81. package/dist-server/utils/check-user-has-role.d.ts +12 -0
  82. package/dist-server/utils/check-user-has-role.js +28 -0
  83. package/dist-server/utils/check-user-has-role.js.map +1 -0
  84. package/package.json +6 -6
  85. package/server/constants/error-code.ts +2 -0
  86. package/server/controllers/change-pwd.ts +3 -2
  87. package/server/controllers/delete-user.ts +16 -13
  88. package/server/controllers/invitation.ts +36 -5
  89. package/server/controllers/profile.ts +29 -2
  90. package/server/controllers/signin.ts +21 -2
  91. package/server/controllers/signup.ts +16 -4
  92. package/server/controllers/unlock-user.ts +1 -0
  93. package/server/controllers/verification.ts +1 -0
  94. package/server/index.ts +1 -0
  95. package/server/middlewares/signin-middleware.ts +3 -3
  96. package/server/middlewares/webauthn-middleware.ts +7 -8
  97. package/server/migrations/1548206416130-SeedUser.ts +2 -1
  98. package/server/router/auth-checkin-router.ts +11 -5
  99. package/server/router/auth-private-process-router.ts +14 -7
  100. package/server/router/auth-public-process-router.ts +22 -10
  101. package/server/router/auth-signin-router.ts +3 -3
  102. package/server/router/webauthn-router.ts +71 -9
  103. package/server/service/appliance/appliance.ts +4 -1
  104. package/server/service/application/application.ts +12 -3
  105. package/server/service/invitation/invitation-mutation.ts +24 -9
  106. package/server/service/user/user-mutation.ts +152 -62
  107. package/server/service/user/user-types.ts +3 -0
  108. package/server/service/user/user.ts +74 -18
  109. package/server/service/verification-token/verification-token.ts +9 -3
  110. package/server/templates/account-unlock-email.ts +1 -1
  111. package/server/templates/invitation-email.ts +1 -1
  112. package/server/templates/verification-email.ts +1 -1
  113. package/server/utils/check-user-has-role.ts +29 -0
  114. package/translations/en.json +5 -1
  115. package/translations/ja.json +5 -1
  116. package/translations/ko.json +6 -3
  117. package/translations/ms.json +5 -1
  118. package/translations/zh.json +5 -1
@@ -1,6 +1,6 @@
1
1
  import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
2
2
  import { GraphQLEmailAddress } from 'graphql-scalars'
3
- import { ILike, In, SelectQueryBuilder } from 'typeorm'
3
+ import { ILike, In, SelectQueryBuilder, EntityManager } from 'typeorm'
4
4
 
5
5
  import { config } from '@things-factory/env'
6
6
  import { Domain, getRepository, ObjectRef } from '@things-factory/shell'
@@ -10,6 +10,7 @@ import { buildDomainUsersQueryBuilder } from '../../utils/get-domain-users'
10
10
  import { Role } from '../role/role'
11
11
  import { User, UserStatus } from './user'
12
12
  import { NewUser, UserPatch } from './user-types'
13
+ import { USERNAME_ALREADY_EXISTS, EMAIL_ALREADY_EXISTS } from '../../constants/error-code'
13
14
 
14
15
  @Resolver(User)
15
16
  export class UserMutation {
@@ -17,19 +18,29 @@ export class UserMutation {
17
18
  @Directive('@transaction')
18
19
  @Mutation(returns => User, { description: 'To create new user' })
19
20
  async createUser(@Arg('user') user: NewUser, @Ctx() context: ResolverContext) {
20
- const { domain } = context.state
21
+ const { domain, tx } = context.state
21
22
  const { defaultPassword } = config.get('password')
22
- const { email } = user
23
+ const { username, email } = user
24
+ const userRepository = getRepository(User, tx)
23
25
 
26
+ user.username = username.trim()
24
27
  user.email = email.trim()
25
28
 
26
- const oldUser: User = await getRepository(User).findOne({ where: { email: ILike(user.email) } })
27
- if (oldUser) {
28
- throw new Error(context.t('error.x already exists in y', { x: context.t('field.user'), y: 'operato' }))
29
+ if (await userRepository.findOne({ where: { username: user.username } })) {
30
+ throw new Error(context.t(USERNAME_ALREADY_EXISTS))
31
+ }
32
+
33
+ if (await userRepository.findOne({ where: { email: ILike(user.email) } })) {
34
+ throw new Error(context.t(EMAIL_ALREADY_EXISTS))
29
35
  }
30
36
 
31
37
  if (!user.password && !defaultPassword) {
32
- throw new Error(context.t('error.initial password or default password should be supported'))
38
+ throw new Error('initial password or default password should be supported.')
39
+ }
40
+
41
+ // TODO username은 다음 패턴을 따라야 한다. pattern="^[A-Za-z0-9]*$"
42
+ if (!/^[A-Za-z0-9]*$/.test(user.username)) {
43
+ throw new Error(context.t('error.invalid x', { x: context.t('field.username') }))
33
44
  }
34
45
 
35
46
  // consider if validation password rule is required
@@ -38,14 +49,14 @@ export class UserMutation {
38
49
 
39
50
  const salt = User.generateSalt()
40
51
 
41
- return await getRepository(User).save({
52
+ return await userRepository.save({
42
53
  creator: context.state.user,
43
54
  updater: context.state.user,
44
55
  ...user,
45
56
  domains: [domain],
46
57
  roles:
47
58
  user.roles && user.roles.length
48
- ? await getRepository(Role).findBy({
59
+ ? await getRepository(Role, tx).findBy({
49
60
  id: In(user.roles.map(role => role.id)),
50
61
  domain: { id: domain.id }
51
62
  })
@@ -64,7 +75,7 @@ export class UserMutation {
64
75
  @Arg('patch') patch: UserPatch,
65
76
  @Ctx() context: ResolverContext
66
77
  ) {
67
- const { domain, user: updater }: { domain: Domain; user: User } = context.state
78
+ const { domain, user: updater, tx }: { domain: Domain; user: User; tx?: EntityManager } = context.state
68
79
  const qb: SelectQueryBuilder<User> = buildDomainUsersQueryBuilder(domain.id, 'USER')
69
80
  const user: User = await qb
70
81
  .andWhere('LOWER(USER.email) = :email', { email: email?.toLowerCase().trim() || '' })
@@ -73,14 +84,16 @@ export class UserMutation {
73
84
  .getOne()
74
85
 
75
86
  if (patch.roles) {
76
- patch.roles = await getRepository(Role).find({ where: { id: In(patch.roles.map((r: Partial<Role>) => r.id)) } })
87
+ patch.roles = await getRepository(Role, tx).find({
88
+ where: { id: In(patch.roles.map((r: Partial<Role>) => r.id)) }
89
+ })
77
90
  }
78
91
 
79
92
  if (patch.status && patch.status === 'activated') {
80
93
  user.status = UserStatus.ACTIVATED
81
94
  }
82
95
 
83
- return await getRepository(User).save({
96
+ return await getRepository(User, tx).save({
84
97
  ...user,
85
98
  ...patch,
86
99
  updater
@@ -92,7 +105,7 @@ export class UserMutation {
92
105
  @Mutation(returns => [User], { description: 'To modify multiple users information' })
93
106
  async updateMultipleUser(@Arg('patches', type => [UserPatch]) patches: UserPatch[], @Ctx() context: ResolverContext) {
94
107
  const { domain, user, tx } = context.state
95
- const userRepo = tx.getRepository(User)
108
+ const userRepo = getRepository(User, tx)
96
109
 
97
110
  let results = []
98
111
  const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
@@ -183,10 +196,10 @@ export class UserMutation {
183
196
  @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
184
197
  @Directive('@transaction')
185
198
  @Mutation(returns => Boolean, { description: 'To delete a user' })
186
- async deleteUser(@Arg('email', type => GraphQLEmailAddress) email: string, @Ctx() context: ResolverContext) {
199
+ async deleteUser(@Arg('username') username: string, @Ctx() context: ResolverContext) {
187
200
  const { tx } = context.state
188
201
 
189
- await commonDeleteUser({ email }, tx)
202
+ await commonDeleteUser({ username }, tx)
190
203
 
191
204
  return true
192
205
  }
@@ -194,25 +207,31 @@ export class UserMutation {
194
207
  @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
195
208
  @Directive('@transaction')
196
209
  @Mutation(returns => Boolean, { description: 'To delete some users' })
197
- async deleteUsers(@Arg('emails', type => [String]) emails: string[], @Ctx() context: ResolverContext) {
210
+ async deleteUsers(@Arg('usernames', type => [String]) usernames: string[], @Ctx() context: ResolverContext) {
198
211
  const { tx } = context.state
199
- await commonDeleteUsers({ emails }, tx)
212
+ await commonDeleteUsers({ usernames }, tx)
200
213
 
201
214
  return true
202
215
  }
203
216
 
204
217
  @Directive('@transaction')
205
218
  @Mutation(returns => Boolean, { description: 'To invite new user' })
206
- async inviteUser(
207
- @Arg('email', type => GraphQLEmailAddress) email: string,
208
- @Ctx() context: ResolverContext
209
- ): Promise<boolean> {
210
- const { domain } = context.state
211
- const invitee: User = await getRepository(User).findOne({
212
- where: { email: ILike(email) },
219
+ async inviteUser(@Arg('username') username: string, @Ctx() context: ResolverContext): Promise<boolean> {
220
+ const { domain, tx } = context.state
221
+ const userRepository = getRepository(User, tx)
222
+
223
+ var invitee: User = await userRepository.findOne({
224
+ where: { username },
213
225
  relations: ['domains']
214
226
  })
215
227
 
228
+ if (!invitee && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
229
+ invitee = await userRepository.findOne({
230
+ where: { email: ILike(username) },
231
+ relations: ['domains']
232
+ })
233
+ }
234
+
216
235
  if (!invitee) {
217
236
  throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
218
237
  }
@@ -221,8 +240,9 @@ export class UserMutation {
221
240
  if (existingDomains.find((d: Domain) => d.id === domain.id)) {
222
241
  throw new Error(context.t('error.x already exists in y', { x: context.t('field.user'), y: domain.name }))
223
242
  }
243
+
224
244
  invitee.domains = [...existingDomains, domain]
225
- await getRepository(User).save(invitee)
245
+ await userRepository.save(invitee)
226
246
 
227
247
  return true
228
248
  }
@@ -230,15 +250,22 @@ export class UserMutation {
230
250
  @Directive('@transaction')
231
251
  @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
232
252
  @Mutation(returns => Boolean, { description: 'To delete domain user' })
233
- async deleteDomainUser(
234
- @Arg('email', type => GraphQLEmailAddress) email: string,
235
- @Ctx() context: ResolverContext
236
- ): Promise<boolean> {
253
+ async deleteDomainUser(@Arg('username') username: string, @Ctx() context: ResolverContext): Promise<boolean> {
237
254
  const { tx, domain } = context.state
255
+ const userRepository = getRepository(User, tx)
256
+
257
+ var user: User = await userRepository.findOne({
258
+ where: { username },
259
+ relations: ['domains', 'roles']
260
+ })
261
+
262
+ if (!user && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
263
+ user = await userRepository.findOne({
264
+ where: { email: ILike(username) },
265
+ relations: ['domains', 'roles']
266
+ })
267
+ }
238
268
 
239
- let user: User = await tx
240
- .getRepository(User)
241
- .findOne({ where: { email: ILike(email) }, relations: ['domains', 'roles', 'roles.domain'] })
242
269
  if (!user) {
243
270
  throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
244
271
  }
@@ -252,9 +279,9 @@ export class UserMutation {
252
279
  user.domains.splice(targetDomainIdx, 1)
253
280
 
254
281
  // Remove domain's roles that user has
255
- user.roles = user.roles.filter((role: Role) => role.domain.id !== domain.id)
282
+ user.roles = user.roles.filter((role: Role) => role.domainId !== domain.id)
256
283
 
257
- await tx.getRepository(User).save(user)
284
+ await userRepository.save(user)
258
285
 
259
286
  return true
260
287
  }
@@ -262,16 +289,40 @@ export class UserMutation {
262
289
  @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')
263
290
  @Directive('@transaction')
264
291
  @Mutation(returns => Boolean, { description: 'To transfer owner of domain' })
265
- async transferOwner(
266
- @Arg('email', type => GraphQLEmailAddress) email: string,
267
- @Ctx() context: ResolverContext
268
- ): Promise<boolean> {
269
- const { domain } = context.state
270
- const user: User = await getRepository(User).findOne({
271
- where: { email: ILike(email) }
292
+ async transferOwner(@Arg('username') username: string, @Ctx() context: ResolverContext): Promise<boolean> {
293
+ const { domain, tx } = context.state
294
+ const userRepository = getRepository(User, tx)
295
+
296
+ var user: User = await userRepository.findOne({
297
+ where: { username },
298
+ relations: ['domains']
272
299
  })
300
+
301
+ if (!user && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
302
+ user = await userRepository.findOne({
303
+ where: { email: ILike(username) },
304
+ relations: ['domains']
305
+ })
306
+ }
307
+
308
+ if (!user) {
309
+ throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
310
+ }
311
+
312
+ if (user.status !== UserStatus.ACTIVATED) {
313
+ throw new Error('Only activated users are eligible to receive admin privileges.')
314
+ }
315
+
316
+ if (user.domains.map((d: Domain) => d.id).indexOf(domain.id) < 0) {
317
+ throw new Error(`User is not belongs to current domain`)
318
+ }
319
+
320
+ if (user.roles.filter((r: Role) => r.domainId == domain.id).length == 0) {
321
+ throw new Error(`Only users with at least one role in this domain are eligible to receive admin privileges.`)
322
+ }
323
+
273
324
  domain.owner = user.id
274
- await getRepository(Domain).save(domain)
325
+ await getRepository(Domain, tx).save(domain)
275
326
 
276
327
  return true
277
328
  }
@@ -279,15 +330,24 @@ export class UserMutation {
279
330
  @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
280
331
  @Directive('@transaction')
281
332
  @Mutation(returns => Boolean, { description: 'To activate user' })
282
- async activateUser(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<boolean> {
333
+ async activateUser(@Arg('username') username: string, @Ctx() context: ResolverContext): Promise<boolean> {
283
334
  const { tx, domain } = context.state
335
+ const userRepository = getRepository(User, tx)
284
336
 
285
- const targetUser: User = await tx.getRepository(User).findOne({
286
- where: { id: userId },
337
+ var targetUser: User = await userRepository.findOne({
338
+ where: { username },
287
339
  relations: ['domains']
288
340
  })
341
+
342
+ if (!targetUser && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
343
+ targetUser = await userRepository.findOne({
344
+ where: { email: ILike(username) },
345
+ relations: ['domains']
346
+ })
347
+ }
348
+
289
349
  if (!targetUser) {
290
- throw new Error('No user found')
350
+ throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
291
351
  }
292
352
 
293
353
  if (!targetUser?.domains?.find((userDomain: Domain) => userDomain.id === domain.id)) {
@@ -297,7 +357,7 @@ export class UserMutation {
297
357
  targetUser.failCount = 0
298
358
  targetUser.status = UserStatus.ACTIVATED
299
359
 
300
- await tx.getRepository(User).save(targetUser)
360
+ await userRepository.save(targetUser)
301
361
 
302
362
  return true
303
363
  }
@@ -305,15 +365,24 @@ export class UserMutation {
305
365
  @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
306
366
  @Directive('@transaction')
307
367
  @Mutation(returns => Boolean, { description: 'To inactivate user' })
308
- async inactivateUser(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<boolean> {
368
+ async inactivateUser(@Arg('username') username: string, @Ctx() context: ResolverContext): Promise<boolean> {
309
369
  const { tx, domain } = context.state
370
+ const userRepository = getRepository(User, tx)
310
371
 
311
- const targetUser: User = await tx.getRepository(User).findOne({
312
- where: { id: userId },
372
+ var targetUser: User = await userRepository.findOne({
373
+ where: { username },
313
374
  relations: ['domains']
314
375
  })
376
+
377
+ if (!targetUser && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
378
+ targetUser = await userRepository.findOne({
379
+ where: { email: ILike(username) },
380
+ relations: ['domains']
381
+ })
382
+ }
383
+
315
384
  if (!targetUser) {
316
- throw new Error('No user found')
385
+ throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
317
386
  }
318
387
 
319
388
  if (!targetUser?.domains?.find((userDomain: Domain) => userDomain.id === domain.id)) {
@@ -326,7 +395,7 @@ export class UserMutation {
326
395
 
327
396
  targetUser.status = UserStatus.INACTIVE
328
397
 
329
- await tx.getRepository(User).save(targetUser)
398
+ await userRepository.save(targetUser)
330
399
 
331
400
  return true
332
401
  }
@@ -334,7 +403,7 @@ export class UserMutation {
334
403
  @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
335
404
  @Directive('@transaction')
336
405
  @Mutation(returns => Boolean, { description: 'To reset password to default' })
337
- async resetPasswordToDefault(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<boolean> {
406
+ async resetPasswordToDefault(@Arg('username') username: string, @Ctx() context: ResolverContext): Promise<boolean> {
338
407
  const { tx, domain } = context.state
339
408
 
340
409
  const { defaultPassword } = config.get('password')
@@ -342,12 +411,22 @@ export class UserMutation {
342
411
  throw new Error('No default password found')
343
412
  }
344
413
 
345
- const targetUser: User = await tx.getRepository(User).findOne({
346
- where: { id: userId },
414
+ const userRepository = getRepository(User, tx)
415
+
416
+ var targetUser: User = await userRepository.findOne({
417
+ where: { username },
347
418
  relations: ['domains']
348
419
  })
420
+
421
+ if (!targetUser && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
422
+ targetUser = await userRepository.findOne({
423
+ where: { email: ILike(username) },
424
+ relations: ['domains']
425
+ })
426
+ }
427
+
349
428
  if (!targetUser) {
350
- throw new Error('No user found')
429
+ throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
351
430
  }
352
431
 
353
432
  if (!targetUser?.domains?.find((userDomain: Domain) => userDomain.id === domain.id)) {
@@ -356,7 +435,8 @@ export class UserMutation {
356
435
 
357
436
  targetUser.salt = User.generateSalt()
358
437
  targetUser.password = User.encode(defaultPassword, targetUser.salt)
359
- await tx.getRepository(User).save(targetUser)
438
+
439
+ await userRepository.save(targetUser)
360
440
 
361
441
  return true
362
442
  }
@@ -365,18 +445,28 @@ export class UserMutation {
365
445
  @Directive('@transaction')
366
446
  @Mutation(returns => User, { description: 'To update roles for a user' })
367
447
  async updateUserRoles(
368
- @Arg('userId') userId: string,
448
+ @Arg('username') username: string,
369
449
  @Arg('availableRoles', type => [ObjectRef]) availableRoles: ObjectRef[],
370
450
  @Arg('selectedRoles', type => [ObjectRef]) selectedRoles: ObjectRef[],
371
451
  @Ctx() context: ResolverContext
372
452
  ) {
373
453
  const { domain, tx } = context.state
374
- let user: User = await tx.getRepository(User).findOne({
375
- where: { id: userId },
454
+ const userRepository = getRepository(User, tx)
455
+
456
+ var user: User = await userRepository.findOne({
457
+ where: { username },
376
458
  relations: ['domains', 'roles']
377
459
  })
460
+
461
+ if (!user && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
462
+ user = await userRepository.findOne({
463
+ where: { email: ILike(username) },
464
+ relations: ['domains', 'roles']
465
+ })
466
+ }
467
+
378
468
  if (!user) {
379
- throw new Error('Failed to find user')
469
+ throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
380
470
  }
381
471
 
382
472
  if (user.domains.map((d: Domain) => d.id).indexOf(domain.id) < 0) {
@@ -387,6 +477,6 @@ export class UserMutation {
387
477
  user.roles = user.roles.filter((r: Role) => availableRoleIds.indexOf(r.id) < 0)
388
478
  user.roles = user.roles.concat(selectedRoles as Role[])
389
479
 
390
- return await tx.getRepository(User).save(user)
480
+ return await userRepository.save(user)
391
481
  }
392
482
  }
@@ -35,6 +35,9 @@ export class PasswordRule {
35
35
 
36
36
  @InputType()
37
37
  export class NewUser {
38
+ @Field()
39
+ username: string
40
+
38
41
  @Field()
39
42
  name: string
40
43
 
@@ -2,7 +2,20 @@ import crypto from 'crypto'
2
2
  import jwt from 'jsonwebtoken'
3
3
  import { Directive, Field, ID, ObjectType } from 'type-graphql'
4
4
  import { GraphQLEmailAddress } from 'graphql-scalars'
5
- import { Column, CreateDateColumn, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'
5
+ import {
6
+ Column,
7
+ CreateDateColumn,
8
+ Entity,
9
+ ILike,
10
+ Index,
11
+ JoinTable,
12
+ ManyToMany,
13
+ ManyToOne,
14
+ OneToMany,
15
+ PrimaryGeneratedColumn,
16
+ RelationId,
17
+ UpdateDateColumn
18
+ } from 'typeorm'
6
19
 
7
20
  import { config } from '@things-factory/env'
8
21
  import { Domain, getRepository } from '@things-factory/shell'
@@ -31,13 +44,23 @@ export enum UserStatus {
31
44
  }
32
45
 
33
46
  @Entity()
34
- @Index('ix_user_0', (user: User) => [user.email], { unique: true })
47
+ @Index('ix_user_0', (user: User) => [user.email], {
48
+ unique: true
49
+ })
50
+ @Index('ix_user_1', (user: User) => [user.username], {
51
+ unique: true,
52
+ where: '"username" IS NOT NULL'
53
+ })
35
54
  @ObjectType()
36
55
  export class User {
37
56
  @PrimaryGeneratedColumn('uuid')
38
57
  @Field(type => ID)
39
58
  readonly id: string
40
59
 
60
+ @Column({ nullable: true })
61
+ @Field({ nullable: true })
62
+ username: string
63
+
41
64
  @Column()
42
65
  @Field({ nullable: true })
43
66
  name: string
@@ -58,7 +81,15 @@ export class User {
58
81
  @Directive('@privilege(category: "security", privilege: "query", domainOwnerGranted: true)')
59
82
  @Column({
60
83
  nullable: true,
61
- type: DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? 'longtext' : DATABASE_TYPE == 'oracle' ? 'clob' : 'varchar'
84
+ type:
85
+ DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
86
+ ? 'longtext'
87
+ : DATABASE_TYPE == 'oracle'
88
+ ? 'clob'
89
+ : DATABASE_TYPE == 'mssql'
90
+ ? 'nvarchar'
91
+ : 'varchar',
92
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
62
93
  })
63
94
  password: string
64
95
 
@@ -89,8 +120,17 @@ export class User {
89
120
  ssoId: string
90
121
 
91
122
  @Column({
92
- type: DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? 'enum' : DATABASE_TYPE == 'oracle' ? 'varchar2' : 'smallint',
93
- enum: UserStatus,
123
+ type:
124
+ DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
125
+ ? 'enum'
126
+ : DATABASE_TYPE == 'oracle'
127
+ ? 'varchar2'
128
+ : DATABASE_TYPE == 'mssql'
129
+ ? 'nvarchar'
130
+ : 'varchar',
131
+ enum:
132
+ DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? UserStatus : undefined,
133
+ length: DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? undefined : 32,
94
134
  default: UserStatus.INACTIVE
95
135
  })
96
136
  @Field(type => String)
@@ -136,15 +176,10 @@ export class User {
136
176
 
137
177
  /* signing for jsonwebtoken */
138
178
  async sign(options?) {
139
- var { expiresIn = sessionExpirySeconds, subdomain } = options || {}
179
+ var { expiresIn = sessionExpirySeconds } = options || {}
140
180
 
141
181
  var user = {
142
- id: this.id,
143
- userType: this.userType,
144
- status: this.status,
145
- domain: {
146
- subdomain
147
- }
182
+ username: this.username || this.email
148
183
  }
149
184
 
150
185
  return await jwt.sign(user, SECRET, {
@@ -233,18 +268,39 @@ export class User {
233
268
  }
234
269
 
235
270
  static async checkAuth(decoded) {
236
- if (decoded?.id === undefined) {
271
+ // id 하위호환성을 위해 단기적으로 유지함
272
+ const { id, username } = decoded || {}
273
+
274
+ if (!id && !username) {
237
275
  throw new AuthError({
238
276
  errorCode: AuthError.ERROR_CODES.USER_NOT_FOUND
239
277
  })
240
278
  }
241
279
 
242
280
  const repository = getRepository(User)
243
- var user = await repository.findOne({
244
- where: { id: decoded.id },
245
- relations: ['domains', 'credentials'],
246
- cache: true
247
- })
281
+ if (id) {
282
+ var user = await repository.findOne({
283
+ where: { id },
284
+ relations: ['domains', 'credentials'],
285
+ cache: true
286
+ })
287
+ } else {
288
+ var user = await repository.findOne({
289
+ where: { username },
290
+ relations: ['domains', 'credentials'],
291
+ cache: true
292
+ })
293
+
294
+ if (!user && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(username)) {
295
+ user = await repository.findOne({
296
+ where: {
297
+ email: ILike(username)
298
+ },
299
+ relations: ['domains', 'credentials'],
300
+ cache: true
301
+ })
302
+ }
303
+ }
248
304
 
249
305
  if (!user)
250
306
  throw new AuthError({
@@ -30,9 +30,15 @@ export class VerificationToken {
30
30
  DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
31
31
  ? 'enum'
32
32
  : DATABASE_TYPE == 'oracle'
33
- ? 'varchar2'
34
- : 'smallint',
35
- enum: VerificationTokenType,
33
+ ? 'varchar2'
34
+ : DATABASE_TYPE == 'mssql'
35
+ ? 'nvarchar'
36
+ : 'varchar',
37
+ enum:
38
+ DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
39
+ ? VerificationTokenType
40
+ : undefined,
41
+ length: DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? undefined : 32,
36
42
  default: VerificationTokenType.ACTIVATION
37
43
  })
38
44
  @Field()
@@ -1,4 +1,4 @@
1
- export function getUnlockUserEmailForm({ name, resetUrl }) {
1
+ export function getUnlockUserEmailForm({ username, name, resetUrl }) {
2
2
  return `
3
3
  <html lang="en">
4
4
  <head>
@@ -1,4 +1,4 @@
1
- export function getInvitationEmailForm({ email, acceptUrl }) {
1
+ export function getInvitationEmailForm({ username, email, acceptUrl }) {
2
2
  return `
3
3
  <html lang="en">
4
4
  <head>
@@ -1,4 +1,4 @@
1
- export function getVerificationEmailForm({ name, verifyUrl }) {
1
+ export function getVerificationEmailForm({ username, name, verifyUrl }) {
2
2
  return `
3
3
  <html lang="en">
4
4
  <head>
@@ -0,0 +1,29 @@
1
+ import { Domain, getRepository } from '@things-factory/shell'
2
+
3
+ import { User } from '../service/user/user'
4
+ import { Role } from 'service'
5
+
6
+ /**
7
+ * @description 사용자가 특정 도메인 또는 상위 도메인에서 특정 역할을 가지고 있는지 확인합니다.
8
+ *
9
+ * @param roleId 확인할 역할의 ID
10
+ * @param domain 역할을 확인할 도메인
11
+ * @param user 역할을 확인할 사용자
12
+ *
13
+ * @returns 사용자가 도메인 또는 상위 도메인에서 역할을 가지고 있는지 여부를 나타내는 boolean을 반환하는 Promise
14
+ */
15
+ export async function checkUserHasRole(roleId: string, domain: Domain, user: User): Promise<Boolean> {
16
+ if (!roleId) {
17
+ return true
18
+ }
19
+
20
+ const me = await getRepository(User).findOne({
21
+ where: { id: user.id },
22
+ relations: ['roles']
23
+ })
24
+
25
+ return me.roles
26
+ .filter(role => role.domainId === domain.id || (domain.parentId && role.domainId === domain.parentId))
27
+ .map(role => role.id)
28
+ .includes(roleId)
29
+ }