@hed-hog/core 0.0.96 → 0.0.99
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/auth/auth.controller.d.ts +24 -11
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +43 -9
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +34 -22
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +256 -13
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/dto/login-email-verification-resend.dto.d.ts +4 -0
- package/dist/auth/dto/login-email-verification-resend.dto.d.ts.map +1 -0
- package/dist/auth/dto/login-email-verification-resend.dto.js +22 -0
- package/dist/auth/dto/login-email-verification-resend.dto.js.map +1 -0
- package/dist/auth/dto/login-email-verification.dto.d.ts +5 -0
- package/dist/auth/dto/login-email-verification.dto.d.ts.map +1 -0
- package/dist/auth/dto/login-email-verification.dto.js +28 -0
- package/dist/auth/dto/login-email-verification.dto.js.map +1 -0
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +1 -0
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +2 -2
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
- package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
- package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
- package/dist/mail/mail.controller.d.ts +2 -2
- package/dist/mail/mail.service.d.ts +2 -2
- package/dist/menu/menu.controller.d.ts +6 -6
- package/dist/menu/menu.service.d.ts +6 -6
- package/dist/profile/dto/email-verification-confirm.dto.d.ts +1 -0
- package/dist/profile/dto/email-verification-confirm.dto.d.ts.map +1 -1
- package/dist/profile/dto/email-verification-confirm.dto.js +7 -0
- package/dist/profile/dto/email-verification-confirm.dto.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +2 -4
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js +1 -0
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +3 -5
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +65 -7
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/role/role.controller.d.ts +3 -3
- package/dist/role/role.service.d.ts +3 -3
- package/dist/screen/screen.controller.d.ts +3 -3
- package/dist/screen/screen.service.d.ts +3 -3
- package/dist/security/security.service.d.ts +1 -0
- package/dist/security/security.service.d.ts.map +1 -1
- package/dist/security/security.service.js +5 -0
- package/dist/security/security.service.js.map +1 -1
- package/dist/setting/setting.controller.d.ts +4 -4
- package/dist/setting/setting.service.d.ts +4 -4
- package/dist/token/token.service.d.ts +1 -0
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +15 -1
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +1 -1
- package/dist/user/user.service.d.ts +46 -4
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +11 -3
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/mail.yaml +19 -0
- package/package.json +3 -3
- package/src/auth/auth.controller.ts +30 -10
- package/src/auth/auth.service.ts +329 -21
- package/src/auth/dto/login-email-verification-resend.dto.ts +7 -0
- package/src/auth/dto/login-email-verification.dto.ts +12 -0
- package/src/challenge/challenge.service.ts +4 -0
- package/src/mail/mail.controller.ts +13 -13
- package/src/profile/dto/email-verification-confirm.dto.ts +7 -1
- package/src/profile/profile.controller.ts +1 -0
- package/src/profile/profile.service.ts +75 -6
- package/src/security/security.service.ts +8 -0
- package/src/token/token.service.ts +17 -1
- package/src/user/user.service.ts +13 -5
package/src/auth/auth.service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
forwardRef,
|
|
7
7
|
Inject,
|
|
8
8
|
Injectable,
|
|
9
|
+
NotFoundException,
|
|
9
10
|
} from '@nestjs/common';
|
|
10
11
|
import { ChallengeService } from '../challenge/challenge.service';
|
|
11
12
|
import { MailService as MailManagerService } from '../mail/mail.service';
|
|
@@ -53,17 +54,314 @@ export class AuthService {
|
|
|
53
54
|
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
async requiresMfaForLogin(locale: string, email: string, user: any) {
|
|
58
|
+
|
|
59
|
+
console.log('MFA required, setting up email MFA');
|
|
60
|
+
|
|
61
|
+
const settings = await this.setting.getSettingValues([
|
|
62
|
+
'require-mfa',
|
|
63
|
+
'require-email-verification',
|
|
64
|
+
'mfa-email-code-length',
|
|
65
|
+
'mfa-challenge-expiration-minutes'
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const code = this.security.generateCode(settings['mfa-email-code-length'] || 6);
|
|
69
|
+
const codeHash = this.security.hashWithPepper(code);
|
|
70
|
+
|
|
71
|
+
const identifier = await this.prisma.user_identifier.findFirst({
|
|
72
|
+
where: {
|
|
73
|
+
user_id: user.id,
|
|
74
|
+
type: 'email',
|
|
75
|
+
value: email,
|
|
76
|
+
},
|
|
77
|
+
select: { id: true }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!identifier) {
|
|
81
|
+
throw new NotFoundException(getLocaleText('identifierNotFound', locale, 'Email identifier not found or already verified.'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const challengeIdentifier = await this.prisma.user_identifier_challenge.create({
|
|
85
|
+
data: {
|
|
86
|
+
hash: codeHash,
|
|
87
|
+
expires_at: new Date(Date.now() + (settings['mfa-challenge-expiration-minutes'] || 15) * 60000),
|
|
88
|
+
user_identifier_id: identifier.id,
|
|
89
|
+
},
|
|
90
|
+
select: {
|
|
91
|
+
id: true,
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const mfa = await this.prisma.user_mfa.create({
|
|
96
|
+
data: {
|
|
97
|
+
name: getLocaleText('mfaEmailDefaultName', locale, 'Email MFA'),
|
|
98
|
+
type: 'email',
|
|
99
|
+
user_id: user.id,
|
|
100
|
+
user_mfa_email: {
|
|
101
|
+
create: {
|
|
102
|
+
email,
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
user_mfa_challenge: {
|
|
106
|
+
create: {
|
|
107
|
+
expires_at: new Date(Date.now() + (settings['mfa-challenge-expiration-minutes'] || 15) * 60000),
|
|
108
|
+
hash: codeHash,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
select: {
|
|
113
|
+
user_mfa_challenge: {
|
|
114
|
+
select: { id: true }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const challengeMfaId = mfa.user_mfa_challenge[0].id;
|
|
120
|
+
|
|
121
|
+
await this.mail.sendTemplatedMail(
|
|
122
|
+
locale,
|
|
123
|
+
{
|
|
124
|
+
email,
|
|
125
|
+
slug: 'auth-sign-up-confirm-email',
|
|
126
|
+
variables: {
|
|
127
|
+
code,
|
|
128
|
+
name: user.name,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
requiresMfa: true,
|
|
135
|
+
token: await this.token.createAccessToken({
|
|
136
|
+
challengeIdentifierId: challengeIdentifier.id,
|
|
137
|
+
challengeMfaId,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async requiresEmailVerificationForLogin(locale: string, email: string, user: any) {
|
|
143
|
+
|
|
144
|
+
const settings = await this.setting.getSettingValues([
|
|
145
|
+
'require-mfa',
|
|
146
|
+
'require-email-verification',
|
|
147
|
+
'mfa-email-code-length',
|
|
148
|
+
'mfa-challenge-expiration-minutes'
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const code = this.security.generateCode(settings['mfa-email-code-length'] || 6);
|
|
152
|
+
const codeHash = this.security.hashWithPepper(code);
|
|
153
|
+
|
|
154
|
+
const identifier = await this.prisma.user_identifier.findFirst({
|
|
155
|
+
where: {
|
|
156
|
+
user_id: user.id,
|
|
157
|
+
type: 'email',
|
|
158
|
+
value: email,
|
|
159
|
+
},
|
|
160
|
+
select: { id: true }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!identifier) {
|
|
164
|
+
throw new NotFoundException(getLocaleText('identifierNotFound', locale, 'Email identifier not found or already verified.'));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const challengeIdentifier = await this.prisma.user_identifier_challenge.create({
|
|
168
|
+
data: {
|
|
169
|
+
hash: codeHash,
|
|
170
|
+
expires_at: new Date(Date.now() + (settings['mfa-challenge-expiration-minutes'] || 15) * 60000),
|
|
171
|
+
user_identifier_id: identifier.id,
|
|
172
|
+
},
|
|
173
|
+
select: {
|
|
174
|
+
id: true,
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await this.mail.sendTemplatedMail(
|
|
179
|
+
locale,
|
|
180
|
+
{
|
|
181
|
+
email,
|
|
182
|
+
slug: 'auth-sign-up-confirm-email',
|
|
183
|
+
variables: {
|
|
184
|
+
code,
|
|
185
|
+
name: user.name,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
requiresEmailVerification: true,
|
|
192
|
+
token: await this.token.createAccessToken({
|
|
193
|
+
challengeIdentifierId: challengeIdentifier.id,
|
|
194
|
+
email
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async emailVerificationLoginResend(locale: string, token: string) {
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const payload = await this.token.verify(locale, token);
|
|
204
|
+
|
|
205
|
+
const challenge = await this.prisma.user_identifier_challenge.findUnique({
|
|
206
|
+
where: { id: payload.challengeIdentifierId },
|
|
207
|
+
select: {
|
|
208
|
+
user_identifier: {
|
|
209
|
+
select: {
|
|
210
|
+
user_id: true
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!challenge) {
|
|
217
|
+
throw new NotFoundException(getLocaleText('challengeNotFound', locale, 'Challenge not found.'));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const user = await this.user.findUserById(locale, challenge.user_identifier.user_id);
|
|
221
|
+
|
|
222
|
+
return await this.requiresEmailVerificationForLogin(locale, payload.email, user);
|
|
223
|
+
|
|
224
|
+
} catch (error: any) {
|
|
225
|
+
if (error.message?.includes('jwt expired') || error.name === 'TokenExpiredError') {
|
|
226
|
+
|
|
227
|
+
const expiredPayload = await this.token.decodeExpiredToken(token);
|
|
228
|
+
const newToken = await this.token.createAccessToken({
|
|
229
|
+
challengeIdentifierId: expiredPayload.challengeIdentifierId,
|
|
230
|
+
email: expiredPayload.email
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return this.emailVerificationLoginResend(locale, newToken);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async emailVerificationLogin(locale: string, token: string, code: string, ipAddress: string, userAgent: string, res: any) {
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const payload = await this.token.verify(locale, token);
|
|
244
|
+
|
|
245
|
+
const challenge = await this.prisma.user_identifier_challenge.findUnique({
|
|
246
|
+
where: { id: payload.challengeIdentifierId },
|
|
247
|
+
select: {
|
|
248
|
+
hash: true,
|
|
249
|
+
user_identifier_id: true,
|
|
250
|
+
user_identifier: {
|
|
251
|
+
select: {
|
|
252
|
+
user_id: true
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (!challenge) {
|
|
259
|
+
throw new NotFoundException(getLocaleText('challengeNotFound', locale, 'Challenge not found.'));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await this.prisma.user_identifier_challenge.update({
|
|
263
|
+
where: { id: payload.challengeIdentifierId },
|
|
264
|
+
data: {
|
|
265
|
+
attempts: { increment: 1 }
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (challenge.hash !== this.security.hashWithPepper(code)) {
|
|
270
|
+
throw new BadRequestException(getLocaleText('invalidVerificationCode', locale, 'Invalid verification code.'));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await this.prisma.$transaction(async (tx) => {
|
|
274
|
+
|
|
275
|
+
await tx.user_identifier_challenge.update({
|
|
276
|
+
where: { id: payload.challengeIdentifierId },
|
|
277
|
+
data: {
|
|
278
|
+
verified_at: new Date(),
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await tx.user_identifier.update({
|
|
283
|
+
where: { id: challenge.user_identifier_id },
|
|
284
|
+
data: {
|
|
285
|
+
verified_at: new Date(),
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const user = await this.user.findUserById(locale, challenge.user_identifier.user_id);
|
|
292
|
+
|
|
293
|
+
return this.login(locale, user as unknown as User, ipAddress, userAgent, res);
|
|
294
|
+
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async login(locale: string, user: User, ipAddress: string, userAgent: string, res: any) {
|
|
301
|
+
|
|
302
|
+
const emails = await this.prisma.user_identifier.findMany({
|
|
303
|
+
where: {
|
|
304
|
+
user_id: user.id,
|
|
305
|
+
type: 'email',
|
|
306
|
+
},
|
|
307
|
+
select: { value: true }
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (!emails || emails.length === 0) {
|
|
311
|
+
throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { accessToken, refreshToken, session } = await this.getAuthenticationPayload(
|
|
315
|
+
locale,
|
|
316
|
+
user.id,
|
|
317
|
+
ipAddress,
|
|
318
|
+
userAgent
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
for (const emailObj of emails) {
|
|
322
|
+
|
|
323
|
+
this.mail.sendTemplatedMail(
|
|
324
|
+
locale,
|
|
325
|
+
{
|
|
326
|
+
email: emailObj.value,
|
|
327
|
+
slug: 'auth-login-new-device',
|
|
328
|
+
variables: {
|
|
329
|
+
name: user.name,
|
|
330
|
+
ipAddress,
|
|
331
|
+
userAgent,
|
|
332
|
+
location: 'Unknown',
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await this.user.registerUserActivity(user.id, "login")
|
|
340
|
+
|
|
341
|
+
await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
|
|
342
|
+
|
|
343
|
+
if (refreshToken) {
|
|
344
|
+
return { accessToken, refreshToken };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { accessToken };
|
|
348
|
+
}
|
|
349
|
+
|
|
56
350
|
async loginWithEmailAndPassword(
|
|
351
|
+
res: any,
|
|
57
352
|
locale: string,
|
|
58
353
|
ipAddress: string,
|
|
59
354
|
userAgent: string,
|
|
60
355
|
{ email, password }: LoginDTO,
|
|
61
356
|
) {
|
|
357
|
+
|
|
62
358
|
const user = await this.user.findUserByEmail(locale, email);
|
|
359
|
+
|
|
63
360
|
if (!user) throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
64
361
|
const credentials = (user as unknown as User).user_credential?.filter((c) => c.type === 'password') || [];
|
|
362
|
+
const identifier = user.user_identifier?.find((i) => i.type === 'email' && i.value === email);
|
|
65
363
|
|
|
66
|
-
if (!await this.security.validatePassword(locale, credentials, password)) {
|
|
364
|
+
if (!(await this.security.validatePassword(locale, credentials, password))) {
|
|
67
365
|
throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
68
366
|
}
|
|
69
367
|
|
|
@@ -100,29 +398,39 @@ export class AuthService {
|
|
|
100
398
|
};
|
|
101
399
|
}
|
|
102
400
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
401
|
+
const settings = await this.setting.getSettingValues([
|
|
402
|
+
'require-mfa',
|
|
403
|
+
'require-email-verification',
|
|
404
|
+
'mfa-email-code-length',
|
|
405
|
+
'mfa-challenge-expiration-minutes'
|
|
406
|
+
]);
|
|
109
407
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
408
|
+
if (settings['require-mfa'] === true && mfaMethods.length === 0) {
|
|
409
|
+
|
|
410
|
+
return this.requiresMfaForLogin(locale, email, user);
|
|
411
|
+
|
|
412
|
+
} else if (settings['require-email-verification'] === true && identifier?.verified_at === null) {
|
|
413
|
+
|
|
414
|
+
return this.requiresEmailVerificationForLogin(locale, email, user);
|
|
415
|
+
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return this.login(locale, user as unknown as User, ipAddress, userAgent, res);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async verifyRoles(_locale: string, userId: number) {
|
|
422
|
+
return this.prisma.role.findMany({
|
|
423
|
+
where: {
|
|
424
|
+
role_user: {
|
|
425
|
+
some: {
|
|
426
|
+
user_id: userId
|
|
427
|
+
}
|
|
120
428
|
}
|
|
429
|
+
},
|
|
430
|
+
select: {
|
|
431
|
+
slug: true
|
|
121
432
|
}
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
await this.user.registerUserActivity(user.id, "login")
|
|
125
|
-
return { accessToken, refreshToken, session };
|
|
433
|
+
})
|
|
126
434
|
}
|
|
127
435
|
|
|
128
436
|
async verifyUser(locale: string, userId: number) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IsNotEmpty, IsString } from 'class-validator';
|
|
2
|
+
import { IsPinCodeWithSetting } from '../../validators/is-pin-code-with-setting.validator';
|
|
3
|
+
|
|
4
|
+
export class LoginEmailVerificationDTO {
|
|
5
|
+
@IsString()
|
|
6
|
+
@IsNotEmpty()
|
|
7
|
+
token: string;
|
|
8
|
+
|
|
9
|
+
@IsPinCodeWithSetting()
|
|
10
|
+
@IsNotEmpty()
|
|
11
|
+
code: string;
|
|
12
|
+
}
|
|
@@ -375,6 +375,9 @@ export class ChallengeService {
|
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
async sendMfaCodeToMultipleEmails(locale: string, userId: number, emails: string[]) {
|
|
378
|
+
|
|
379
|
+
console.log('Sending MFA codes to multiple emails:', emails);
|
|
380
|
+
|
|
378
381
|
if (!emails || emails.length === 0) {
|
|
379
382
|
return { success: true };
|
|
380
383
|
}
|
|
@@ -416,6 +419,7 @@ export class ChallengeService {
|
|
|
416
419
|
});
|
|
417
420
|
|
|
418
421
|
for (const email of emails) {
|
|
422
|
+
|
|
419
423
|
this.mail.sendTemplatedMail(
|
|
420
424
|
locale,
|
|
421
425
|
{
|
|
@@ -2,19 +2,19 @@ import { DeleteDTO, Role } from '@hed-hog/api';
|
|
|
2
2
|
import { Locale } from '@hed-hog/api-locale';
|
|
3
3
|
import { Pagination } from '@hed-hog/api-pagination';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
Body,
|
|
6
|
+
Controller,
|
|
7
|
+
Delete,
|
|
8
|
+
Get,
|
|
9
|
+
Header,
|
|
10
|
+
Inject,
|
|
11
|
+
Param,
|
|
12
|
+
ParseIntPipe,
|
|
13
|
+
Patch,
|
|
14
|
+
Post,
|
|
15
|
+
Query,
|
|
16
|
+
Res,
|
|
17
|
+
forwardRef,
|
|
18
18
|
} from '@nestjs/common';
|
|
19
19
|
import { Response } from 'express';
|
|
20
20
|
import { CreateDTO } from './dto/create.dto';
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
|
|
2
2
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
3
|
-
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
|
3
|
+
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
|
|
4
|
+
import { IsPinCodeWithSetting } from '../../validators/is-pin-code-with-setting.validator';
|
|
4
5
|
|
|
5
6
|
export class EmailVerificationDto {
|
|
6
7
|
@IsString({ message: (args) => getLocaleText('validation.stringRequired', args.value) })
|
|
8
|
+
@IsPinCodeWithSetting()
|
|
7
9
|
pin: string;
|
|
8
10
|
|
|
9
11
|
@IsNumber({}, { message: (args) => getLocaleText('validation.numberRequired', args.value) })
|
|
10
12
|
@IsNotEmpty({ message: (args) => getLocaleText('validation.fieldRequired', args.value) })
|
|
11
13
|
challengeId: number;
|
|
14
|
+
|
|
15
|
+
@IsOptional()
|
|
16
|
+
@IsString({ message: (args) => getLocaleText('validation.stringRequired', args.value) })
|
|
17
|
+
name?: string;
|
|
12
18
|
}
|
|
@@ -81,6 +81,7 @@ export class ProfileController {
|
|
|
81
81
|
|
|
82
82
|
@Post('mfa/email/verify/confirm')
|
|
83
83
|
sendEmailVerificationMfaConfirm(@User() {id}, @Body() data: EmailVerificationDto, @Locale() locale: string) {
|
|
84
|
+
console.log('sendEmailVerificationMfaConfirm')
|
|
84
85
|
return this.profileService.confirmEmailVerificationMfa(locale, id, data);
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -135,15 +135,84 @@ export class ProfileService {
|
|
|
135
135
|
return { success: true };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
async confirmEmailVerificationMfa(locale: string, userId: number, { pin, challengeId }: EmailVerificationDto) {
|
|
138
|
+
async confirmEmailVerificationMfa(locale: string, userId: number, { pin, challengeId, name }: EmailVerificationDto) {
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const user = await this.prisma.user.findUnique({
|
|
141
|
+
where: { id: userId },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!user) {
|
|
145
|
+
throw new NotFoundException(getLocaleText('userNotFound', locale, 'User not found.'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const challenge = await this.prisma.user_mfa_challenge.findFirst({
|
|
149
|
+
where: {
|
|
150
|
+
id: challengeId,
|
|
151
|
+
expires_at: {
|
|
152
|
+
gt: new Date()
|
|
153
|
+
},
|
|
154
|
+
verified_at: null
|
|
155
|
+
},
|
|
156
|
+
include: {
|
|
157
|
+
user_mfa: {
|
|
158
|
+
include: {
|
|
159
|
+
user_mfa_email: true
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!challenge) {
|
|
166
|
+
throw new NotFoundException(getLocaleText('challengeNotFound', locale, 'Challenge not found.'));
|
|
145
167
|
}
|
|
146
168
|
|
|
169
|
+
await this.prisma.user_mfa_challenge.update({
|
|
170
|
+
where: {
|
|
171
|
+
id: challengeId
|
|
172
|
+
},
|
|
173
|
+
data: {
|
|
174
|
+
attempts: { increment: 1 }
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (challenge.hash !== this.security.hashWithPepper(pin)) {
|
|
179
|
+
throw new BadRequestException(getLocaleText('invalidPin', locale, 'Invalid PIN.'));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await this.prisma.$transaction(async (tx) => {
|
|
183
|
+
|
|
184
|
+
await tx.user_mfa_challenge.update({
|
|
185
|
+
where: { id: challengeId },
|
|
186
|
+
data: { verified_at: new Date() },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await tx.user_mfa.update({
|
|
190
|
+
where: { id: challenge.user_mfa.id },
|
|
191
|
+
data: {
|
|
192
|
+
verified_at: new Date(),
|
|
193
|
+
name: name || challenge.user_mfa.name
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await tx.user_identifier.updateMany({
|
|
198
|
+
where: {
|
|
199
|
+
user_id: userId,
|
|
200
|
+
verified_at: { not: null },
|
|
201
|
+
value: challenge.user_mfa.user_mfa_email[0].email,
|
|
202
|
+
},
|
|
203
|
+
data: {
|
|
204
|
+
enabled: true,
|
|
205
|
+
verified_at: new Date(),
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const updatedUser = await this.prisma.user.findFirst({ where: { id: user.id }})
|
|
212
|
+
const codes = await this.createMfaRecoveryCodes(user.id);
|
|
213
|
+
const newToken = await this.getToken(updatedUser);
|
|
214
|
+
return { ...newToken, codes };
|
|
215
|
+
|
|
147
216
|
}
|
|
148
217
|
|
|
149
218
|
async confirmEmailVerification(locale: string, userId: number, { pin, challengeId }: EmailVerificationDto) {
|
|
@@ -18,8 +18,13 @@ export class TokenService {
|
|
|
18
18
|
|
|
19
19
|
async verify(locale: string, token: string) {
|
|
20
20
|
try {
|
|
21
|
-
return this.jwt.verifyAsync(token, {
|
|
21
|
+
return this.jwt.verifyAsync(token, {
|
|
22
|
+
secret: this.security.getJwtSecret(),
|
|
23
|
+
});
|
|
22
24
|
} catch (error) {
|
|
25
|
+
|
|
26
|
+
console.log('JWT ERROR', error);
|
|
27
|
+
|
|
23
28
|
// Return 401 for JWT errors (expired, invalid, etc.)
|
|
24
29
|
throw new UnauthorizedException(
|
|
25
30
|
(error as any).message || getLocaleText('accessDenied', locale, 'Access denied.')
|
|
@@ -107,4 +112,15 @@ export class TokenService {
|
|
|
107
112
|
throw new ForbiddenException('Invalid or expired MFA token');
|
|
108
113
|
}
|
|
109
114
|
}
|
|
115
|
+
|
|
116
|
+
async decodeExpiredToken(token: string): Promise<any> {
|
|
117
|
+
try {
|
|
118
|
+
return await this.jwt.verifyAsync(token, {
|
|
119
|
+
secret: this.security.getJwtSecret(),
|
|
120
|
+
ignoreExpiration: true,
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new UnauthorizedException('Invalid token');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
110
126
|
}
|
package/src/user/user.service.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
2
|
import { PaginationService } from '@hed-hog/api-pagination';
|
|
3
|
-
import { PrismaService
|
|
3
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
4
|
import {
|
|
5
5
|
BadRequestException,
|
|
6
6
|
Inject,
|
|
@@ -143,8 +143,16 @@ export class UserService {
|
|
|
143
143
|
include: {
|
|
144
144
|
user_account: true,
|
|
145
145
|
user_credential: true,
|
|
146
|
-
user_identifier:
|
|
147
|
-
|
|
146
|
+
user_identifier: {
|
|
147
|
+
where: {
|
|
148
|
+
enabled: true,
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
user_mfa: {
|
|
152
|
+
where: {
|
|
153
|
+
verified_at: { not: null },
|
|
154
|
+
}
|
|
155
|
+
},
|
|
148
156
|
user_activity: {
|
|
149
157
|
orderBy: {
|
|
150
158
|
created_at: 'desc',
|
|
@@ -228,7 +236,7 @@ export class UserService {
|
|
|
228
236
|
return { user, challenge };
|
|
229
237
|
}
|
|
230
238
|
|
|
231
|
-
|
|
239
|
+
async findUserById(locale: string, id: number) {
|
|
232
240
|
const user = await this.prismaService.user.findUnique({
|
|
233
241
|
where: { id },
|
|
234
242
|
include: {
|
|
@@ -247,7 +255,7 @@ export class UserService {
|
|
|
247
255
|
return user;
|
|
248
256
|
}
|
|
249
257
|
|
|
250
|
-
async findUserByEmail(
|
|
258
|
+
async findUserByEmail(_locale: string, email: string) {
|
|
251
259
|
const user = await this.prismaService.user.findFirst({
|
|
252
260
|
where: {
|
|
253
261
|
user_identifier: {
|