@hed-hog/core 0.0.276 → 0.0.279

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 (77) hide show
  1. package/README.md +60 -0
  2. package/dist/auth/auth.controller.d.ts +8 -1
  3. package/dist/auth/auth.controller.d.ts.map +1 -1
  4. package/dist/auth/auth.controller.js +7 -7
  5. package/dist/auth/auth.controller.js.map +1 -1
  6. package/dist/auth/auth.service.d.ts +10 -1
  7. package/dist/auth/auth.service.d.ts.map +1 -1
  8. package/dist/auth/auth.service.js +34 -8
  9. package/dist/auth/auth.service.js.map +1 -1
  10. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +12 -0
  11. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  12. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
  13. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  14. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +12 -0
  15. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.js +25 -0
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  18. package/dist/profile/profile.service.js +1 -1
  19. package/dist/profile/profile.service.js.map +1 -1
  20. package/dist/role/guards/role.guard.d.ts +1 -0
  21. package/dist/role/guards/role.guard.d.ts.map +1 -1
  22. package/dist/role/guards/role.guard.js +18 -0
  23. package/dist/role/guards/role.guard.js.map +1 -1
  24. package/dist/session/session.service.js +1 -1
  25. package/dist/session/session.service.js.map +1 -1
  26. package/dist/user/dto/reset-password.dto.d.ts +4 -0
  27. package/dist/user/dto/reset-password.dto.d.ts.map +1 -0
  28. package/dist/user/dto/reset-password.dto.js +26 -0
  29. package/dist/user/dto/reset-password.dto.js.map +1 -0
  30. package/dist/user/user.controller.d.ts +5 -0
  31. package/dist/user/user.controller.d.ts.map +1 -1
  32. package/dist/user/user.controller.js +13 -0
  33. package/dist/user/user.controller.js.map +1 -1
  34. package/dist/user/user.service.d.ts +6 -0
  35. package/dist/user/user.service.d.ts.map +1 -1
  36. package/dist/user/user.service.js +65 -0
  37. package/dist/user/user.service.js.map +1 -1
  38. package/hedhog/data/dashboard_component.yaml +74 -12
  39. package/hedhog/data/dashboard_component_role.yaml +223 -145
  40. package/hedhog/data/dashboard_item.yaml +42 -22
  41. package/hedhog/data/dashboard_role.yaml +18 -12
  42. package/hedhog/data/menu.yaml +6 -0
  43. package/hedhog/data/route.yaml +65 -1
  44. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
  45. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +17 -17
  46. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +23 -12
  47. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +80 -5
  48. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +17 -13
  49. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +16 -12
  50. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +27 -16
  51. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +13 -9
  52. package/hedhog/frontend/app/dashboard/components/widgets/menus-card.tsx.ejs +58 -0
  53. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
  54. package/hedhog/frontend/app/dashboard/components/widgets/routes-card.tsx.ejs +58 -0
  55. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +6 -6
  56. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +6 -6
  57. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +6 -6
  58. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +6 -6
  59. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +15 -11
  60. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +18 -15
  61. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +20 -4
  62. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
  63. package/hedhog/frontend/app/mail/log/page.tsx.ejs +5 -11
  64. package/hedhog/frontend/app/users/page.tsx.ejs +331 -10
  65. package/hedhog/frontend/messages/en.json +29 -3
  66. package/hedhog/frontend/messages/pt.json +29 -3
  67. package/package.json +4 -4
  68. package/src/auth/auth.controller.ts +21 -20
  69. package/src/auth/auth.service.ts +63 -15
  70. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
  71. package/src/dashboard/dashboard-core/dashboard-core.service.ts +34 -0
  72. package/src/profile/profile.service.ts +1 -1
  73. package/src/role/guards/role.guard.ts +36 -7
  74. package/src/session/session.service.ts +2 -2
  75. package/src/user/dto/reset-password.dto.ts +11 -0
  76. package/src/user/user.controller.ts +24 -14
  77. package/src/user/user.service.ts +84 -0
@@ -208,7 +208,24 @@
208
208
  "roleRemoved": "Role removed successfully",
209
209
  "errorLoadingRoles": "Error loading roles",
210
210
  "errorAssigningRole": "Error assigning role",
211
- "errorRemovingRole": "Error removing role"
211
+ "errorRemovingRole": "Error removing role",
212
+ "passwordResetTitle": "Password Reset",
213
+ "passwordResetDescription": "Generate a new password for this user.",
214
+ "passwordResetNotice": "After this reset, the user must change the password on the next login.",
215
+ "buttonResetPassword": "Reset Password",
216
+ "passwordResetDialogTitle": "Reset User Password",
217
+ "passwordResetDialogDescription": "A strong password is generated by default. You can edit it before confirming.",
218
+ "passwordResetFieldLabel": "New temporary password",
219
+ "buttonRegeneratePassword": "Regenerate password",
220
+ "passwordResetConfirm": "Confirm reset",
221
+ "passwordResetSubmitting": "Resetting...",
222
+ "passwordResetSuccess": "Password reset successfully.",
223
+ "passwordResultTitle": "Password Generated",
224
+ "passwordResultDescription": "Copy and share this password now. It will only be shown once.",
225
+ "buttonCopyPassword": "Copy password",
226
+ "passwordCopied": "Password copied to clipboard.",
227
+ "passwordCopyError": "Unable to copy password.",
228
+ "close": "Close"
212
229
  },
213
230
  "RolePage": {
214
231
  "title": "Roles Management",
@@ -401,6 +418,7 @@
401
418
  "passwordRequired": "Password is required",
402
419
  "passwordMinLength": "Password must be at least 6 characters long",
403
420
  "mfaVerificationMessage": "Enter the verification code",
421
+ "passwordResetRequiredMessage": "You must change your password before continuing.",
404
422
  "hidePassword": "Hide password",
405
423
  "showPassword": "Show password",
406
424
  "loginWith": "Sign in with"
@@ -1026,10 +1044,12 @@
1026
1044
  "extraLarge": "Extra Large",
1027
1045
  "clearSize": "Clear Size",
1028
1046
  "heading": "Heading",
1047
+ "alignment": "Alignment",
1029
1048
  "alignLeft": "Align Left",
1030
1049
  "alignCenter": "Center",
1031
1050
  "alignRight": "Align Right",
1032
1051
  "justify": "Justify",
1052
+ "lists": "Lists",
1033
1053
  "bulletList": "Bullet List",
1034
1054
  "numberedList": "Numbered List",
1035
1055
  "addLink": "Add Link",
@@ -1043,6 +1063,7 @@
1043
1063
  "clearFormatting": "Clear Formatting",
1044
1064
  "undo": "Undo",
1045
1065
  "redo": "Redo",
1066
+ "more": "More",
1046
1067
  "advancedMode": "Advanced Mode (HTML)",
1047
1068
  "advancedModeTitle": "Advanced Mode - HTML Editor",
1048
1069
  "advancedModeDescription": "Edit HTML directly with syntax highlighting and automatic indentation",
@@ -1142,8 +1163,11 @@
1142
1163
  },
1143
1164
  "ForbiddenDialog": {
1144
1165
  "title": "Access Denied",
1145
- "defaultMessage": "You do not have permission to access this resource.",
1146
- "understood": "Understood"
1166
+ "defaultMessage": "You do not have access to request this resource.",
1167
+ "understood": "Understood",
1168
+ "statusCode": "Status",
1169
+ "method": "Method",
1170
+ "url": "URL"
1147
1171
  },
1148
1172
  "ForbiddenPage": {
1149
1173
  "message": "Forbidden: You don't have permission to access this page.",
@@ -1156,6 +1180,8 @@
1156
1180
  "sessionsToday": "Sessions Today",
1157
1181
  "emailsSent": "E-mails Sent",
1158
1182
  "permissions": "Permissions",
1183
+ "menus": "Menus",
1184
+ "routes": "Routes",
1159
1185
  "userGrowthTitle": "User Growth",
1160
1186
  "userGrowthDescription": "Monthly evolution of users and sessions",
1161
1187
  "users": "Users",
@@ -208,7 +208,24 @@
208
208
  "roleRemoved": "Cargo removido com sucesso",
209
209
  "errorLoadingRoles": "Erro ao carregar cargos",
210
210
  "errorAssigningRole": "Erro ao atribuir cargo",
211
- "errorRemovingRole": "Erro ao remover cargo"
211
+ "errorRemovingRole": "Erro ao remover cargo",
212
+ "passwordResetTitle": "Redefinição de senha",
213
+ "passwordResetDescription": "Gere uma nova senha para este usuário.",
214
+ "passwordResetNotice": "Após essa redefinição, o usuário será obrigado a alterar a senha no próximo login.",
215
+ "buttonResetPassword": "Redefinir senha",
216
+ "passwordResetDialogTitle": "Redefinir senha do usuário",
217
+ "passwordResetDialogDescription": "Uma senha forte é gerada por padrão. Você pode editar antes de confirmar.",
218
+ "passwordResetFieldLabel": "Nova senha temporária",
219
+ "buttonRegeneratePassword": "Gerar outra senha",
220
+ "passwordResetConfirm": "Confirmar redefinição",
221
+ "passwordResetSubmitting": "Redefinindo...",
222
+ "passwordResetSuccess": "Senha redefinida com sucesso.",
223
+ "passwordResultTitle": "Senha gerada",
224
+ "passwordResultDescription": "Copie e compartilhe essa senha agora. Ela será exibida apenas uma vez.",
225
+ "buttonCopyPassword": "Copiar senha",
226
+ "passwordCopied": "Senha copiada para a área de transferência.",
227
+ "passwordCopyError": "Não foi possível copiar a senha.",
228
+ "close": "Fechar"
212
229
  },
213
230
  "RolePage": {
214
231
  "title": "Gerenciamento de Cargos",
@@ -401,6 +418,7 @@
401
418
  "passwordRequired": "Senha obrigatória",
402
419
  "passwordMinLength": "Senha mínima de 6 caracteres",
403
420
  "mfaVerificationMessage": "Digite o código de verificação",
421
+ "passwordResetRequiredMessage": "Você deve alterar sua senha antes de continuar.",
404
422
  "hidePassword": "Ocultar senha",
405
423
  "showPassword": "Mostrar senha",
406
424
  "loginWith": "Entrar com"
@@ -1031,10 +1049,12 @@
1031
1049
  "extraLarge": "Extra Grande",
1032
1050
  "clearSize": "Limpar Tamanho",
1033
1051
  "heading": "Título",
1052
+ "alignment": "Alinhamento",
1034
1053
  "alignLeft": "Alinhar à Esquerda",
1035
1054
  "alignCenter": "Centralizar",
1036
1055
  "alignRight": "Alinhar à Direita",
1037
1056
  "justify": "Justificar",
1057
+ "lists": "Listas",
1038
1058
  "bulletList": "Lista com Marcadores",
1039
1059
  "numberedList": "Lista Numerada",
1040
1060
  "addLink": "Adicionar Link",
@@ -1048,6 +1068,7 @@
1048
1068
  "clearFormatting": "Limpar Formatação",
1049
1069
  "undo": "Desfazer",
1050
1070
  "redo": "Refazer",
1071
+ "more": "Mais",
1051
1072
  "advancedMode": "Modo Avançado (HTML)",
1052
1073
  "advancedModeTitle": "Modo Avançado - Editor HTML",
1053
1074
  "advancedModeDescription": "Edite o HTML diretamente com syntax highlighting e indentação automática",
@@ -1197,8 +1218,11 @@
1197
1218
  },
1198
1219
  "ForbiddenDialog": {
1199
1220
  "title": "Acesso Negado",
1200
- "defaultMessage": "Você não tem permissão para acessar este recurso.",
1201
- "understood": "Entendi"
1221
+ "defaultMessage": "Você não tem acesso para solicitar este recurso.",
1222
+ "understood": "Entendi",
1223
+ "statusCode": "Status",
1224
+ "method": "Método",
1225
+ "url": "URL"
1202
1226
  },
1203
1227
  "ForbiddenPage": {
1204
1228
  "message": "Proibido: Você não tem permissão para acessar esta página.",
@@ -1211,6 +1235,8 @@
1211
1235
  "sessionsToday": "Sessões Hoje",
1212
1236
  "emailsSent": "E-mails Enviados",
1213
1237
  "permissions": "Permissões",
1238
+ "menus": "Menus",
1239
+ "routes": "Rotas",
1214
1240
  "userGrowthTitle": "Crescimento de Usuários",
1215
1241
  "userGrowthDescription": "Evolução mensal de usuários e sessões",
1216
1242
  "users": "Usuários",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.276",
3
+ "version": "0.0.279",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -31,11 +31,11 @@
31
31
  "speakeasy": "^2.0.0",
32
32
  "uuid": "^11.1.0",
33
33
  "@hed-hog/api-pagination": "0.0.6",
34
- "@hed-hog/api-prisma": "0.0.5",
35
34
  "@hed-hog/api-types": "0.0.1",
35
+ "@hed-hog/api-locale": "0.0.13",
36
36
  "@hed-hog/api-mail": "0.0.8",
37
- "@hed-hog/api": "0.0.4",
38
- "@hed-hog/api-locale": "0.0.13"
37
+ "@hed-hog/api-prisma": "0.0.5",
38
+ "@hed-hog/api": "0.0.4"
39
39
  },
40
40
  "exports": {
41
41
  ".": {
@@ -1,18 +1,19 @@
1
1
  import { Public, Role, User, UserOptional } from '@hed-hog/api';
2
2
  import { Locale } from '@hed-hog/api-locale';
3
- import {
4
- BadRequestException,
5
- Body,
6
- Controller,
7
- forwardRef,
8
- Get,
9
- Headers,
10
- Inject,
11
- Ip,
12
- Post,
13
- Req,
14
- Res
15
- } from '@nestjs/common';
3
+ import {
4
+ BadRequestException,
5
+ Body,
6
+ Controller,
7
+ forwardRef,
8
+ Get,
9
+ Headers,
10
+ Inject,
11
+ Ip,
12
+ Post,
13
+ Req,
14
+ Res,
15
+ UnauthorizedException,
16
+ } from '@nestjs/common';
16
17
  import { TokenService } from '../token/token.service';
17
18
  import { CreateWithEmailAndPasswordDTO } from '../user/dto/create-with-email-and-password.dto';
18
19
  import { UserService } from '../user/user.service';
@@ -123,7 +124,7 @@ export class AuthController {
123
124
  @Headers('user-agent') userAgent: string,
124
125
  @Res({ passthrough: true }) res,
125
126
  ) {
126
- const { accessToken, refreshToken, session } = await this.service.verifyMfaCode(
127
+ const { accessToken, refreshToken, session, requiresPasswordReset } = await this.service.verifyMfaCode(
127
128
  locale,
128
129
  token,
129
130
  code,
@@ -133,7 +134,7 @@ export class AuthController {
133
134
  );
134
135
 
135
136
  await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
136
- return { accessToken, refreshToken };
137
+ return { accessToken, refreshToken, requiresPasswordReset };
137
138
  }
138
139
 
139
140
  @Public()
@@ -145,7 +146,7 @@ export class AuthController {
145
146
  @Headers('user-agent') userAgent: string,
146
147
  @Res({ passthrough: true }) res,
147
148
  ) {
148
- const { accessToken, refreshToken, session } = await this.service.verifyMfaRecoveryCode(
149
+ const { accessToken, refreshToken, session, requiresPasswordReset } = await this.service.verifyMfaRecoveryCode(
149
150
  locale,
150
151
  token,
151
152
  code,
@@ -154,7 +155,7 @@ export class AuthController {
154
155
  );
155
156
 
156
157
  await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
157
- return { accessToken, refreshToken };
158
+ return { accessToken, refreshToken, requiresPasswordReset };
158
159
  }
159
160
 
160
161
  @Public()
@@ -184,7 +185,7 @@ export class AuthController {
184
185
  @Headers('user-agent') userAgent: string,
185
186
  @Res({ passthrough: true }) res,
186
187
  ) {
187
- const { accessToken, refreshToken, session } = await this.service.verifyWebAuthnAuthentication(
188
+ const { accessToken, refreshToken, session, requiresPasswordReset } = await this.service.verifyWebAuthnAuthentication(
188
189
  locale,
189
190
  mfaToken,
190
191
  assertionResponse,
@@ -193,7 +194,7 @@ export class AuthController {
193
194
  );
194
195
 
195
196
  await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
196
- return { accessToken, refreshToken };
197
+ return { accessToken, refreshToken, requiresPasswordReset };
197
198
  }
198
199
 
199
200
  @Public()
@@ -215,7 +216,7 @@ export class AuthController {
215
216
  ) {
216
217
  const refreshToken = req.cookies['rt'] || refreshTokenFromBody;
217
218
  if (!refreshToken) {
218
- throw new BadRequestException('Refresh token not provided');
219
+ throw new UnauthorizedException('Refresh token not provided');
219
220
  }
220
221
 
221
222
  await this.service.logout(res, req, refreshToken);
@@ -1,13 +1,14 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PrismaService } from '@hed-hog/api-prisma';
3
3
  import { User } from '@hed-hog/api-types';
4
- import {
5
- BadRequestException,
6
- forwardRef,
7
- Inject,
8
- Injectable,
9
- NotFoundException,
10
- } from '@nestjs/common';
4
+ import {
5
+ BadRequestException,
6
+ forwardRef,
7
+ Inject,
8
+ Injectable,
9
+ NotFoundException,
10
+ UnauthorizedException,
11
+ } from '@nestjs/common';
11
12
  import { ChallengeService } from '../challenge/challenge.service';
12
13
  import { MailService as MailManagerService } from '../mail/mail.service';
13
14
  import { SecurityService } from '../security/security.service';
@@ -54,6 +55,31 @@ export class AuthService {
54
55
 
55
56
  }
56
57
 
58
+ private hasPendingPasswordReset(user: User | any) {
59
+ return Boolean(
60
+ user?.user_credential?.some(
61
+ (credential) =>
62
+ credential.type === 'password' &&
63
+ credential.requires_reset === true &&
64
+ !credential.revoked_at,
65
+ ),
66
+ );
67
+ }
68
+
69
+ private async getRequiresPasswordResetByUserId(userId: number) {
70
+ const credential = await this.prisma.user_credential.findFirst({
71
+ where: {
72
+ user_id: userId,
73
+ type: 'password',
74
+ requires_reset: true,
75
+ revoked_at: null,
76
+ },
77
+ select: { id: true },
78
+ });
79
+
80
+ return Boolean(credential?.id);
81
+ }
82
+
57
83
  async requiresMfaForLogin(locale: string, email: string, user: any) {
58
84
 
59
85
  console.log('MFA required, setting up email MFA');
@@ -340,11 +366,13 @@ export class AuthService {
340
366
 
341
367
  await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
342
368
 
369
+ const requiresPasswordReset = this.hasPendingPasswordReset(user);
370
+
343
371
  if (refreshToken) {
344
- return { accessToken, refreshToken };
372
+ return { accessToken, refreshToken, requiresPasswordReset };
345
373
  }
346
374
 
347
- return { accessToken };
375
+ return { accessToken, requiresPasswordReset };
348
376
  }
349
377
 
350
378
  async loginWithEmailAndPassword(
@@ -358,10 +386,17 @@ export class AuthService {
358
386
  const user = await this.user.findUserByEmail(locale, email);
359
387
 
360
388
  if (!user) throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
389
+
390
+ console.debug({ user });
391
+
361
392
  const credentials = (user as unknown as User).user_credential?.filter((c) => c.type === 'password') || [];
393
+
394
+ console.debug({ credentials });
395
+
362
396
  const identifier = user.user_identifier?.find((i) => i.type === 'email' && i.value === email);
363
397
 
364
398
  if (!(await this.security.validatePassword(locale, credentials, password))) {
399
+ console.debug('Invalid password');
365
400
  throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
366
401
  }
367
402
 
@@ -435,14 +470,18 @@ export class AuthService {
435
470
 
436
471
  async verifyUser(locale: string, userId: number) {
437
472
  const user = await this.user.findUserById(locale, userId);
473
+ const requiresPasswordReset = this.hasPendingPasswordReset(user);
438
474
  delete user.user_credential;
439
- return user;
475
+ return {
476
+ ...user,
477
+ requires_password_reset: requiresPasswordReset,
478
+ };
440
479
  }
441
480
 
442
481
  async refreshAccessToken(locale: string, refreshToken: string, ipAddress: string, userAgent: string) {
443
482
 
444
483
  if (!refreshToken) {
445
- throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
484
+ throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
446
485
  }
447
486
 
448
487
  const { session, token } = await this.session.refresh(
@@ -492,7 +531,7 @@ export class AuthService {
492
531
  }
493
532
  }
494
533
  },
495
- data: { hash: passwordHash },
534
+ data: { hash: passwordHash, requires_reset: false },
496
535
  });
497
536
 
498
537
  const userIdentifier = await this.prisma.user_identifier.findFirst({
@@ -646,7 +685,10 @@ export class AuthService {
646
685
  );
647
686
 
648
687
  await this.user.registerUserActivity(user.id, "login");
649
- return { accessToken, refreshToken, session };
688
+ const requiresPasswordReset = await this.getRequiresPasswordResetByUserId(
689
+ user.id,
690
+ );
691
+ return { accessToken, refreshToken, session, requiresPasswordReset };
650
692
  }
651
693
 
652
694
  async verifyMfaRecoveryCode(locale: string, mfaToken: string, recoveryCode: string, ipAddress: string, userAgent: string) {
@@ -685,7 +727,10 @@ export class AuthService {
685
727
  );
686
728
 
687
729
  await this.user.registerUserActivity(user.id, "login");
688
- return { accessToken, refreshToken, session };
730
+ const requiresPasswordReset = await this.getRequiresPasswordResetByUserId(
731
+ user.id,
732
+ );
733
+ return { accessToken, refreshToken, session, requiresPasswordReset };
689
734
  }
690
735
 
691
736
  async resendMfaCode(locale: string, mfaToken: string) {
@@ -846,6 +891,9 @@ export class AuthService {
846
891
  );
847
892
 
848
893
  await this.user.registerUserActivity(userId, 'login');
849
- return { accessToken, refreshToken, session };
894
+ const requiresPasswordReset = await this.getRequiresPasswordResetByUserId(
895
+ userId,
896
+ );
897
+ return { accessToken, refreshToken, session, requiresPasswordReset };
850
898
  }
851
899
  }
@@ -23,6 +23,11 @@ export class DashboardCoreController {
23
23
  return this.dashboardCoreService.getMailStatistics();
24
24
  }
25
25
 
26
+ @Get('stats/overview/system')
27
+ getSystemStatistics() {
28
+ return this.dashboardCoreService.getSystemStatistics();
29
+ }
30
+
26
31
  @Get('widgets/me')
27
32
  getWidgetsData(@User() user, @Locale() locale: string) {
28
33
  return this.dashboardCoreService.getWidgetsData(user.id, locale);
@@ -297,6 +297,40 @@ export class DashboardCoreService {
297
297
  };
298
298
  }
299
299
 
300
+ async getSystemStatistics() {
301
+ const now = new Date();
302
+ const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
303
+ const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
304
+ const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59);
305
+
306
+ const [
307
+ menusCount,
308
+ routesCount,
309
+ menusCurrentMonth,
310
+ menusLastMonth,
311
+ ] = await Promise.all([
312
+ this.prismaService.menu.count(),
313
+ this.prismaService.route.count(),
314
+ this.prismaService.menu.count({ where: { created_at: { gte: currentMonthStart } } }),
315
+ this.prismaService.menu.count({ where: { created_at: { gte: lastMonthStart, lte: lastMonthEnd } } }),
316
+ ]);
317
+
318
+ const menusChange = this.calculateChange(menusCurrentMonth, menusLastMonth);
319
+
320
+ return {
321
+ cards: {
322
+ menus: {
323
+ value: menusCount,
324
+ change: menusChange,
325
+ },
326
+ routes: {
327
+ value: routesCount,
328
+ change: null,
329
+ },
330
+ },
331
+ };
332
+ }
333
+
300
334
  async getUserLayout(userId: number, slug: string, localeCode: string) {
301
335
  const dashboard = await this.prismaService.dashboard.findFirst({ where: { slug } });
302
336
 
@@ -355,7 +355,7 @@ export class ProfileService {
355
355
  const passwordHash = await this.security.hashArgon2(newPassword);
356
356
  await this.prisma.user_credential.update({
357
357
  where: { id: credential.id },
358
- data: { hash: passwordHash },
358
+ data: { hash: passwordHash, requires_reset: false },
359
359
  });
360
360
 
361
361
  // Get user data and email for notification
@@ -2,13 +2,13 @@ import { IS_PUBLIC_KEY, WITH_ROLE } from '@hed-hog/api';
2
2
  import { getLocaleText } from '@hed-hog/api-locale';
3
3
  import { PrismaService } from '@hed-hog/api-prisma';
4
4
  import {
5
- CanActivate,
6
- ExecutionContext,
7
- forwardRef,
8
- Inject,
9
- Injectable,
10
- RequestMethod,
11
- UnauthorizedException,
5
+ CanActivate,
6
+ ExecutionContext,
7
+ forwardRef,
8
+ Inject,
9
+ Injectable,
10
+ RequestMethod,
11
+ UnauthorizedException,
12
12
  } from '@nestjs/common';
13
13
  import { METHOD_METADATA } from '@nestjs/common/constants';
14
14
  import { Reflector } from '@nestjs/core';
@@ -16,6 +16,11 @@ import { Request } from 'express';
16
16
 
17
17
  @Injectable()
18
18
  export class RoleGuard implements CanActivate {
19
+ private readonly forcePasswordResetAllowedRoutes = new Set([
20
+ 'PUT /profile/change-password',
21
+ 'GET /auth/verify',
22
+ ]);
23
+
19
24
  constructor(
20
25
  private reflector: Reflector,
21
26
  @Inject(forwardRef(() => PrismaService))
@@ -128,6 +133,30 @@ export class RoleGuard implements CanActivate {
128
133
  throw new UnauthorizedException(message);
129
134
  }
130
135
 
136
+ const hasPendingPasswordReset = await this.prisma.user_credential.findFirst({
137
+ where: {
138
+ user_id: userId,
139
+ type: 'password',
140
+ requires_reset: true,
141
+ revoked_at: null,
142
+ },
143
+ select: { id: true },
144
+ });
145
+
146
+ if (
147
+ hasPendingPasswordReset &&
148
+ !this.forcePasswordResetAllowedRoutes.has(`${httpMethod} ${fullPath}`)
149
+ ) {
150
+ const locale = request['locale'] || 'en';
151
+ throw new UnauthorizedException(
152
+ getLocaleText(
153
+ 'profile.changePassword.required',
154
+ locale,
155
+ 'Password update required before accessing this resource.',
156
+ ),
157
+ );
158
+ }
159
+
131
160
  return true;
132
161
  }
133
162
 
@@ -2,7 +2,7 @@ import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
3
3
  import { PrismaService } from '@hed-hog/api-prisma';
4
4
  import { HttpService } from '@nestjs/axios';
5
- import { BadRequestException, forwardRef, HttpException, HttpStatus, Inject, Injectable, NotFoundException } from '@nestjs/common';
5
+ import { BadRequestException, forwardRef, HttpException, HttpStatus, Inject, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
6
6
  import { firstValueFrom } from 'rxjs';
7
7
  import { SecurityService } from '../security/security.service';
8
8
  import { SettingService } from '../setting/setting.service';
@@ -88,7 +88,7 @@ export class SessionService {
88
88
  });
89
89
 
90
90
  if (!session) {
91
- throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
91
+ throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
92
92
  }
93
93
 
94
94
  await this.prisma.user_session.update({
@@ -0,0 +1,11 @@
1
+ import { getLocaleText } from '@hed-hog/api-locale';
2
+ import { IsOptional } from 'class-validator';
3
+ import { IsStrongPasswordWithSettings } from '../../validators/is-strong-password-with-settings.validator';
4
+
5
+ export class ResetPasswordDTO {
6
+ @IsOptional()
7
+ @IsStrongPasswordWithSettings({
8
+ message: (args) => getLocaleText('validation.passwordStrength', args.value),
9
+ })
10
+ password?: string;
11
+ }
@@ -2,24 +2,25 @@ import { Public, 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
- BadRequestException,
6
- Body,
7
- Controller,
8
- Delete,
9
- Get,
10
- Inject,
11
- Param,
12
- ParseIntPipe,
13
- Patch,
14
- Post,
15
- Res,
16
- UploadedFile,
17
- UseInterceptors,
18
- forwardRef,
5
+ BadRequestException,
6
+ Body,
7
+ Controller,
8
+ Delete,
9
+ Get,
10
+ Inject,
11
+ Param,
12
+ ParseIntPipe,
13
+ Patch,
14
+ Post,
15
+ Res,
16
+ UploadedFile,
17
+ UseInterceptors,
18
+ forwardRef,
19
19
  } from '@nestjs/common';
20
20
  import { FileInterceptor } from '@nestjs/platform-express';
21
21
  import { DeleteDTO } from '../dto/delete.dto';
22
22
  import { CreateWithEmailAndPasswordDTO } from './dto/create-with-email-and-password.dto';
23
+ import { ResetPasswordDTO } from './dto/reset-password.dto';
23
24
  import { UpdateDTO } from './dto/update.dto';
24
25
  import { UserService } from './user.service';
25
26
 
@@ -65,6 +66,15 @@ export class UserController {
65
66
  return this.userService.update(locale, userId, data);
66
67
  }
67
68
 
69
+ @Patch(':userId/reset-password')
70
+ async resetPassword(
71
+ @Param('userId', ParseIntPipe) userId: number,
72
+ @Body() data: ResetPasswordDTO,
73
+ @Locale() locale: string,
74
+ ) {
75
+ return this.userService.resetPassword(locale, userId, data);
76
+ }
77
+
68
78
  @UseInterceptors(
69
79
  FileInterceptor('avatar', {
70
80
  fileFilter: (req, file, cb) => {