@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.
Files changed (72) hide show
  1. package/dist/auth/auth.controller.d.ts +24 -11
  2. package/dist/auth/auth.controller.d.ts.map +1 -1
  3. package/dist/auth/auth.controller.js +43 -9
  4. package/dist/auth/auth.controller.js.map +1 -1
  5. package/dist/auth/auth.service.d.ts +34 -22
  6. package/dist/auth/auth.service.d.ts.map +1 -1
  7. package/dist/auth/auth.service.js +256 -13
  8. package/dist/auth/auth.service.js.map +1 -1
  9. package/dist/auth/dto/login-email-verification-resend.dto.d.ts +4 -0
  10. package/dist/auth/dto/login-email-verification-resend.dto.d.ts.map +1 -0
  11. package/dist/auth/dto/login-email-verification-resend.dto.js +22 -0
  12. package/dist/auth/dto/login-email-verification-resend.dto.js.map +1 -0
  13. package/dist/auth/dto/login-email-verification.dto.d.ts +5 -0
  14. package/dist/auth/dto/login-email-verification.dto.d.ts.map +1 -0
  15. package/dist/auth/dto/login-email-verification.dto.js +28 -0
  16. package/dist/auth/dto/login-email-verification.dto.js.map +1 -0
  17. package/dist/challenge/challenge.service.d.ts.map +1 -1
  18. package/dist/challenge/challenge.service.js +1 -0
  19. package/dist/challenge/challenge.service.js.map +1 -1
  20. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +2 -2
  21. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
  22. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  23. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  24. package/dist/mail/mail.controller.d.ts +2 -2
  25. package/dist/mail/mail.service.d.ts +2 -2
  26. package/dist/menu/menu.controller.d.ts +6 -6
  27. package/dist/menu/menu.service.d.ts +6 -6
  28. package/dist/profile/dto/email-verification-confirm.dto.d.ts +1 -0
  29. package/dist/profile/dto/email-verification-confirm.dto.d.ts.map +1 -1
  30. package/dist/profile/dto/email-verification-confirm.dto.js +7 -0
  31. package/dist/profile/dto/email-verification-confirm.dto.js.map +1 -1
  32. package/dist/profile/profile.controller.d.ts +2 -4
  33. package/dist/profile/profile.controller.d.ts.map +1 -1
  34. package/dist/profile/profile.controller.js +1 -0
  35. package/dist/profile/profile.controller.js.map +1 -1
  36. package/dist/profile/profile.service.d.ts +3 -5
  37. package/dist/profile/profile.service.d.ts.map +1 -1
  38. package/dist/profile/profile.service.js +65 -7
  39. package/dist/profile/profile.service.js.map +1 -1
  40. package/dist/role/role.controller.d.ts +3 -3
  41. package/dist/role/role.service.d.ts +3 -3
  42. package/dist/screen/screen.controller.d.ts +3 -3
  43. package/dist/screen/screen.service.d.ts +3 -3
  44. package/dist/security/security.service.d.ts +1 -0
  45. package/dist/security/security.service.d.ts.map +1 -1
  46. package/dist/security/security.service.js +5 -0
  47. package/dist/security/security.service.js.map +1 -1
  48. package/dist/setting/setting.controller.d.ts +4 -4
  49. package/dist/setting/setting.service.d.ts +4 -4
  50. package/dist/token/token.service.d.ts +1 -0
  51. package/dist/token/token.service.d.ts.map +1 -1
  52. package/dist/token/token.service.js +15 -1
  53. package/dist/token/token.service.js.map +1 -1
  54. package/dist/user/user.controller.d.ts +1 -1
  55. package/dist/user/user.service.d.ts +46 -4
  56. package/dist/user/user.service.d.ts.map +1 -1
  57. package/dist/user/user.service.js +11 -3
  58. package/dist/user/user.service.js.map +1 -1
  59. package/hedhog/data/mail.yaml +19 -0
  60. package/package.json +3 -3
  61. package/src/auth/auth.controller.ts +30 -10
  62. package/src/auth/auth.service.ts +329 -21
  63. package/src/auth/dto/login-email-verification-resend.dto.ts +7 -0
  64. package/src/auth/dto/login-email-verification.dto.ts +12 -0
  65. package/src/challenge/challenge.service.ts +4 -0
  66. package/src/mail/mail.controller.ts +13 -13
  67. package/src/profile/dto/email-verification-confirm.dto.ts +7 -1
  68. package/src/profile/profile.controller.ts +1 -0
  69. package/src/profile/profile.service.ts +75 -6
  70. package/src/security/security.service.ts +8 -0
  71. package/src/token/token.service.ts +17 -1
  72. package/src/user/user.service.ts +13 -5
@@ -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 { accessToken, refreshToken, session } = await this.getAuthenticationPayload(
104
- locale,
105
- user.id,
106
- ipAddress,
107
- userAgent
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
- this.mail.sendTemplatedMail(
111
- locale,
112
- {
113
- email,
114
- slug: 'auth-login-new-device',
115
- variables: {
116
- name: user.name,
117
- ipAddress,
118
- userAgent,
119
- location: 'Unknown',
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,7 @@
1
+ import { IsNotEmpty, IsString } from 'class-validator';
2
+
3
+ export class LoginEmailVerificationResendDTO {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ token: string;
7
+ }
@@ -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
- 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,
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
- return {
141
- locale,
142
- userId,
143
- pin,
144
- challengeId
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) {
@@ -100,4 +100,12 @@ export class SecurityService {
100
100
  }
101
101
  }
102
102
 
103
+ generateCode(length: number): string {
104
+
105
+ return Array.from(crypto.getRandomValues(new Uint8Array(length || 6)))
106
+ .map(n => (n % 10).toString())
107
+ .join('');
108
+
109
+ }
110
+
103
111
  }
@@ -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, { secret: this.security.getJwtSecret() });
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
  }
@@ -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, user } from '@hed-hog/api-prisma';
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: true,
147
- user_mfa: true,
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
- async findUserById(locale: string, id: number) {
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(locale: string, email: string): Promise<user | null> {
258
+ async findUserByEmail(_locale: string, email: string) {
251
259
  const user = await this.prismaService.user.findFirst({
252
260
  where: {
253
261
  user_identifier: {