@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.
- package/README.md +60 -0
- package/dist/auth/auth.controller.d.ts +8 -1
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +7 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +10 -1
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +34 -8
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +25 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/profile/profile.service.js +1 -1
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/role/guards/role.guard.d.ts +1 -0
- package/dist/role/guards/role.guard.d.ts.map +1 -1
- package/dist/role/guards/role.guard.js +18 -0
- package/dist/role/guards/role.guard.js.map +1 -1
- package/dist/session/session.service.js +1 -1
- package/dist/session/session.service.js.map +1 -1
- package/dist/user/dto/reset-password.dto.d.ts +4 -0
- package/dist/user/dto/reset-password.dto.d.ts.map +1 -0
- package/dist/user/dto/reset-password.dto.js +26 -0
- package/dist/user/dto/reset-password.dto.js.map +1 -0
- package/dist/user/user.controller.d.ts +5 -0
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +13 -0
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +6 -0
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +65 -0
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/dashboard_component.yaml +74 -12
- package/hedhog/data/dashboard_component_role.yaml +223 -145
- package/hedhog/data/dashboard_item.yaml +42 -22
- package/hedhog/data/dashboard_role.yaml +18 -12
- package/hedhog/data/menu.yaml +6 -0
- package/hedhog/data/route.yaml +65 -1
- package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +17 -17
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +23 -12
- package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +80 -5
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +17 -13
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +16 -12
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +27 -16
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +13 -9
- package/hedhog/frontend/app/dashboard/components/widgets/menus-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
- package/hedhog/frontend/app/dashboard/components/widgets/routes-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +15 -11
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +18 -15
- package/hedhog/frontend/app/dashboard/dashboard.css.ejs +20 -4
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +5 -11
- package/hedhog/frontend/app/users/page.tsx.ejs +331 -10
- package/hedhog/frontend/messages/en.json +29 -3
- package/hedhog/frontend/messages/pt.json +29 -3
- package/package.json +4 -4
- package/src/auth/auth.controller.ts +21 -20
- package/src/auth/auth.service.ts +63 -15
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +34 -0
- package/src/profile/profile.service.ts +1 -1
- package/src/role/guards/role.guard.ts +36 -7
- package/src/session/session.service.ts +2 -2
- package/src/user/dto/reset-password.dto.ts +11 -0
- package/src/user/user.controller.ts +24 -14
- 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
|
|
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
|
|
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.
|
|
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.
|
|
38
|
-
"@hed-hog/api
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
219
|
+
throw new UnauthorizedException('Refresh token not provided');
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
await this.service.logout(res, req, refreshToken);
|
package/src/auth/auth.service.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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) => {
|