@drax/identity-back 0.8.11 → 0.10.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 (48) hide show
  1. package/dist/config/IdentityConfig.js +1 -0
  2. package/dist/controllers/UserController.js +152 -5
  3. package/dist/factory/UserRegistryServiceFactory.js +24 -0
  4. package/dist/html/RegistrationCompleteHtml.js +51 -0
  5. package/dist/models/UserModel.js +6 -1
  6. package/dist/repository/mongo/UserApiKeyMongoRepository.js +2 -2
  7. package/dist/repository/mongo/UserMongoRepository.js +28 -0
  8. package/dist/repository/sqlite/UserSqliteRepository.js +44 -1
  9. package/dist/routes/UserRoutes.js +11 -6
  10. package/dist/services/UserEmailService.js +54 -0
  11. package/dist/services/UserService.js +97 -6
  12. package/package.json +7 -6
  13. package/src/config/IdentityConfig.ts +2 -0
  14. package/src/controllers/UserController.ts +190 -21
  15. package/src/html/RegistrationCompleteHtml.ts +52 -0
  16. package/src/interfaces/IUserRepository.ts +5 -0
  17. package/src/models/UserModel.ts +6 -1
  18. package/src/repository/mongo/UserApiKeyMongoRepository.ts +2 -2
  19. package/src/repository/mongo/UserMongoRepository.ts +32 -1
  20. package/src/repository/sqlite/UserSqliteRepository.ts +51 -1
  21. package/src/routes/UserRoutes.ts +16 -6
  22. package/src/services/UserEmailService.ts +78 -0
  23. package/src/services/UserService.ts +107 -12
  24. package/tsconfig.tsbuildinfo +1 -1
  25. package/types/config/IdentityConfig.d.ts +2 -1
  26. package/types/config/IdentityConfig.d.ts.map +1 -1
  27. package/types/controllers/UserController.d.ts +7 -2
  28. package/types/controllers/UserController.d.ts.map +1 -1
  29. package/types/factory/UserApiKeyServiceFactory.d.ts +1 -1
  30. package/types/factory/UserRegistryServiceFactory.d.ts +4 -0
  31. package/types/factory/UserRegistryServiceFactory.d.ts.map +1 -0
  32. package/types/html/RegistrationCompleteHtml.d.ts +3 -0
  33. package/types/html/RegistrationCompleteHtml.d.ts.map +1 -0
  34. package/types/interfaces/IUserRepository.d.ts +4 -0
  35. package/types/interfaces/IUserRepository.d.ts.map +1 -1
  36. package/types/models/UserModel.d.ts.map +1 -1
  37. package/types/repository/mongo/UserApiKeyMongoRepository.d.ts +2 -2
  38. package/types/repository/mongo/UserApiKeyMongoRepository.d.ts.map +1 -1
  39. package/types/repository/mongo/UserMongoRepository.d.ts +5 -0
  40. package/types/repository/mongo/UserMongoRepository.d.ts.map +1 -1
  41. package/types/repository/sqlite/UserApiKeySqliteRepository.d.ts +1 -1
  42. package/types/repository/sqlite/UserSqliteRepository.d.ts +5 -0
  43. package/types/repository/sqlite/UserSqliteRepository.d.ts.map +1 -1
  44. package/types/routes/UserRoutes.d.ts.map +1 -1
  45. package/types/services/UserEmailService.d.ts +7 -0
  46. package/types/services/UserEmailService.d.ts.map +1 -0
  47. package/types/services/UserService.d.ts +6 -0
  48. package/types/services/UserService.d.ts.map +1 -1
@@ -29,7 +29,11 @@ const tableFields: SqliteTableField[] = [
29
29
  {name: "avatar", type: "TEXT", unique: false, primary: false},
30
30
  {name: "origin", type: "TEXT", unique: false, primary: false},
31
31
  {name: "createdAt", type: "TEXT", unique: false, primary: false},
32
- {name: "updatedAt", type: "TEXT", unique: false, primary: false}
32
+ {name: "updatedAt", type: "TEXT", unique: false, primary: false},
33
+ {name: "emailVerified", type: "INTEGER", unique: false, primary: false},
34
+ {name: "phoneVerified", type: "INTEGER", unique: false, primary: false},
35
+ {name: "emailCode", type: "TEXT", unique: false, primary: false},
36
+ {name: "phoneCode", type: "TEXT", unique: false, primary: false},
33
37
  ]
34
38
 
35
39
  class UserSqliteRepository implements IUserRepository {
@@ -95,6 +99,10 @@ class UserSqliteRepository implements IUserRepository {
95
99
 
96
100
  }
97
101
 
102
+ async updatePartial(id: string, userData: IUserUpdate): Promise<IUser> {
103
+ return this.update(id, userData)
104
+ }
105
+
98
106
  async update(id: string, userData: IUserUpdate): Promise<IUser> {
99
107
  try {
100
108
  if (!await this.findRoleById(userData.role)) {
@@ -108,7 +116,9 @@ class UserSqliteRepository implements IUserRepository {
108
116
  const setClauses = Object.keys(userData)
109
117
  .map(field => `${field} = @${field}`)
110
118
  .join(', ');
119
+
111
120
  userData.id = id
121
+
112
122
  const stmt = this.db.prepare(`UPDATE users
113
123
  SET ${setClauses}
114
124
  WHERE id = @id `);
@@ -151,6 +161,16 @@ class UserSqliteRepository implements IUserRepository {
151
161
  return user
152
162
  }
153
163
 
164
+ async findByUsernameWithPassword(username: string): Promise<IUser> {
165
+ const user = this.db.prepare('SELECT * FROM users WHERE username = ?').get(username);
166
+ if (!user) {
167
+ return null
168
+ }
169
+ user.role = await this.findRoleById(user.role)
170
+ user.tenant = await this.findTenantById(user.tenant)
171
+ return user
172
+ }
173
+
154
174
  async findByEmail(email: string): Promise<IUser> {
155
175
  const user = this.db.prepare('SELECT * FROM users WHERE email = ?').get(email);
156
176
  if (!user) {
@@ -161,6 +181,36 @@ class UserSqliteRepository implements IUserRepository {
161
181
  return user
162
182
  }
163
183
 
184
+ async findByEmailCode(code: string): Promise<IUser> {
185
+ const user = this.db.prepare('SELECT * FROM users WHERE emailVerifyCode = ? AND emailVerified = 0').get(code);
186
+ if (!user) {
187
+ return null
188
+ }
189
+ user.role = await this.findRoleById(user.role)
190
+ user.tenant = await this.findTenantById(user.tenant)
191
+ return user
192
+ }
193
+
194
+ async findByRecoveryCode(code: string): Promise<IUser> {
195
+ const user = this.db.prepare('SELECT * FROM users WHERE recoveryCode = ? AND active = 1').get(code);
196
+ if (!user) {
197
+ return null
198
+ }
199
+ user.role = await this.findRoleById(user.role)
200
+ user.tenant = await this.findTenantById(user.tenant)
201
+ return user
202
+ }
203
+
204
+ async findByPhoneCode(code: string): Promise<IUser> {
205
+ const user = this.db.prepare('SELECT * FROM users WHERE phoneCode = ? AND phoneVerified = 0').get(code);
206
+ if (!user) {
207
+ return null
208
+ }
209
+ user.role = await this.findRoleById(user.role)
210
+ user.tenant = await this.findTenantById(user.tenant)
211
+ return user
212
+ }
213
+
164
214
  async paginate({
165
215
  page= 1,
166
216
  limit= 5,
@@ -4,9 +4,9 @@ async function UserRoutes(fastify, options) {
4
4
 
5
5
  const controller: UserController = new UserController()
6
6
 
7
- fastify.post('/api/auth', (req,rep) => controller.auth(req,rep))
7
+ fastify.post('/api/auth/login', (req,rep) => controller.auth(req,rep))
8
8
 
9
- fastify.get('/api/me', (req,rep) => controller.me(req,rep))
9
+ fastify.get('/api/auth/me', (req,rep) => controller.me(req,rep))
10
10
 
11
11
  fastify.get('/api/users/search', (req,rep) => controller.search(req,rep) )
12
12
 
@@ -20,13 +20,23 @@ async function UserRoutes(fastify, options) {
20
20
 
21
21
  fastify.delete('/api/users/:id', (req,rep) => controller.delete(req,rep))
22
22
 
23
- fastify.post('/api/password', (req,rep) => controller.myPassword(req,rep))
23
+ fastify.post('/api/users/register', (req,rep) => controller.register(req,rep))
24
24
 
25
- fastify.post('/api/password/:id', (req,rep) => controller.password(req,rep))
25
+ fastify.get('/api/users/verify-email/:code', (req,rep) => controller.verifyEmail(req,rep))
26
26
 
27
- fastify.post('/api/user/avatar', (req,rep) => controller.updateAvatar(req,rep))
27
+ fastify.get('/api/users/verify-phone/:code', (req,rep) => controller.verifyPhone(req,rep))
28
28
 
29
- fastify.get('/api/user/avatar/:filename', (req,rep) => controller.getAvatar(req,rep))
29
+ fastify.post('/api/users/password/change', (req,rep) => controller.changeMyPassword(req,rep))
30
+
31
+ fastify.post('/api/users/password/change/:id', (req,rep) => controller.changePassword(req,rep))
32
+
33
+ fastify.post('/api/users/password/recovery/request', (req,rep) => controller.passwordRecoveryRequest(req,rep))
34
+
35
+ fastify.post('/api/users/password/recovery/complete', (req,rep) => controller.recoveryPasswordComplete(req,rep))
36
+
37
+ fastify.post('/api/users/avatar', (req,rep) => controller.updateAvatar(req,rep))
38
+
39
+ fastify.get('/api/users/avatar/:filename', (req,rep) => controller.getAvatar(req,rep))
30
40
 
31
41
  }
32
42
 
@@ -0,0 +1,78 @@
1
+ import {EmailTransportConfig, EmailLayoutServiceFactory, EmailTransportServiceFactory} from "@drax/email-back"
2
+ import {CommonConfig, DraxConfig} from "@drax/common-back";
3
+ import type {SendMailOptions} from "nodemailer";
4
+
5
+ class UserEmailService {
6
+
7
+ static async emailVerifyCode(emailCode: string, emailTo:string){
8
+
9
+
10
+ let emailLayout = EmailLayoutServiceFactory.instance
11
+
12
+ let baseurl = DraxConfig.getOrLoad(CommonConfig.BaseUrl)
13
+
14
+ let body = `
15
+ <h2 style="font-size: 22px; color: #333333; font-weight: 600; margin: 0 0 10px 0;">
16
+ Verificación de Email
17
+ </h2>
18
+ <p style="font-size: 16px; line-height: 1.6; color: #555555; margin: 0 0 15px 0;">
19
+ Para confirmar tu email, haz clic en el siguiente enlace:
20
+ </p>
21
+ <a href="${baseurl}/api/users/verify-email/${emailCode}" style="color: #333333; text-decoration: none; border: 1px solid #333333; padding: 10px 20px; text-align: center; text-decoration: none; display: inline-block;">Verificar Email</a>
22
+ `
23
+
24
+ const emailFrom = DraxConfig.getOrLoad(EmailTransportConfig.authUsername)
25
+
26
+ const emailOptions : SendMailOptions = {
27
+ subject: "Verificación de Email",
28
+ from: emailFrom,
29
+ to: emailTo,
30
+ html: emailLayout.html(body)
31
+ }
32
+
33
+ await EmailTransportServiceFactory.instance.sendEmail(emailOptions)
34
+
35
+ }
36
+
37
+
38
+ static async recoveryCode(recoveryCode: string, emailTo:string){
39
+
40
+
41
+ let emailLayout = EmailLayoutServiceFactory.instance
42
+
43
+ let baseurl = DraxConfig.getOrLoad(CommonConfig.BaseUrl)
44
+
45
+ let body = `
46
+ <h2 style="font-size: 22px; color: #333333; font-weight: 600; margin: 0 0 10px 0;">
47
+ Recuperación de Contraseña
48
+ </h2>
49
+ <p style="font-size: 16px; line-height: 1.6; color: #555555; margin: 0 0 15px 0;">
50
+ Accede al siguiente link para recuperar tu contraseña:
51
+ </p>
52
+ <p style="text-align: center; width: 100%;">
53
+ <a href="${baseurl}/password/recovery/complete/${recoveryCode}"
54
+ style="color: #333333; text-decoration: none; border: 1px solid #333333; padding: 10px 20px; text-align: center; text-decoration: none; display: inline-block;">
55
+ Recuperar Contraseña
56
+ </a>
57
+ </p>
58
+
59
+ `
60
+
61
+ const emailFrom = DraxConfig.getOrLoad(EmailTransportConfig.authUsername)
62
+
63
+ const emailOptions : SendMailOptions = {
64
+ subject: "Recuperación de Contraseña",
65
+ from: emailFrom,
66
+ to: emailTo,
67
+ html: emailLayout.html(body)
68
+ }
69
+
70
+ await EmailTransportServiceFactory.instance.sendEmail(emailOptions)
71
+
72
+ }
73
+
74
+ }
75
+
76
+ export default UserEmailService
77
+
78
+ export {UserEmailService}
@@ -1,7 +1,8 @@
1
1
  import type {IUser, IUserCreate, IUserUpdate} from "@drax/identity-share";
2
2
  import type {IUserRepository} from "../interfaces/IUserRepository";
3
+
3
4
  import {ZodError} from "zod";
4
- import {ValidationError, ZodErrorToValidationError} from "@drax/common-back";
5
+ import {SecuritySensitiveError, ValidationError, ZodErrorToValidationError} from "@drax/common-back";
5
6
  import AuthUtils from "../utils/AuthUtils.js";
6
7
  import {createUserSchema, editUserSchema, userBaseSchema} from "../zod/UserZod.js";
7
8
  import BadCredentialsError from "../errors/BadCredentialsError.js";
@@ -9,12 +10,12 @@ import {IDraxPaginateOptions, IDraxPaginateResult} from "@drax/crud-share";
9
10
  import {AbstractService} from "@drax/crud-back";
10
11
  import {randomUUID} from "crypto"
11
12
 
12
- class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
13
+ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
13
14
 
14
15
  _repository: IUserRepository
15
16
 
16
17
  constructor(userRepository: IUserRepository) {
17
- super(userRepository,userBaseSchema);
18
+ super(userRepository, userBaseSchema);
18
19
  this._repository = userRepository;
19
20
  console.log("UserService constructor")
20
21
  }
@@ -22,7 +23,7 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
22
23
  async auth(username: string, password: string) {
23
24
  let user = null
24
25
  console.log("auth username", username)
25
- user = await this.findByUsername(username)
26
+ user = await this.findByUsernameWithPassword(username)
26
27
  if (user && user.active && AuthUtils.checkPassword(password, user.password)) {
27
28
  //TODO: Generar session
28
29
  const session = randomUUID()
@@ -38,7 +39,7 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
38
39
  console.log("auth email", email)
39
40
  user = await this.findByEmail(email)
40
41
 
41
- if(!user && createIfNotFound){
42
+ if (!user && createIfNotFound) {
42
43
  userData.password = userData.password ? userData.password : randomUUID()
43
44
  userData.active = userData.active === undefined ? true : userData.active
44
45
  user = await this.create(userData)
@@ -49,10 +50,11 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
49
50
  const accessToken = AuthUtils.generateToken(user.id.toString(), user.username, user.role.id, user.tenant?.id, session)
50
51
  return {accessToken: accessToken}
51
52
  } else {
52
- throw new BadCredentialsError()
53
+ throw new BadCredentialsError()
53
54
  }
54
55
  }
55
56
 
57
+
56
58
  async changeUserPassword(userId: string, newPassword: string) {
57
59
  const user = await this.findById(userId)
58
60
  if (user) {
@@ -68,7 +70,6 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
68
70
  async changeOwnPassword(userId: string, currentPassword: string, newPassword: string) {
69
71
  const user = await this.findById(userId)
70
72
 
71
-
72
73
  if (user && user.active) {
73
74
 
74
75
  if (currentPassword === newPassword) {
@@ -98,12 +99,95 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
98
99
  }
99
100
  }
100
101
 
102
+ async recoveryCode(email: string): Promise<string> {
103
+ try{
104
+ const recoveryCode = randomUUID()
105
+ const user = await this._repository.findByEmail(email)
106
+ if(user && user.active){
107
+ await this._repository.updatePartial(user.id, {recoveryCode: recoveryCode})
108
+ return recoveryCode
109
+ }else{
110
+ throw new SecuritySensitiveError()
111
+ }
112
+ }catch (e) {
113
+ console.error("recoveryCode:", e)
114
+ throw e
115
+ }
116
+ }
117
+
118
+ async changeUserPasswordByCode(recoveryCode: string, newPassword: string): Promise<boolean> {
119
+ try {
120
+ console.log("changeUserPasswordByCode recoveryCode", recoveryCode)
121
+ const user = await this._repository.findByRecoveryCode(recoveryCode)
122
+ console.log("changeUserPasswordByCode user", user)
123
+ if (user && user.active) {
124
+ newPassword = AuthUtils.hashPassword(newPassword)
125
+ await this._repository.changePassword(user.id, newPassword)
126
+ await this._repository.updatePartial(user.id, {recoveryCode: null})
127
+ return true
128
+ } else {
129
+ throw new ValidationError([{field:'recoveryCode', reason: 'validation.notFound'}])
130
+ }
131
+ } catch (e) {
132
+ console.error("changeUserPasswordByCode", e)
133
+ throw e
134
+ }
135
+ }
136
+
137
+ async register(userData: IUserCreate): Promise<IUser> {
138
+ try {
139
+
140
+ userData.emailVerified = false
141
+ userData.phoneVerified = false
142
+ userData.active = false
143
+
144
+ userData.emailCode = randomUUID()
145
+ userData.phoneCode = randomUUID()
146
+
147
+
148
+ let user = await this.create(userData)
149
+
150
+ return user
151
+ } catch (e) {
152
+ console.error("Error registry user", e)
153
+ if (e instanceof ZodError) {
154
+ throw ZodErrorToValidationError(e, userData)
155
+ }
156
+ throw e
157
+ }
158
+ }
159
+
160
+ async verifyEmail(emailCode: string): Promise<boolean> {
161
+ const user = await this._repository.findByEmailCode(emailCode)
162
+ if (user && user.emailVerified === false) {
163
+ await this._repository.updatePartial(user.id, {
164
+ emailVerified: true,
165
+ active: true
166
+ })
167
+ return true
168
+ } else {
169
+ throw new ValidationError([{field: 'emailCode', reason: 'validation.notFound'}])
170
+ }
171
+ }
172
+
173
+ async verifyPhone(phoneCode: string): Promise<boolean> {
174
+ const user = await this._repository.findByPhoneCode(phoneCode)
175
+ if (user && user.phoneVerified === false) {
176
+ await this._repository.updatePartial(user.id, {
177
+ phoneVerified: true,
178
+ active: true
179
+ })
180
+ return true
181
+ } else {
182
+ throw new ValidationError([{field: 'phoneCode', reason: 'validation.notFound'}])
183
+ }
184
+ }
101
185
 
102
186
  async create(userData: IUserCreate): Promise<IUser> {
103
187
  try {
104
188
  userData.name = userData?.name?.trim()
105
- userData.username = userData.username.trim()
106
- userData.password = userData.password.trim()
189
+ userData.username = userData?.username.trim()
190
+ userData.password = userData?.password.trim()
107
191
  userData.tenant = userData.tenant === "" ? null : userData.tenant
108
192
 
109
193
  await createUserSchema.parseAsync(userData)
@@ -123,8 +207,8 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
123
207
 
124
208
  async update(id: string, userData: IUserUpdate) {
125
209
  try {
126
- userData.name = userData.name.trim()
127
- userData.username = userData.username.trim()
210
+ userData.name = userData?.name.trim()
211
+ userData.username = userData?.username.trim()
128
212
  delete userData.password
129
213
  userData.tenant = userData.tenant === "" ? null : userData.tenant
130
214
 
@@ -145,7 +229,7 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
145
229
  async delete(id: string): Promise<boolean> {
146
230
  try {
147
231
  const result: boolean = await this._repository.delete(id);
148
- if(!result){
232
+ if (!result) {
149
233
  throw new Error("error.deletionFailed")
150
234
  }
151
235
  return result;
@@ -178,6 +262,17 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate>{
178
262
 
179
263
  }
180
264
 
265
+ async findByUsernameWithPassword(username: string): Promise<IUser | null> {
266
+ try {
267
+ const user: IUser = await this._repository.findByUsernameWithPassword(username);
268
+ return user
269
+ } catch (e) {
270
+ console.error("Error finding user by username", e)
271
+ throw e
272
+ }
273
+
274
+ }
275
+
181
276
  async findByEmail(email: string): Promise<IUser | null> {
182
277
  try {
183
278
  const user: IUser = await this._repository.findByEmail(email);