@drax/identity-back 0.9.0 → 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.
- package/dist/config/IdentityConfig.js +1 -0
- package/dist/controllers/UserController.js +152 -5
- package/dist/factory/UserRegistryServiceFactory.js +24 -0
- package/dist/html/RegistrationCompleteHtml.js +51 -0
- package/dist/models/UserModel.js +6 -1
- package/dist/repository/mongo/UserApiKeyMongoRepository.js +2 -2
- package/dist/repository/mongo/UserMongoRepository.js +28 -0
- package/dist/repository/sqlite/UserSqliteRepository.js +44 -1
- package/dist/routes/UserRoutes.js +11 -6
- package/dist/services/UserEmailService.js +54 -0
- package/dist/services/UserService.js +97 -6
- package/package.json +7 -6
- package/src/config/IdentityConfig.ts +2 -0
- package/src/controllers/UserController.ts +190 -21
- package/src/html/RegistrationCompleteHtml.ts +52 -0
- package/src/interfaces/IUserRepository.ts +5 -0
- package/src/models/UserModel.ts +6 -1
- package/src/repository/mongo/UserApiKeyMongoRepository.ts +2 -2
- package/src/repository/mongo/UserMongoRepository.ts +32 -1
- package/src/repository/sqlite/UserSqliteRepository.ts +51 -1
- package/src/routes/UserRoutes.ts +16 -6
- package/src/services/UserEmailService.ts +78 -0
- package/src/services/UserService.ts +107 -12
- package/tsconfig.tsbuildinfo +1 -1
- package/types/config/IdentityConfig.d.ts +2 -1
- package/types/config/IdentityConfig.d.ts.map +1 -1
- package/types/controllers/UserController.d.ts +7 -2
- package/types/controllers/UserController.d.ts.map +1 -1
- package/types/factory/UserApiKeyServiceFactory.d.ts +1 -1
- package/types/factory/UserRegistryServiceFactory.d.ts +4 -0
- package/types/factory/UserRegistryServiceFactory.d.ts.map +1 -0
- package/types/html/RegistrationCompleteHtml.d.ts +3 -0
- package/types/html/RegistrationCompleteHtml.d.ts.map +1 -0
- package/types/interfaces/IUserRepository.d.ts +4 -0
- package/types/interfaces/IUserRepository.d.ts.map +1 -1
- package/types/models/UserModel.d.ts.map +1 -1
- package/types/repository/mongo/UserApiKeyMongoRepository.d.ts +2 -2
- package/types/repository/mongo/UserApiKeyMongoRepository.d.ts.map +1 -1
- package/types/repository/mongo/UserMongoRepository.d.ts +5 -0
- package/types/repository/mongo/UserMongoRepository.d.ts.map +1 -1
- package/types/repository/sqlite/UserApiKeySqliteRepository.d.ts +1 -1
- package/types/repository/sqlite/UserSqliteRepository.d.ts +5 -0
- package/types/repository/sqlite/UserSqliteRepository.d.ts.map +1 -1
- package/types/routes/UserRoutes.d.ts.map +1 -1
- package/types/services/UserEmailService.d.ts +7 -0
- package/types/services/UserEmailService.d.ts.map +1 -0
- package/types/services/UserService.d.ts +6 -0
- package/types/services/UserService.d.ts.map +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
|
-
import { ValidationError, ZodErrorToValidationError } from "@drax/common-back";
|
|
2
|
+
import { SecuritySensitiveError, ValidationError, ZodErrorToValidationError } from "@drax/common-back";
|
|
3
3
|
import AuthUtils from "../utils/AuthUtils.js";
|
|
4
4
|
import { createUserSchema, editUserSchema, userBaseSchema } from "../zod/UserZod.js";
|
|
5
5
|
import BadCredentialsError from "../errors/BadCredentialsError.js";
|
|
@@ -14,7 +14,7 @@ class UserService extends AbstractService {
|
|
|
14
14
|
async auth(username, password) {
|
|
15
15
|
let user = null;
|
|
16
16
|
console.log("auth username", username);
|
|
17
|
-
user = await this.
|
|
17
|
+
user = await this.findByUsernameWithPassword(username);
|
|
18
18
|
if (user && user.active && AuthUtils.checkPassword(password, user.password)) {
|
|
19
19
|
//TODO: Generar session
|
|
20
20
|
const session = randomUUID();
|
|
@@ -83,11 +83,92 @@ class UserService extends AbstractService {
|
|
|
83
83
|
throw new BadCredentialsError();
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
async recoveryCode(email) {
|
|
87
|
+
try {
|
|
88
|
+
const recoveryCode = randomUUID();
|
|
89
|
+
const user = await this._repository.findByEmail(email);
|
|
90
|
+
if (user && user.active) {
|
|
91
|
+
await this._repository.updatePartial(user.id, { recoveryCode: recoveryCode });
|
|
92
|
+
return recoveryCode;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
throw new SecuritySensitiveError();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.error("recoveryCode:", e);
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async changeUserPasswordByCode(recoveryCode, newPassword) {
|
|
104
|
+
try {
|
|
105
|
+
console.log("changeUserPasswordByCode recoveryCode", recoveryCode);
|
|
106
|
+
const user = await this._repository.findByRecoveryCode(recoveryCode);
|
|
107
|
+
console.log("changeUserPasswordByCode user", user);
|
|
108
|
+
if (user && user.active) {
|
|
109
|
+
newPassword = AuthUtils.hashPassword(newPassword);
|
|
110
|
+
await this._repository.changePassword(user.id, newPassword);
|
|
111
|
+
await this._repository.updatePartial(user.id, { recoveryCode: null });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
throw new ValidationError([{ field: 'recoveryCode', reason: 'validation.notFound' }]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
console.error("changeUserPasswordByCode", e);
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async register(userData) {
|
|
124
|
+
try {
|
|
125
|
+
userData.emailVerified = false;
|
|
126
|
+
userData.phoneVerified = false;
|
|
127
|
+
userData.active = false;
|
|
128
|
+
userData.emailCode = randomUUID();
|
|
129
|
+
userData.phoneCode = randomUUID();
|
|
130
|
+
let user = await this.create(userData);
|
|
131
|
+
return user;
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
console.error("Error registry user", e);
|
|
135
|
+
if (e instanceof ZodError) {
|
|
136
|
+
throw ZodErrorToValidationError(e, userData);
|
|
137
|
+
}
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async verifyEmail(emailCode) {
|
|
142
|
+
const user = await this._repository.findByEmailCode(emailCode);
|
|
143
|
+
if (user && user.emailVerified === false) {
|
|
144
|
+
await this._repository.updatePartial(user.id, {
|
|
145
|
+
emailVerified: true,
|
|
146
|
+
active: true
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
throw new ValidationError([{ field: 'emailCode', reason: 'validation.notFound' }]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async verifyPhone(phoneCode) {
|
|
155
|
+
const user = await this._repository.findByPhoneCode(phoneCode);
|
|
156
|
+
if (user && user.phoneVerified === false) {
|
|
157
|
+
await this._repository.updatePartial(user.id, {
|
|
158
|
+
phoneVerified: true,
|
|
159
|
+
active: true
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
throw new ValidationError([{ field: 'phoneCode', reason: 'validation.notFound' }]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
86
167
|
async create(userData) {
|
|
87
168
|
try {
|
|
88
169
|
userData.name = userData?.name?.trim();
|
|
89
|
-
userData.username = userData
|
|
90
|
-
userData.password = userData
|
|
170
|
+
userData.username = userData?.username.trim();
|
|
171
|
+
userData.password = userData?.password.trim();
|
|
91
172
|
userData.tenant = userData.tenant === "" ? null : userData.tenant;
|
|
92
173
|
await createUserSchema.parseAsync(userData);
|
|
93
174
|
userData.password = AuthUtils.hashPassword(userData.password.trim());
|
|
@@ -104,8 +185,8 @@ class UserService extends AbstractService {
|
|
|
104
185
|
}
|
|
105
186
|
async update(id, userData) {
|
|
106
187
|
try {
|
|
107
|
-
userData.name = userData
|
|
108
|
-
userData.username = userData
|
|
188
|
+
userData.name = userData?.name.trim();
|
|
189
|
+
userData.username = userData?.username.trim();
|
|
109
190
|
delete userData.password;
|
|
110
191
|
userData.tenant = userData.tenant === "" ? null : userData.tenant;
|
|
111
192
|
await editUserSchema.parseAsync(userData);
|
|
@@ -153,6 +234,16 @@ class UserService extends AbstractService {
|
|
|
153
234
|
throw e;
|
|
154
235
|
}
|
|
155
236
|
}
|
|
237
|
+
async findByUsernameWithPassword(username) {
|
|
238
|
+
try {
|
|
239
|
+
const user = await this._repository.findByUsernameWithPassword(username);
|
|
240
|
+
return user;
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
console.error("Error finding user by username", e);
|
|
244
|
+
throw e;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
156
247
|
async findByEmail(email) {
|
|
157
248
|
try {
|
|
158
249
|
const user = await this._repository.findByEmail(email);
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.10.0",
|
|
7
7
|
"description": "Identity module for user management, authentication and authorization.",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
"author": "Cristian Incarnato & Drax Team",
|
|
29
29
|
"license": "ISC",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@drax/common-back": "^0.
|
|
32
|
-
"@drax/crud-back": "^0.
|
|
33
|
-
"@drax/crud-share": "^0.
|
|
34
|
-
"@drax/
|
|
31
|
+
"@drax/common-back": "^0.10.0",
|
|
32
|
+
"@drax/crud-back": "^0.10.0",
|
|
33
|
+
"@drax/crud-share": "^0.10.0",
|
|
34
|
+
"@drax/email-back": "^0.10.0",
|
|
35
|
+
"@drax/identity-share": "^0.10.0",
|
|
35
36
|
"bcryptjs": "^2.4.3",
|
|
36
37
|
"express-jwt": "^8.4.1",
|
|
37
38
|
"graphql": "^16.8.2",
|
|
@@ -62,5 +63,5 @@
|
|
|
62
63
|
"debug": "0"
|
|
63
64
|
}
|
|
64
65
|
},
|
|
65
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "bc1b8a35563ccffdf7719cfcc588f12df0e012bb"
|
|
66
67
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type {IUser, IUserUpdate, IUserCreate} from "@drax/identity-share";
|
|
2
2
|
import {AbstractFastifyController} from "@drax/crud-back";
|
|
3
|
-
import
|
|
3
|
+
import RegistrationCompleteHtml from "../html/RegistrationCompleteHtml.js";
|
|
4
|
+
import {
|
|
5
|
+
CommonConfig,
|
|
6
|
+
DraxConfig,
|
|
7
|
+
StoreManager,
|
|
8
|
+
UploadFileError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
UnauthorizedError,
|
|
11
|
+
SecuritySensitiveError
|
|
12
|
+
} from "@drax/common-back";
|
|
4
13
|
|
|
5
14
|
import UserServiceFactory from "../factory/UserServiceFactory.js";
|
|
6
15
|
import RoleServiceFactory from "../factory/RoleServiceFactory.js";
|
|
@@ -9,13 +18,14 @@ import UserPermissions from "../permissions/UserPermissions.js";
|
|
|
9
18
|
import BadCredentialsError from "../errors/BadCredentialsError.js";
|
|
10
19
|
import {join} from "path";
|
|
11
20
|
import {IdentityConfig} from "../config/IdentityConfig.js";
|
|
21
|
+
import UserEmailService from "../services/UserEmailService.js";
|
|
12
22
|
|
|
13
23
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
14
24
|
const AVATAR_DIR = DraxConfig.getOrLoad(IdentityConfig.AvatarDir) || 'avatar';
|
|
15
25
|
const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : ''
|
|
16
26
|
|
|
17
27
|
|
|
18
|
-
class UserController extends AbstractFastifyController<IUser, IUserCreate, IUserUpdate>
|
|
28
|
+
class UserController extends AbstractFastifyController<IUser, IUserCreate, IUserUpdate> {
|
|
19
29
|
|
|
20
30
|
protected service: UserService
|
|
21
31
|
|
|
@@ -76,11 +86,11 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
76
86
|
const search = request.query.search
|
|
77
87
|
const userService = UserServiceFactory()
|
|
78
88
|
const filters = []
|
|
79
|
-
if(request.rbac.getAuthUser.tenantId){
|
|
89
|
+
if (request.rbac.getAuthUser.tenantId) {
|
|
80
90
|
filters.push({field: 'tenant', operator: 'eq', value: request.rbac.getAuthUser.tenantId})
|
|
81
91
|
}
|
|
82
92
|
let paginateResult = await userService.paginate({page, limit, orderBy, order, search, filters})
|
|
83
|
-
for(let item of paginateResult.items){
|
|
93
|
+
for (let item of paginateResult.items) {
|
|
84
94
|
item.password = undefined
|
|
85
95
|
delete item.password
|
|
86
96
|
}
|
|
@@ -104,11 +114,11 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
104
114
|
try {
|
|
105
115
|
request.rbac.assertPermission(UserPermissions.View)
|
|
106
116
|
const filters = []
|
|
107
|
-
if(request.rbac.getAuthUser.tenantId){
|
|
117
|
+
if (request.rbac.getAuthUser.tenantId) {
|
|
108
118
|
filters.push({field: 'tenant', operator: 'eq', value: request.rbac.getAuthUser.tenantId})
|
|
109
119
|
}
|
|
110
120
|
const search = request.query.search
|
|
111
|
-
let item = await this.service.search(search,1000,filters)
|
|
121
|
+
let item = await this.service.search(search, 1000, filters)
|
|
112
122
|
return item
|
|
113
123
|
} catch (e) {
|
|
114
124
|
console.error(e)
|
|
@@ -125,6 +135,90 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
async register(request, reply) {
|
|
139
|
+
try {
|
|
140
|
+
if (request.rbac.getAuthUser) {
|
|
141
|
+
throw new UnauthorizedError()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const payload = request.body
|
|
145
|
+
|
|
146
|
+
const defaultRole = DraxConfig.getOrLoad(IdentityConfig.defaultRole)
|
|
147
|
+
|
|
148
|
+
if (!defaultRole) {
|
|
149
|
+
throw new ValidationError([{field: 'username', reason: 'Default role not found'}])
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const roleService = RoleServiceFactory()
|
|
153
|
+
const role = await roleService.findByName(defaultRole)
|
|
154
|
+
|
|
155
|
+
if (!role) {
|
|
156
|
+
throw new ValidationError([{field: 'role', reason: 'Role not found'}])
|
|
157
|
+
} else if (role.name === 'Admin') {
|
|
158
|
+
payload.tenant = null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
payload.role = role.id
|
|
162
|
+
payload.origin ??= 'Registry'
|
|
163
|
+
|
|
164
|
+
const userService = UserServiceFactory()
|
|
165
|
+
let user = await userService.register(payload)
|
|
166
|
+
|
|
167
|
+
//SEND EMAIL FOR EMAIL VERIFICATION
|
|
168
|
+
await UserEmailService.emailVerifyCode(user.emailCode, user.email)
|
|
169
|
+
|
|
170
|
+
return user
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.error(e)
|
|
173
|
+
if (e instanceof ValidationError) {
|
|
174
|
+
reply.statusCode = e.statusCode
|
|
175
|
+
reply.send({error: e.message, inputErrors: e.errors})
|
|
176
|
+
} else if (e instanceof UnauthorizedError) {
|
|
177
|
+
reply.statusCode = e.statusCode
|
|
178
|
+
reply.send({error: e.message})
|
|
179
|
+
} else {
|
|
180
|
+
reply.statusCode = 500
|
|
181
|
+
reply.send({error: 'error.server'})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async verifyEmail(request, reply) {
|
|
187
|
+
try {
|
|
188
|
+
const emailCode = request.params.code
|
|
189
|
+
const userService = UserServiceFactory()
|
|
190
|
+
const r = await userService.verifyEmail(emailCode)
|
|
191
|
+
if(r){
|
|
192
|
+
const html = RegistrationCompleteHtml
|
|
193
|
+
reply.header('Content-Type', 'text/html; charset=utf-8').send(html)
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.error(e)
|
|
197
|
+
if (e instanceof ValidationError) {
|
|
198
|
+
reply.statusCode = e.statusCode
|
|
199
|
+
reply.send({error: e.message, inputErrors: e.errors})
|
|
200
|
+
}
|
|
201
|
+
reply.code(500)
|
|
202
|
+
reply.send({error: 'error.server'})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async verifyPhone(request, reply) {
|
|
207
|
+
try {
|
|
208
|
+
const phoneCode = request.params.code
|
|
209
|
+
const userService = UserServiceFactory()
|
|
210
|
+
return await userService.verifyPhone(phoneCode)
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.error(e)
|
|
213
|
+
if (e instanceof ValidationError) {
|
|
214
|
+
reply.statusCode = e.statusCode
|
|
215
|
+
reply.send({error: e.message, inputErrors: e.errors})
|
|
216
|
+
}
|
|
217
|
+
reply.code(500)
|
|
218
|
+
reply.send({error: 'error.server'})
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
128
222
|
async create(request, reply) {
|
|
129
223
|
try {
|
|
130
224
|
request.rbac.assertPermission(UserPermissions.Create)
|
|
@@ -132,11 +226,11 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
132
226
|
|
|
133
227
|
const roleService = RoleServiceFactory()
|
|
134
228
|
const role = await roleService.findById(payload.role)
|
|
135
|
-
if(!role){
|
|
229
|
+
if (!role) {
|
|
136
230
|
throw new ValidationError([{field: 'role', reason: 'Role not found'}])
|
|
137
|
-
}else if(role.name === 'Admin'){
|
|
231
|
+
} else if (role.name === 'Admin') {
|
|
138
232
|
payload.tenant = null
|
|
139
|
-
}else if(request.rbac.getAuthUser.tenantId){
|
|
233
|
+
} else if (request.rbac.getAuthUser.tenantId) {
|
|
140
234
|
payload.tenant = request.rbac.getAuthUser.tenantId
|
|
141
235
|
}
|
|
142
236
|
|
|
@@ -146,6 +240,7 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
146
240
|
let user = await userService.create(payload)
|
|
147
241
|
return user
|
|
148
242
|
} catch (e) {
|
|
243
|
+
console.error(e)
|
|
149
244
|
if (e instanceof ValidationError) {
|
|
150
245
|
reply.statusCode = e.statusCode
|
|
151
246
|
reply.send({error: e.message, inputErrors: e.errors})
|
|
@@ -168,11 +263,11 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
168
263
|
|
|
169
264
|
const roleService = RoleServiceFactory()
|
|
170
265
|
const role = await roleService.findById(payload.role)
|
|
171
|
-
if(!role){
|
|
266
|
+
if (!role) {
|
|
172
267
|
throw new ValidationError([{field: 'role', reason: 'Role not found'}])
|
|
173
|
-
}else if(role.name === 'Admin'){
|
|
268
|
+
} else if (role.name === 'Admin') {
|
|
174
269
|
payload.tenant = null
|
|
175
|
-
}else if(request.rbac.getAuthUser.tenantId){
|
|
270
|
+
} else if (request.rbac.getAuthUser.tenantId) {
|
|
176
271
|
payload.tenant = request.rbac.getAuthUser.tenantId
|
|
177
272
|
}
|
|
178
273
|
|
|
@@ -180,6 +275,7 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
180
275
|
let user = await userService.update(id, payload)
|
|
181
276
|
return user
|
|
182
277
|
} catch (e) {
|
|
278
|
+
console.error(e)
|
|
183
279
|
if (e instanceof ValidationError) {
|
|
184
280
|
reply.statusCode = e.statusCode
|
|
185
281
|
reply.send({error: e.message, inputErrors: e.errors})
|
|
@@ -202,13 +298,14 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
202
298
|
request.rbac.assertPermission(UserPermissions.Delete)
|
|
203
299
|
const id = request.params.id
|
|
204
300
|
const userService = UserServiceFactory()
|
|
205
|
-
let r
|
|
206
|
-
if(r){
|
|
301
|
+
let r: boolean = await userService.delete(id)
|
|
302
|
+
if (r) {
|
|
207
303
|
reply.send({message: 'Deleted successfully'})
|
|
208
|
-
}else{
|
|
304
|
+
} else {
|
|
209
305
|
reply.statusCode(400).send({message: 'Not deleted'})
|
|
210
306
|
}
|
|
211
307
|
} catch (e) {
|
|
308
|
+
console.error(e)
|
|
212
309
|
if (e instanceof ValidationError) {
|
|
213
310
|
reply.statusCode = e.statusCode
|
|
214
311
|
reply.send({error: e.message, inputErrors: e.errors})
|
|
@@ -222,9 +319,81 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
222
319
|
}
|
|
223
320
|
}
|
|
224
321
|
|
|
225
|
-
async
|
|
322
|
+
async passwordRecoveryRequest(request, reply) {
|
|
323
|
+
//let message = 'Si el correo electrónico existe, se han enviado instrucciones.'
|
|
324
|
+
let message = 'user.events.recoveryPasswordInfo'
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
328
|
+
const email = request.body.email
|
|
329
|
+
|
|
330
|
+
if(!email || !emailRegex.test(email)){
|
|
331
|
+
throw new ValidationError([{field: 'email', reason: 'validation.email.invalid'}])
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const userService = UserServiceFactory()
|
|
335
|
+
const code = await userService.recoveryCode(email)
|
|
336
|
+
|
|
337
|
+
if (code) {
|
|
338
|
+
await UserEmailService.recoveryCode(code, email)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
reply.send({message})
|
|
342
|
+
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error('recoveryPassword error', e)
|
|
345
|
+
if (e instanceof ValidationError) {
|
|
346
|
+
reply.statusCode = e.statusCode
|
|
347
|
+
reply.send({error: e.message, inputErrors: e.errors})
|
|
348
|
+
}else if (e instanceof SecuritySensitiveError) {
|
|
349
|
+
reply.statusCode = e.statusCode
|
|
350
|
+
reply.send({message})
|
|
351
|
+
} else {
|
|
352
|
+
reply.statusCode = 500
|
|
353
|
+
reply.send({error: 'error.server'})
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async recoveryPasswordComplete(request, reply) {
|
|
359
|
+
|
|
226
360
|
try {
|
|
227
|
-
|
|
361
|
+
const recoveryCode = request.body.recoveryCode
|
|
362
|
+
const newPassword = request.body.newPassword
|
|
363
|
+
|
|
364
|
+
if(!recoveryCode){
|
|
365
|
+
throw new ValidationError([{field:'recoveryCode', reason: 'validation.required'}])
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if(!newPassword){
|
|
369
|
+
throw new ValidationError([{field:'newPassword', reason: 'validation.required'}])
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const userService = UserServiceFactory()
|
|
373
|
+
const result: boolean = await userService.changeUserPasswordByCode(recoveryCode, newPassword)
|
|
374
|
+
if(result){
|
|
375
|
+
reply.send({message: 'action.success'})
|
|
376
|
+
}else{
|
|
377
|
+
reply.statusCode = 400
|
|
378
|
+
reply.send({message: 'action.failure'})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.error('recoveryPassword error', e)
|
|
383
|
+
if (e instanceof ValidationError) {
|
|
384
|
+
reply.statusCode = e.statusCode
|
|
385
|
+
reply.send({error: e.message, inputErrors: e.errors})
|
|
386
|
+
} else {
|
|
387
|
+
reply.statusCode = 500
|
|
388
|
+
reply.send({error: 'error.server'})
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
async changeMyPassword(request, reply) {
|
|
395
|
+
try {
|
|
396
|
+
if (!request.authUser) {
|
|
228
397
|
throw new UnauthorizedError()
|
|
229
398
|
}
|
|
230
399
|
const userId = request.authUser.id
|
|
@@ -233,7 +402,7 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
233
402
|
const userService = UserServiceFactory()
|
|
234
403
|
return await userService.changeOwnPassword(userId, currentPassword, newPassword)
|
|
235
404
|
} catch (e) {
|
|
236
|
-
console.error('
|
|
405
|
+
console.error('changeMyPassword error', e)
|
|
237
406
|
if (e instanceof ValidationError) {
|
|
238
407
|
reply.statusCode = e.statusCode
|
|
239
408
|
reply.send({error: e.message, inputErrors: e.errors})
|
|
@@ -247,11 +416,11 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
247
416
|
}
|
|
248
417
|
}
|
|
249
418
|
|
|
250
|
-
async
|
|
419
|
+
async changePassword(request, reply) {
|
|
251
420
|
try {
|
|
252
421
|
request.rbac.assertPermission(UserPermissions.Update)
|
|
253
422
|
const userId = request.params.id
|
|
254
|
-
if(!userId){
|
|
423
|
+
if (!userId) {
|
|
255
424
|
throw new UnauthorizedError()
|
|
256
425
|
}
|
|
257
426
|
const newPassword = request.body.newPassword
|
|
@@ -288,7 +457,7 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
288
457
|
|
|
289
458
|
const destinationPath = join(BASE_FILE_DIR, AVATAR_DIR)
|
|
290
459
|
const storedFile = await StoreManager.saveFile(file, destinationPath)
|
|
291
|
-
const urlFile = BASE_URL + '/api/
|
|
460
|
+
const urlFile = BASE_URL + '/api/users/avatar/' + storedFile.filename
|
|
292
461
|
|
|
293
462
|
//Save into DB
|
|
294
463
|
const userService = UserServiceFactory()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const html = `
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Registro Completo</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
height: 100vh;
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
background-color: #f5f5f5;
|
|
16
|
+
font-family: Arial, sans-serif;
|
|
17
|
+
}
|
|
18
|
+
.container {
|
|
19
|
+
width: 300px;
|
|
20
|
+
border: 1px solid grey;
|
|
21
|
+
padding: 20px;
|
|
22
|
+
text-align: center;
|
|
23
|
+
background: white;
|
|
24
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
25
|
+
border-radius: 8px;
|
|
26
|
+
}
|
|
27
|
+
.title {
|
|
28
|
+
color: darkgreen;
|
|
29
|
+
font-size: 20px;
|
|
30
|
+
font-weight: bold;
|
|
31
|
+
margin-bottom: 20px;
|
|
32
|
+
}
|
|
33
|
+
.link {
|
|
34
|
+
margin-top: 20px;
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
color: #007bff;
|
|
37
|
+
}
|
|
38
|
+
.link:hover {
|
|
39
|
+
text-decoration: underline;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<div class="container">
|
|
45
|
+
<div class="title">Registro Completo</div>
|
|
46
|
+
<a href="/login" class="link">Login</a>
|
|
47
|
+
</div>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
export default html
|
|
@@ -4,9 +4,14 @@ import {IDraxCrud, IDraxFieldFilter} from "@drax/crud-share";
|
|
|
4
4
|
interface IUserRepository extends IDraxCrud<IUser, IUserCreate, IUserUpdate>{
|
|
5
5
|
findById(id: string): Promise<IUser | null>;
|
|
6
6
|
findByUsername(username: string): Promise<IUser | null>;
|
|
7
|
+
findByUsernameWithPassword(username: string): Promise<IUser | null>;
|
|
7
8
|
findByEmail(email: string): Promise<IUser | null>;
|
|
8
9
|
changePassword(id: string, password:string):Promise<Boolean>;
|
|
9
10
|
changeAvatar(id: string, avatarUrl: string): Promise<Boolean>;
|
|
11
|
+
|
|
12
|
+
findByEmailCode(code: string): Promise<IUser | null>;
|
|
13
|
+
findByPhoneCode(code: string): Promise<IUser | null>;
|
|
14
|
+
findByRecoveryCode(code: string): Promise<IUser | null>;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export {IUserRepository}
|
package/src/models/UserModel.ts
CHANGED
|
@@ -33,7 +33,7 @@ const UserSchema = new mongoose.Schema<IUser>({
|
|
|
33
33
|
message: "Invalid email format"
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
|
-
password: {type: String, required: true, index: false},
|
|
36
|
+
password: {type: String, required: true, index: false, select: false},
|
|
37
37
|
name: {type: String, required: false, index: false},
|
|
38
38
|
active: {type: Boolean, required: true, default: false, index: false},
|
|
39
39
|
phone: {
|
|
@@ -69,6 +69,11 @@ const UserSchema = new mongoose.Schema<IUser>({
|
|
|
69
69
|
required: false,
|
|
70
70
|
index: false
|
|
71
71
|
}],
|
|
72
|
+
emailVerified: {type: Boolean, required: true, default: false, index: false},
|
|
73
|
+
phoneVerified: {type: Boolean, required: true, default: false, index: false},
|
|
74
|
+
emailCode: {type: String, required: false, index: false, select: false},
|
|
75
|
+
phoneCode: {type: String, required: false, index: false, select: false},
|
|
76
|
+
recoveryCode: {type: String, required: false, index: false, select: false},
|
|
72
77
|
}, {timestamps: true});
|
|
73
78
|
|
|
74
79
|
UserSchema.set('toJSON', {getters: true});
|
|
@@ -11,7 +11,7 @@ import {PaginateResult} from "mongoose";
|
|
|
11
11
|
import {IDraxPaginateOptions, IDraxPaginateResult} from "@drax/crud-share";
|
|
12
12
|
import {IUserApiKeyRepository} from "../../interfaces/IUserApiKeyRepository";
|
|
13
13
|
|
|
14
|
-
class
|
|
14
|
+
class UserApiKeyMongoRepository implements IUserApiKeyRepository {
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
constructor() {
|
|
@@ -101,4 +101,4 @@ class UserMongoRepository implements IUserApiKeyRepository {
|
|
|
101
101
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export default
|
|
104
|
+
export default UserApiKeyMongoRepository
|
|
@@ -12,7 +12,6 @@ import type {IUserRepository} from "../../interfaces/IUserRepository";
|
|
|
12
12
|
import {Cursor, PaginateResult} from "mongoose";
|
|
13
13
|
import RoleMongoRepository from "./RoleMongoRepository.js";
|
|
14
14
|
import {IDraxFindOptions, IDraxPaginateOptions, IDraxPaginateResult} from "@drax/crud-share";
|
|
15
|
-
import {IRole, ITenant} from "@drax/identity-share";
|
|
16
15
|
import type {IDraxFieldFilter} from "@drax/crud-share/dist";
|
|
17
16
|
|
|
18
17
|
class UserMongoRepository implements IUserRepository {
|
|
@@ -58,6 +57,18 @@ class UserMongoRepository implements IUserRepository {
|
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
async updatePartial(id: string, data: any): Promise<IUser> {
|
|
61
|
+
try {
|
|
62
|
+
const item: mongoose.HydratedDocument<IUser> = await UserModel.findOneAndUpdate({_id: id}, data, {new: true}).populate(['role','tenant']).exec()
|
|
63
|
+
return item
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e instanceof mongoose.Error.ValidationError) {
|
|
66
|
+
throw MongooseErrorToValidationError(e)
|
|
67
|
+
}
|
|
68
|
+
throw e
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
async delete(id: string): Promise<boolean> {
|
|
62
73
|
const result: DeleteResult = await UserModel.deleteOne({_id: id}).exec()
|
|
63
74
|
return result.deletedCount == 1
|
|
@@ -74,11 +85,31 @@ class UserMongoRepository implements IUserRepository {
|
|
|
74
85
|
return user
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
async findByUsernameWithPassword(username: string): Promise<IUser> {
|
|
89
|
+
const user: mongoose.HydratedDocument<IUser> = await UserModel.findOne({username: username}).select('+password').populate(['role','tenant']).exec()
|
|
90
|
+
return user
|
|
91
|
+
}
|
|
92
|
+
|
|
77
93
|
async findByEmail(email: string): Promise<IUser> {
|
|
78
94
|
const user: mongoose.HydratedDocument<IUser> = await UserModel.findOne({email: email}).populate(['role','tenant']).exec()
|
|
79
95
|
return user
|
|
80
96
|
}
|
|
81
97
|
|
|
98
|
+
async findByEmailCode(code: string): Promise<IUser> {
|
|
99
|
+
const user: mongoose.HydratedDocument<IUser> = await UserModel.findOne({emailCode: {$eq: code}, emailVerified: {$eq: false} }).populate(['role','tenant']).exec()
|
|
100
|
+
return user
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async findByPhoneCode(code: string): Promise<IUser> {
|
|
104
|
+
const user: mongoose.HydratedDocument<IUser> = await UserModel.findOne({phoneCode: {$eq: code}, phoneVerified: {$eq: false} }).populate(['role','tenant']).exec()
|
|
105
|
+
return user
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async findByRecoveryCode(code: string): Promise<IUser> {
|
|
109
|
+
const user: mongoose.HydratedDocument<IUser> = await UserModel.findOne({recoveryCode: {$eq: code}, active: {$eq: true} }).populate(['role','tenant']).exec()
|
|
110
|
+
return user
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
async paginate({
|
|
83
114
|
page= 1,
|
|
84
115
|
limit= 5,
|