@things-factory/auth-base 8.0.0-beta.8 → 8.0.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 (212) hide show
  1. package/client/actions/auth.ts +24 -0
  2. package/client/auth.ts +272 -0
  3. package/client/bootstrap.ts +47 -0
  4. package/client/directive/privileged.ts +28 -0
  5. package/client/index.ts +3 -0
  6. package/client/profiled.ts +83 -0
  7. package/client/reducers/auth.ts +31 -0
  8. package/dist-client/index.d.ts +0 -1
  9. package/dist-client/index.js +0 -1
  10. package/dist-client/index.js.map +1 -1
  11. package/dist-client/tsconfig.tsbuildinfo +1 -1
  12. package/dist-server/constants/error-code.d.ts +0 -2
  13. package/dist-server/constants/error-code.js +1 -3
  14. package/dist-server/constants/error-code.js.map +1 -1
  15. package/dist-server/controllers/change-pwd.js +2 -2
  16. package/dist-server/controllers/change-pwd.js.map +1 -1
  17. package/dist-server/controllers/delete-user.js +12 -13
  18. package/dist-server/controllers/delete-user.js.map +1 -1
  19. package/dist-server/controllers/invitation.d.ts +1 -2
  20. package/dist-server/controllers/invitation.js +5 -30
  21. package/dist-server/controllers/invitation.js.map +1 -1
  22. package/dist-server/controllers/profile.d.ts +3 -4
  23. package/dist-server/controllers/profile.js +2 -20
  24. package/dist-server/controllers/profile.js.map +1 -1
  25. package/dist-server/controllers/signin.d.ts +1 -4
  26. package/dist-server/controllers/signin.js +1 -17
  27. package/dist-server/controllers/signin.js.map +1 -1
  28. package/dist-server/controllers/signup.js +4 -13
  29. package/dist-server/controllers/signup.js.map +1 -1
  30. package/dist-server/controllers/unlock-user.js +0 -1
  31. package/dist-server/controllers/unlock-user.js.map +1 -1
  32. package/dist-server/controllers/verification.js +0 -1
  33. package/dist-server/controllers/verification.js.map +1 -1
  34. package/dist-server/middlewares/signin-middleware.js +4 -9
  35. package/dist-server/middlewares/signin-middleware.js.map +1 -1
  36. package/dist-server/middlewares/webauthn-middleware.js.map +1 -1
  37. package/dist-server/migrations/1548206416130-SeedUser.js +1 -2
  38. package/dist-server/migrations/1548206416130-SeedUser.js.map +1 -1
  39. package/dist-server/router/auth-checkin-router.js +2 -8
  40. package/dist-server/router/auth-checkin-router.js.map +1 -1
  41. package/dist-server/router/auth-private-process-router.js +7 -12
  42. package/dist-server/router/auth-private-process-router.js.map +1 -1
  43. package/dist-server/router/auth-public-process-router.js +9 -20
  44. package/dist-server/router/auth-public-process-router.js.map +1 -1
  45. package/dist-server/router/auth-signin-router.js +3 -3
  46. package/dist-server/router/auth-signin-router.js.map +1 -1
  47. package/dist-server/router/webauthn-router.js +1 -51
  48. package/dist-server/router/webauthn-router.js.map +1 -1
  49. package/dist-server/service/invitation/invitation-mutation.d.ts +2 -3
  50. package/dist-server/service/invitation/invitation-mutation.js +8 -20
  51. package/dist-server/service/invitation/invitation-mutation.js.map +1 -1
  52. package/dist-server/service/user/user-mutation.d.ts +9 -10
  53. package/dist-server/service/user/user-mutation.js +54 -112
  54. package/dist-server/service/user/user-mutation.js.map +1 -1
  55. package/dist-server/service/user/user-types.d.ts +0 -1
  56. package/dist-server/service/user/user-types.js +0 -4
  57. package/dist-server/service/user/user-types.js.map +1 -1
  58. package/dist-server/service/user/user.d.ts +0 -1
  59. package/dist-server/service/user/user.js +14 -40
  60. package/dist-server/service/user/user.js.map +1 -1
  61. package/dist-server/templates/account-unlock-email.d.ts +1 -2
  62. package/dist-server/templates/account-unlock-email.js +1 -1
  63. package/dist-server/templates/account-unlock-email.js.map +1 -1
  64. package/dist-server/templates/invitation-email.d.ts +1 -2
  65. package/dist-server/templates/invitation-email.js +1 -1
  66. package/dist-server/templates/invitation-email.js.map +1 -1
  67. package/dist-server/templates/verification-email.d.ts +1 -2
  68. package/dist-server/templates/verification-email.js +1 -1
  69. package/dist-server/templates/verification-email.js.map +1 -1
  70. package/dist-server/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +6 -6
  72. package/server/constants/error-code.ts +20 -0
  73. package/server/constants/error-message.ts +0 -0
  74. package/server/constants/max-age.ts +1 -0
  75. package/server/controllers/auth.ts +5 -0
  76. package/server/controllers/change-pwd.ts +99 -0
  77. package/server/controllers/checkin.ts +21 -0
  78. package/server/controllers/delete-user.ts +68 -0
  79. package/server/controllers/invitation.ts +132 -0
  80. package/server/controllers/profile.ts +28 -0
  81. package/server/controllers/reset-password.ts +126 -0
  82. package/server/controllers/signin.ts +79 -0
  83. package/server/controllers/signup.ts +60 -0
  84. package/server/controllers/unlock-user.ts +61 -0
  85. package/server/controllers/utils/make-invitation-token.ts +5 -0
  86. package/server/controllers/utils/make-verification-token.ts +4 -0
  87. package/server/controllers/utils/password-rule.ts +120 -0
  88. package/server/controllers/utils/save-invitation-token.ts +10 -0
  89. package/server/controllers/utils/save-verification-token.ts +12 -0
  90. package/server/controllers/verification.ts +83 -0
  91. package/server/errors/auth-error.ts +24 -0
  92. package/server/errors/index.ts +2 -0
  93. package/server/errors/user-domain-not-match-error.ts +29 -0
  94. package/server/index.ts +37 -0
  95. package/server/middlewares/authenticate-401-middleware.ts +114 -0
  96. package/server/middlewares/domain-authenticate-middleware.ts +78 -0
  97. package/server/middlewares/graphql-authenticate-middleware.ts +13 -0
  98. package/server/middlewares/index.ts +67 -0
  99. package/server/middlewares/jwt-authenticate-middleware.ts +84 -0
  100. package/server/middlewares/signin-middleware.ts +55 -0
  101. package/server/middlewares/webauthn-middleware.ts +127 -0
  102. package/server/migrations/1548206416130-SeedUser.ts +59 -0
  103. package/server/migrations/1566805283882-SeedPrivilege.ts +28 -0
  104. package/server/migrations/index.ts +9 -0
  105. package/server/router/auth-checkin-router.ts +107 -0
  106. package/server/router/auth-private-process-router.ts +107 -0
  107. package/server/router/auth-public-process-router.ts +302 -0
  108. package/server/router/auth-signin-router.ts +55 -0
  109. package/server/router/auth-signup-router.ts +95 -0
  110. package/server/router/index.ts +9 -0
  111. package/server/router/oauth2/index.ts +2 -0
  112. package/server/router/oauth2/oauth2-authorize-router.ts +81 -0
  113. package/server/router/oauth2/oauth2-router.ts +165 -0
  114. package/server/router/oauth2/oauth2-server.ts +262 -0
  115. package/server/router/oauth2/passport-oauth2-client-password.ts +87 -0
  116. package/server/router/oauth2/passport-refresh-token.ts +87 -0
  117. package/server/router/path-base-domain-router.ts +8 -0
  118. package/server/router/site-root-router.ts +48 -0
  119. package/server/router/webauthn-router.ts +87 -0
  120. package/server/routes.ts +80 -0
  121. package/server/service/app-binding/app-binding-mutation.ts +22 -0
  122. package/server/service/app-binding/app-binding-query.ts +92 -0
  123. package/server/service/app-binding/app-binding-types.ts +11 -0
  124. package/server/service/app-binding/app-binding.ts +17 -0
  125. package/server/service/app-binding/index.ts +4 -0
  126. package/server/service/appliance/appliance-mutation.ts +113 -0
  127. package/server/service/appliance/appliance-query.ts +76 -0
  128. package/server/service/appliance/appliance-types.ts +56 -0
  129. package/server/service/appliance/appliance.ts +133 -0
  130. package/server/service/appliance/index.ts +6 -0
  131. package/server/service/application/application-mutation.ts +104 -0
  132. package/server/service/application/application-query.ts +98 -0
  133. package/server/service/application/application-types.ts +76 -0
  134. package/server/service/application/application.ts +216 -0
  135. package/server/service/application/index.ts +6 -0
  136. package/server/service/auth-provider/auth-provider-mutation.ts +159 -0
  137. package/server/service/auth-provider/auth-provider-parameter-spec.ts +24 -0
  138. package/server/service/auth-provider/auth-provider-query.ts +88 -0
  139. package/server/service/auth-provider/auth-provider-type.ts +67 -0
  140. package/server/service/auth-provider/auth-provider.ts +155 -0
  141. package/server/service/auth-provider/index.ts +7 -0
  142. package/server/service/domain-generator/domain-generator-mutation.ts +117 -0
  143. package/server/service/domain-generator/domain-generator-types.ts +46 -0
  144. package/server/service/domain-generator/index.ts +3 -0
  145. package/server/service/granted-role/granted-role-mutation.ts +156 -0
  146. package/server/service/granted-role/granted-role-query.ts +60 -0
  147. package/server/service/granted-role/granted-role.ts +27 -0
  148. package/server/service/granted-role/index.ts +6 -0
  149. package/server/service/index.ts +90 -0
  150. package/server/service/invitation/index.ts +6 -0
  151. package/server/service/invitation/invitation-mutation.ts +63 -0
  152. package/server/service/invitation/invitation-query.ts +33 -0
  153. package/server/service/invitation/invitation-types.ts +11 -0
  154. package/server/service/invitation/invitation.ts +63 -0
  155. package/server/service/login-history/index.ts +5 -0
  156. package/server/service/login-history/login-history-query.ts +51 -0
  157. package/server/service/login-history/login-history-type.ts +12 -0
  158. package/server/service/login-history/login-history.ts +45 -0
  159. package/server/service/partner/index.ts +6 -0
  160. package/server/service/partner/partner-mutation.ts +61 -0
  161. package/server/service/partner/partner-query.ts +102 -0
  162. package/server/service/partner/partner-types.ts +11 -0
  163. package/server/service/partner/partner.ts +57 -0
  164. package/server/service/password-history/index.ts +3 -0
  165. package/server/service/password-history/password-history.ts +16 -0
  166. package/server/service/privilege/index.ts +6 -0
  167. package/server/service/privilege/privilege-directive.ts +77 -0
  168. package/server/service/privilege/privilege-mutation.ts +92 -0
  169. package/server/service/privilege/privilege-query.ts +94 -0
  170. package/server/service/privilege/privilege-types.ts +60 -0
  171. package/server/service/privilege/privilege.ts +102 -0
  172. package/server/service/role/index.ts +6 -0
  173. package/server/service/role/role-mutation.ts +109 -0
  174. package/server/service/role/role-query.ts +155 -0
  175. package/server/service/role/role-types.ts +81 -0
  176. package/server/service/role/role.ts +72 -0
  177. package/server/service/user/domain-query.ts +24 -0
  178. package/server/service/user/index.ts +7 -0
  179. package/server/service/user/user-mutation.ts +413 -0
  180. package/server/service/user/user-query.ts +145 -0
  181. package/server/service/user/user-types.ts +97 -0
  182. package/server/service/user/user.ts +354 -0
  183. package/server/service/users-auth-providers/index.ts +5 -0
  184. package/server/service/users-auth-providers/users-auth-providers.ts +71 -0
  185. package/server/service/verification-token/index.ts +3 -0
  186. package/server/service/verification-token/verification-token.ts +60 -0
  187. package/server/service/web-auth-credential/index.ts +3 -0
  188. package/server/service/web-auth-credential/web-auth-credential.ts +67 -0
  189. package/server/templates/account-unlock-email.ts +65 -0
  190. package/server/templates/invitation-email.ts +66 -0
  191. package/server/templates/reset-password-email.ts +65 -0
  192. package/server/templates/verification-email.ts +66 -0
  193. package/server/types.ts +21 -0
  194. package/server/utils/accepts.ts +11 -0
  195. package/server/utils/access-token-cookie.ts +61 -0
  196. package/server/utils/check-permission.ts +52 -0
  197. package/server/utils/check-user-belongs-domain.ts +19 -0
  198. package/server/utils/check-user-has-role.ts +29 -0
  199. package/server/utils/encrypt-state.ts +22 -0
  200. package/server/utils/get-aes-256-key.ts +13 -0
  201. package/server/utils/get-domain-from-hostname.ts +7 -0
  202. package/server/utils/get-domain-users.ts +38 -0
  203. package/server/utils/get-secret.ts +13 -0
  204. package/server/utils/get-user-domains.ts +112 -0
  205. package/translations/en.json +1 -5
  206. package/translations/ja.json +1 -5
  207. package/translations/ko.json +3 -6
  208. package/translations/ms.json +1 -5
  209. package/translations/zh.json +1 -5
  210. package/dist-client/verify-webauthn.d.ts +0 -13
  211. package/dist-client/verify-webauthn.js +0 -72
  212. package/dist-client/verify-webauthn.js.map +0 -1
@@ -0,0 +1,413 @@
1
+ import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
2
+ import { GraphQLEmailAddress } from 'graphql-scalars'
3
+ import { ILike, In, SelectQueryBuilder, EntityManager } from 'typeorm'
4
+
5
+ import { config } from '@things-factory/env'
6
+ import { Domain, getRepository, ObjectRef } from '@things-factory/shell'
7
+
8
+ import { deleteUser as commonDeleteUser, deleteUsers as commonDeleteUsers } from '../../controllers/delete-user'
9
+ import { buildDomainUsersQueryBuilder } from '../../utils/get-domain-users'
10
+ import { Role } from '../role/role'
11
+ import { User, UserStatus } from './user'
12
+ import { NewUser, UserPatch } from './user-types'
13
+
14
+ @Resolver(User)
15
+ export class UserMutation {
16
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
17
+ @Directive('@transaction')
18
+ @Mutation(returns => User, { description: 'To create new user' })
19
+ async createUser(@Arg('user') user: NewUser, @Ctx() context: ResolverContext) {
20
+ const { domain, tx } = context.state
21
+ const { defaultPassword } = config.get('password')
22
+ const { email } = user
23
+
24
+ user.email = email.trim()
25
+
26
+ const oldUser: User = await getRepository(User, tx).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
+ }
30
+
31
+ if (!user.password && !defaultPassword) {
32
+ throw new Error(context.t('error.initial password or default password should be supported'))
33
+ }
34
+
35
+ // consider if validation password rule is required
36
+ /* check if password is following the rule */
37
+ // User.validatePasswordByRule(user.password, context.lng)
38
+
39
+ const salt = User.generateSalt()
40
+
41
+ return await getRepository(User, tx).save({
42
+ creator: context.state.user,
43
+ updater: context.state.user,
44
+ ...user,
45
+ domains: [domain],
46
+ roles:
47
+ user.roles && user.roles.length
48
+ ? await getRepository(Role, tx).findBy({
49
+ id: In(user.roles.map(role => role.id)),
50
+ domain: { id: domain.id }
51
+ })
52
+ : [],
53
+ salt,
54
+ passwordUpdatedAt: new Date(),
55
+ password: user.password ? User.encode(user.password, salt) : User.encode(defaultPassword, salt)
56
+ })
57
+ }
58
+
59
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
60
+ @Directive('@transaction')
61
+ @Mutation(returns => User, { description: 'To modify user information' })
62
+ async updateUser(
63
+ @Arg('email', type => GraphQLEmailAddress) email: string,
64
+ @Arg('patch') patch: UserPatch,
65
+ @Ctx() context: ResolverContext
66
+ ) {
67
+ const { domain, user: updater, tx }: { domain: Domain; user: User; tx?: EntityManager } = context.state
68
+ const qb: SelectQueryBuilder<User> = buildDomainUsersQueryBuilder(domain.id, 'USER')
69
+ const user: User = await qb
70
+ .andWhere('LOWER(USER.email) = :email', { email: email?.toLowerCase().trim() || '' })
71
+ .leftJoinAndSelect('USER.roles', 'ROLES')
72
+ .leftJoinAndSelect('ROLES.domain', 'R_DOMAIN')
73
+ .getOne()
74
+
75
+ if (patch.roles) {
76
+ patch.roles = await getRepository(Role, tx).find({
77
+ where: { id: In(patch.roles.map((r: Partial<Role>) => r.id)) }
78
+ })
79
+ }
80
+
81
+ if (patch.status && patch.status === 'activated') {
82
+ user.status = UserStatus.ACTIVATED
83
+ }
84
+
85
+ return await getRepository(User, tx).save({
86
+ ...user,
87
+ ...patch,
88
+ updater
89
+ } as any)
90
+ }
91
+
92
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
93
+ @Directive('@transaction')
94
+ @Mutation(returns => [User], { description: 'To modify multiple users information' })
95
+ async updateMultipleUser(@Arg('patches', type => [UserPatch]) patches: UserPatch[], @Ctx() context: ResolverContext) {
96
+ const { domain, user, tx } = context.state
97
+ const userRepo = getRepository(User, tx)
98
+
99
+ let results = []
100
+ const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
101
+ const _updateRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === 'M')
102
+
103
+ if (_createRecords.length > 0) {
104
+ for (let i = 0; i < _createRecords.length; i++) {
105
+ const newRecord = _createRecords[i]
106
+
107
+ // consider if validation password rule is required
108
+ /* check if password is following the rule */
109
+ // User.validatePasswordByRule(newRecord.password, context.lng)
110
+
111
+ const salt = User.generateSalt()
112
+ const result = await userRepo.save({
113
+ ...(newRecord as any),
114
+ domains: [domain],
115
+ salt,
116
+ password: User.encode(newRecord.password, salt),
117
+ passwordUpdatedAt: new Date(),
118
+ creator: user,
119
+ updater: user
120
+ })
121
+
122
+ // repository api는 작동하지 않음.
123
+ // await tx
124
+ // .createQueryBuilder()
125
+ // .insert()
126
+ // .into('users_domains')
127
+ // .values({
128
+ // usersId: result.id,
129
+ // domainsId: domain.id
130
+ // })
131
+ // .execute()
132
+
133
+ results.push({ ...result, cuFlag: '+' })
134
+ }
135
+ }
136
+
137
+ if (_updateRecords.length > 0) {
138
+ for (let i = 0; i < _updateRecords.length; i++) {
139
+ const updateRecord = _updateRecords[i]
140
+ // consider if validation password rule is required
141
+ /* check if password is following the rule */
142
+ // User.validatePasswordByRule(updateRecord.password, context.lng)
143
+
144
+ const user = await userRepo.findOne({ where: { id: updateRecord.id }, relations: ['domains'] })
145
+ var domains = user.domains.find(d => d.id === domain.id) ? user.domains : [...user.domains, domain]
146
+
147
+ const result = await userRepo.save({
148
+ ...user,
149
+ ...(updateRecord as any),
150
+ domains,
151
+ password: updateRecord.password ? User.encode(updateRecord.password, user.salt) : user.password,
152
+ updater: user
153
+ })
154
+
155
+ if (!updateRecord.status) {
156
+ continue
157
+ }
158
+
159
+ // const domain = await user.domain
160
+ // if (!domain) {
161
+ // continue
162
+ // }
163
+
164
+ // const domainId = domain.id
165
+ // const domains = await user.domains
166
+ // if (!domains.find(domain => domain.id == domainId)) {
167
+ // await tx
168
+ // .createQueryBuilder()
169
+ // .insert()
170
+ // .into('users_domains')
171
+ // .values({
172
+ // usersId: user.id,
173
+ // domainsId: domain.id
174
+ // })
175
+ // .execute()
176
+ // }
177
+
178
+ results.push({ ...result, cuFlag: 'M' })
179
+ }
180
+ }
181
+
182
+ return results
183
+ }
184
+
185
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
186
+ @Directive('@transaction')
187
+ @Mutation(returns => Boolean, { description: 'To delete a user' })
188
+ async deleteUser(@Arg('email', type => GraphQLEmailAddress) email: string, @Ctx() context: ResolverContext) {
189
+ const { tx } = context.state
190
+
191
+ await commonDeleteUser({ email }, tx)
192
+
193
+ return true
194
+ }
195
+
196
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
197
+ @Directive('@transaction')
198
+ @Mutation(returns => Boolean, { description: 'To delete some users' })
199
+ async deleteUsers(@Arg('emails', type => [String]) emails: string[], @Ctx() context: ResolverContext) {
200
+ const { tx } = context.state
201
+ await commonDeleteUsers({ emails }, tx)
202
+
203
+ return true
204
+ }
205
+
206
+ @Directive('@transaction')
207
+ @Mutation(returns => Boolean, { description: 'To invite new user' })
208
+ async inviteUser(
209
+ @Arg('email', type => GraphQLEmailAddress) email: string,
210
+ @Ctx() context: ResolverContext
211
+ ): Promise<boolean> {
212
+ const { domain, tx } = context.state
213
+ const invitee: User = await getRepository(User, tx).findOne({
214
+ where: { email: ILike(email) },
215
+ relations: ['domains']
216
+ })
217
+
218
+ if (!invitee) {
219
+ throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
220
+ }
221
+
222
+ const existingDomains: Domain[] = invitee.domains
223
+ if (existingDomains.find((d: Domain) => d.id === domain.id)) {
224
+ throw new Error(context.t('error.x already exists in y', { x: context.t('field.user'), y: domain.name }))
225
+ }
226
+ invitee.domains = [...existingDomains, domain]
227
+ await getRepository(User, tx).save(invitee)
228
+
229
+ return true
230
+ }
231
+
232
+ @Directive('@transaction')
233
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
234
+ @Mutation(returns => Boolean, { description: 'To delete domain user' })
235
+ async deleteDomainUser(
236
+ @Arg('email', type => GraphQLEmailAddress) email: string,
237
+ @Ctx() context: ResolverContext
238
+ ): Promise<boolean> {
239
+ const { tx, domain } = context.state
240
+
241
+ let user: User = await getRepository(User, tx).findOne({
242
+ where: { email: ILike(email) },
243
+ relations: ['domains', 'roles', 'roles.domain']
244
+ })
245
+ if (!user) {
246
+ throw new Error(context.t('error.failed to find x', { x: context.t('field.user') }))
247
+ }
248
+
249
+ const targetDomainIdx: number = user.domains.findIndex((userDomain: Domain) => userDomain.id === domain.id)
250
+ if (targetDomainIdx < 0) {
251
+ throw new Error(context.t('error.x is not a member of y', { x: user.name, y: domain.name }))
252
+ }
253
+
254
+ // Remove domain relation with user
255
+ user.domains.splice(targetDomainIdx, 1)
256
+
257
+ // Remove domain's roles that user has
258
+ user.roles = user.roles.filter((role: Role) => role.domain.id !== domain.id)
259
+
260
+ await getRepository(User, tx).save(user)
261
+
262
+ return true
263
+ }
264
+
265
+ @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')
266
+ @Directive('@transaction')
267
+ @Mutation(returns => Boolean, { description: 'To transfer owner of domain' })
268
+ async transferOwner(
269
+ @Arg('email', type => GraphQLEmailAddress) email: string,
270
+ @Ctx() context: ResolverContext
271
+ ): Promise<boolean> {
272
+ const { domain, tx } = context.state
273
+ const user: User = await getRepository(User, tx).findOne({
274
+ where: { email: ILike(email) },
275
+ relations: ['domains', 'roles']
276
+ })
277
+
278
+ if (!user) {
279
+ throw new Error('Failed to find user')
280
+ }
281
+
282
+ if (user.status !== UserStatus.ACTIVATED) {
283
+ throw new Error('Only activated users are eligible to receive admin privileges.')
284
+ }
285
+
286
+ if (user.domains.map((d: Domain) => d.id).indexOf(domain.id) < 0) {
287
+ throw new Error(`User is not belongs to current domain`)
288
+ }
289
+
290
+ if (user.roles.filter((r: Role) => r.domainId == domain.id).length == 0) {
291
+ throw new Error(`Only users with at least one role in this domain are eligible to receive admin privileges.`)
292
+ }
293
+
294
+ domain.owner = user.id
295
+ await getRepository(Domain, tx).save(domain)
296
+
297
+ return true
298
+ }
299
+
300
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
301
+ @Directive('@transaction')
302
+ @Mutation(returns => Boolean, { description: 'To activate user' })
303
+ async activateUser(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<boolean> {
304
+ const { tx, domain } = context.state
305
+
306
+ const targetUser: User = await getRepository(User, tx).findOne({
307
+ where: { id: userId },
308
+ relations: ['domains']
309
+ })
310
+ if (!targetUser) {
311
+ throw new Error('No user found')
312
+ }
313
+
314
+ if (!targetUser?.domains?.find((userDomain: Domain) => userDomain.id === domain.id)) {
315
+ throw new Error('User is not belong to domain')
316
+ }
317
+
318
+ targetUser.failCount = 0
319
+ targetUser.status = UserStatus.ACTIVATED
320
+
321
+ await getRepository(User, tx).save(targetUser)
322
+
323
+ return true
324
+ }
325
+
326
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
327
+ @Directive('@transaction')
328
+ @Mutation(returns => Boolean, { description: 'To inactivate user' })
329
+ async inactivateUser(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<boolean> {
330
+ const { tx, domain } = context.state
331
+
332
+ const targetUser: User = await getRepository(User, tx).findOne({
333
+ where: { id: userId },
334
+ relations: ['domains']
335
+ })
336
+ if (!targetUser) {
337
+ throw new Error('No user found')
338
+ }
339
+
340
+ if (!targetUser?.domains?.find((userDomain: Domain) => userDomain.id === domain.id)) {
341
+ throw new Error('User is not belong to domain')
342
+ }
343
+
344
+ if (targetUser.userType == 'admin' || targetUser.id === domain.owner) {
345
+ throw new Error('Admin deactivation not allowed')
346
+ }
347
+
348
+ targetUser.status = UserStatus.INACTIVE
349
+
350
+ await getRepository(User, tx).save(targetUser)
351
+
352
+ return true
353
+ }
354
+
355
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
356
+ @Directive('@transaction')
357
+ @Mutation(returns => Boolean, { description: 'To reset password to default' })
358
+ async resetPasswordToDefault(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<boolean> {
359
+ const { tx, domain } = context.state
360
+
361
+ const { defaultPassword } = config.get('password')
362
+ if (!defaultPassword) {
363
+ throw new Error('No default password found')
364
+ }
365
+
366
+ const targetUser: User = await getRepository(User, tx).findOne({
367
+ where: { id: userId },
368
+ relations: ['domains']
369
+ })
370
+ if (!targetUser) {
371
+ throw new Error('No user found')
372
+ }
373
+
374
+ if (!targetUser?.domains?.find((userDomain: Domain) => userDomain.id === domain.id)) {
375
+ throw new Error('User is not belong to domain')
376
+ }
377
+
378
+ targetUser.salt = User.generateSalt()
379
+ targetUser.password = User.encode(defaultPassword, targetUser.salt)
380
+ await getRepository(User, tx).save(targetUser)
381
+
382
+ return true
383
+ }
384
+
385
+ @Directive('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)')
386
+ @Directive('@transaction')
387
+ @Mutation(returns => User, { description: 'To update roles for a user' })
388
+ async updateUserRoles(
389
+ @Arg('userId') userId: string,
390
+ @Arg('availableRoles', type => [ObjectRef]) availableRoles: ObjectRef[],
391
+ @Arg('selectedRoles', type => [ObjectRef]) selectedRoles: ObjectRef[],
392
+ @Ctx() context: ResolverContext
393
+ ) {
394
+ const { domain, tx } = context.state
395
+ let user: User = await getRepository(User, tx).findOne({
396
+ where: { id: userId },
397
+ relations: ['domains', 'roles']
398
+ })
399
+ if (!user) {
400
+ throw new Error('Failed to find user')
401
+ }
402
+
403
+ if (user.domains.map((d: Domain) => d.id).indexOf(domain.id) < 0) {
404
+ throw new Error(`User is not belongs to current domain`)
405
+ }
406
+
407
+ const availableRoleIds: string[] = availableRoles.map((r: Role) => r.id)
408
+ user.roles = user.roles.filter((r: Role) => availableRoleIds.indexOf(r.id) < 0)
409
+ user.roles = user.roles.concat(selectedRoles as Role[])
410
+
411
+ return await getRepository(User, tx).save(user)
412
+ }
413
+ }
@@ -0,0 +1,145 @@
1
+ import { Arg, Args, Ctx, Directive, FieldResolver, Query, Resolver, Root } from 'type-graphql'
2
+ import { GraphQLEmailAddress } from 'graphql-scalars'
3
+ import { ILike, SelectQueryBuilder } from 'typeorm'
4
+
5
+ import { config } from '@things-factory/env'
6
+ import { getRepository, ListParam, getQueryBuilderFromListParams } from '@things-factory/shell'
7
+
8
+ import { checkUserBelongsDomain } from '../../utils/check-user-belongs-domain'
9
+ import { buildDomainUsersQueryBuilder } from '../../utils/get-domain-users'
10
+ import { User } from './user'
11
+ import { PasswordRule, UserList } from './user-types'
12
+
13
+ const passwordRule = config.get('password') || {
14
+ lowerCase: true,
15
+ upperCase: true,
16
+ digit: true,
17
+ specialCharacter: true,
18
+ allowRepeat: false,
19
+ useTightPattern: true,
20
+ useLoosePattern: false,
21
+ tightCharacterLength: 8,
22
+ looseCharacterLength: 15
23
+ }
24
+
25
+ @Resolver(User)
26
+ export class UserQuery {
27
+ @Query(returns => PasswordRule, {
28
+ description:
29
+ 'Retrieves the current password rule configuration for the system, such as required character types and minimum length.'
30
+ })
31
+ passwordRule(@Ctx() context: ResolverContext): PasswordRule {
32
+ return passwordRule
33
+ }
34
+
35
+ @Directive('@privilege(category: "user", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
36
+ @Query(returns => User, { description: 'Fetches a user by their email address within the current domain.' })
37
+ async user(@Arg('email', type => GraphQLEmailAddress) email: string, @Ctx() context: ResolverContext): Promise<User> {
38
+ const { domain } = context.state
39
+
40
+ const qb: SelectQueryBuilder<User> = buildDomainUsersQueryBuilder(domain.id, 'USER')
41
+ qb.andWhere(`LOWER(USER.email) = :email`, { email: email.toLowerCase().trim() })
42
+
43
+ return qb.getOne()
44
+ }
45
+
46
+ @Directive('@privilege(category: "user", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
47
+ @Query(returns => UserList, {
48
+ description: 'Fetches a list of users based on provided search parameters within the current domain.'
49
+ })
50
+ async users(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<UserList> {
51
+ const { domain } = context.state
52
+
53
+ const qb = getQueryBuilderFromListParams({
54
+ repository: getRepository(User),
55
+ params,
56
+ alias: 'USER',
57
+ searchables: ['name', 'email', 'description']
58
+ })
59
+
60
+ qb.select().andWhere(qb => {
61
+ const subQuery = qb
62
+ .subQuery()
63
+ .select('USERS_DOMAINS.users_id')
64
+ .from('users_domains', 'USERS_DOMAINS')
65
+ .where('USERS_DOMAINS.domains_id = :domainId', { domainId: domain.id })
66
+ .getQuery()
67
+
68
+ return 'USER.id IN ' + subQuery
69
+ })
70
+
71
+ const [items, total] = await qb.getManyAndCount()
72
+
73
+ const foundUsers: User[] = items.map((item: User) => {
74
+ item.owner = item.id === domain.owner
75
+ return item
76
+ })
77
+
78
+ return { items: foundUsers, total }
79
+ }
80
+
81
+ @Query(returns => Boolean, { description: 'Checks if the current authenticated user belongs to the current domain.' })
82
+ async checkUserBelongsDomain(@Ctx() context: ResolverContext): Promise<Boolean> {
83
+ const { user, domain } = context.state
84
+
85
+ if (user) {
86
+ return await checkUserBelongsDomain(domain, user)
87
+ } else {
88
+ throw new Error(`Failed to get current user information.`)
89
+ }
90
+ }
91
+
92
+ @Query(returns => Boolean, {
93
+ description: 'Determines whether the system provides a default password when creating a new user.'
94
+ })
95
+ async checkResettablePasswordToDefault(@Ctx() context: ResolverContext): Promise<Boolean> {
96
+ const { defaultPassword } = config.get('password')
97
+
98
+ return Boolean(defaultPassword)
99
+ }
100
+
101
+ @Query(returns => Boolean, {
102
+ description: 'Checks if the system is configured to provide a default password for new users.'
103
+ })
104
+ async checkDefaultPassword(@Ctx() context: ResolverContext): Promise<Boolean> {
105
+ const { defaultPassword } = config.get('password')
106
+
107
+ return Boolean(defaultPassword)
108
+ }
109
+
110
+ @Directive('@privilege(category: "user", privilege: "query")')
111
+ @Query(returns => Boolean, { description: 'Checks if a user with the given email address exists in the system.' })
112
+ async checkUserExistence(@Arg('email', type => GraphQLEmailAddress) email: string): Promise<Boolean> {
113
+ return Boolean(await getRepository(User).count({ where: { email: ILike(email) } }))
114
+ }
115
+
116
+ @FieldResolver()
117
+ async domains(@Root() user: User) {
118
+ return (
119
+ await getRepository(User).findOne({
120
+ where: { id: user.id },
121
+ relations: ['domains']
122
+ })
123
+ ).domains
124
+ }
125
+
126
+ @FieldResolver()
127
+ async roles(@Root() user: User) {
128
+ return (
129
+ await getRepository(User).findOne({
130
+ where: { id: user.id },
131
+ relations: ['roles']
132
+ })
133
+ ).roles
134
+ }
135
+
136
+ @FieldResolver()
137
+ async updater(@Root() user: User): Promise<User> {
138
+ return await getRepository(User).findOneBy({ id: user.updaterId })
139
+ }
140
+
141
+ @FieldResolver()
142
+ async creator(@Root() user: User): Promise<User> {
143
+ return await getRepository(User).findOneBy({ id: user.creatorId })
144
+ }
145
+ }
@@ -0,0 +1,97 @@
1
+ import { ObjectType, InputType, Field, ID, Int } from 'type-graphql'
2
+ import { GraphQLEmailAddress } from 'graphql-scalars'
3
+ import { ObjectRef } from '@things-factory/shell'
4
+ import { User } from './user'
5
+
6
+ @ObjectType()
7
+ export class PasswordRule {
8
+ @Field({ nullable: true })
9
+ lowerCase?: boolean
10
+
11
+ @Field({ nullable: true })
12
+ upperCase?: boolean
13
+
14
+ @Field({ nullable: true })
15
+ digit?: boolean
16
+
17
+ @Field({ nullable: true })
18
+ specialCharacter?: boolean
19
+
20
+ @Field({ nullable: true })
21
+ allowRepeat?: boolean
22
+
23
+ @Field({ nullable: true })
24
+ useTightPattern?: boolean
25
+
26
+ @Field({ nullable: true })
27
+ useLoosePattern?: boolean
28
+
29
+ @Field({ nullable: true })
30
+ tightCharacterLength?: number
31
+
32
+ @Field({ nullable: true })
33
+ looseCharacterLength?: number
34
+ }
35
+
36
+ @InputType()
37
+ export class NewUser {
38
+ @Field()
39
+ name: string
40
+
41
+ @Field({ nullable: true })
42
+ description?: string
43
+
44
+ @Field(type => GraphQLEmailAddress)
45
+ email: string
46
+
47
+ @Field({ nullable: true })
48
+ password?: string
49
+
50
+ @Field({ nullable: true })
51
+ userType?: string
52
+
53
+ @Field(type => [ObjectRef], { nullable: true })
54
+ roles?: ObjectRef[]
55
+ }
56
+
57
+ @InputType()
58
+ export class UserPatch {
59
+ @Field(type => ID, { nullable: true })
60
+ id?: string
61
+
62
+ @Field({ nullable: true })
63
+ name?: string
64
+
65
+ @Field(type => [ObjectRef], { nullable: true })
66
+ domains?: [ObjectRef]
67
+
68
+ @Field({ nullable: true })
69
+ description?: string
70
+
71
+ @Field(type => GraphQLEmailAddress, { nullable: true })
72
+ email?: string
73
+
74
+ @Field({ nullable: true })
75
+ password?: string
76
+
77
+ @Field({ nullable: true })
78
+ status?: string
79
+
80
+ @Field(type => [ObjectRef], { nullable: true })
81
+ roles?: ObjectRef[]
82
+
83
+ @Field({ nullable: true })
84
+ userType?: string
85
+
86
+ @Field({ nullable: true })
87
+ cuFlag?: string
88
+ }
89
+
90
+ @ObjectType()
91
+ export class UserList {
92
+ @Field(type => [User], { nullable: true })
93
+ items: User[]
94
+
95
+ @Field(type => Int, { nullable: true })
96
+ total: number
97
+ }