@hed-hog/core 0.0.276 → 0.0.278
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 +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/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 +77 -33
- package/hedhog/data/dashboard_component_role.yaml +132 -66
- package/hedhog/data/dashboard_item.yaml +100 -100
- package/hedhog/data/dashboard_role.yaml +18 -12
- package/hedhog/data/menu.yaml +6 -0
- package/hedhog/data/route.yaml +57 -1
- package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +24 -24
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +4 -4
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +23 -19
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +15 -14
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +3 -3
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +34 -33
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
- package/hedhog/frontend/app/users/page.tsx.ejs +322 -1
- package/hedhog/frontend/messages/en.json +19 -1
- package/hedhog/frontend/messages/pt.json +19 -1
- package/package.json +4 -4
- package/src/auth/auth.controller.ts +21 -20
- package/src/auth/auth.service.ts +63 -15
- 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"
|
|
@@ -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"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.278",
|
|
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-prisma": "0.0.5",
|
|
36
36
|
"@hed-hog/api-mail": "0.0.8",
|
|
37
|
-
"@hed-hog/api": "0.0.
|
|
38
|
-
"@hed-hog/api
|
|
37
|
+
"@hed-hog/api-locale": "0.0.13",
|
|
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
|
}
|
|
@@ -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) => {
|
package/src/user/user.service.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { DeleteDTO } from '../dto/delete.dto';
|
|
|
13
13
|
import { FileService } from '../file/file.service';
|
|
14
14
|
import { SecurityService } from '../security/security.service';
|
|
15
15
|
import { CreateWithEmailAndPasswordDTO } from './dto/create-with-email-and-password.dto';
|
|
16
|
+
import { ResetPasswordDTO } from './dto/reset-password.dto';
|
|
16
17
|
import { UpdateDTO } from './dto/update.dto';
|
|
17
18
|
|
|
18
19
|
// Constants
|
|
@@ -28,6 +29,14 @@ const DEFAULT_ROLE_SLUG = 'user';
|
|
|
28
29
|
const DEFAULT_LOCALE = 'en';
|
|
29
30
|
const DAYS_IN_MS = 24 * 60 * 60 * 1000;
|
|
30
31
|
const NEW_USERS_PERIOD_DAYS = 7;
|
|
32
|
+
const RANDOM_PASSWORD_LENGTH = 16;
|
|
33
|
+
|
|
34
|
+
const PASSWORD_CHARSETS = {
|
|
35
|
+
lowercase: 'abcdefghijkmnopqrstuvwxyz',
|
|
36
|
+
uppercase: 'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
|
37
|
+
numbers: '23456789',
|
|
38
|
+
symbols: '@#$%&*!?-_+',
|
|
39
|
+
} as const;
|
|
31
40
|
|
|
32
41
|
const USER_SORT_FIELDS = [
|
|
33
42
|
'id',
|
|
@@ -197,6 +206,46 @@ export class UserService {
|
|
|
197
206
|
});
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
async resetPassword(
|
|
210
|
+
locale: string,
|
|
211
|
+
userId: number,
|
|
212
|
+
{ password }: ResetPasswordDTO,
|
|
213
|
+
) {
|
|
214
|
+
await this.validateUserExists(locale, userId);
|
|
215
|
+
|
|
216
|
+
const nextPassword = password || this.generateRandomPassword();
|
|
217
|
+
const passwordHash = await this.security.hashArgon2(nextPassword);
|
|
218
|
+
|
|
219
|
+
const updateResult = await this.prismaService.user_credential.updateMany({
|
|
220
|
+
where: {
|
|
221
|
+
user_id: userId,
|
|
222
|
+
type: CREDENTIAL_TYPE.PASSWORD,
|
|
223
|
+
},
|
|
224
|
+
data: {
|
|
225
|
+
hash: passwordHash,
|
|
226
|
+
requires_reset: true,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (updateResult.count === 0) {
|
|
231
|
+
await this.prismaService.user_credential.create({
|
|
232
|
+
data: {
|
|
233
|
+
user_id: userId,
|
|
234
|
+
type: CREDENTIAL_TYPE.PASSWORD,
|
|
235
|
+
hash: passwordHash,
|
|
236
|
+
requires_reset: true,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await this.registerUserActivity(userId, 'resetPassword');
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
password: nextPassword,
|
|
245
|
+
requiresReset: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
200
249
|
async delete(locale: string, { ids }: DeleteDTO) {
|
|
201
250
|
this.validateDeleteIds(locale, ids);
|
|
202
251
|
|
|
@@ -424,6 +473,41 @@ export class UserService {
|
|
|
424
473
|
return role;
|
|
425
474
|
}
|
|
426
475
|
|
|
476
|
+
private generateRandomPassword(length = RANDOM_PASSWORD_LENGTH) {
|
|
477
|
+
const cryptoObj = globalThis.crypto;
|
|
478
|
+
const groups = [
|
|
479
|
+
PASSWORD_CHARSETS.lowercase,
|
|
480
|
+
PASSWORD_CHARSETS.uppercase,
|
|
481
|
+
PASSWORD_CHARSETS.numbers,
|
|
482
|
+
PASSWORD_CHARSETS.symbols,
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
const allChars = groups.join('');
|
|
486
|
+
const values = new Uint32Array(length + groups.length);
|
|
487
|
+
cryptoObj.getRandomValues(values);
|
|
488
|
+
|
|
489
|
+
const passwordChars: string[] = [];
|
|
490
|
+
|
|
491
|
+
groups.forEach((group, index) => {
|
|
492
|
+
const randomIndex = values[index] % group.length;
|
|
493
|
+
passwordChars.push(group[randomIndex]);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
for (let i = groups.length; i < values.length; i++) {
|
|
497
|
+
const randomIndex = values[i] % allChars.length;
|
|
498
|
+
passwordChars.push(allChars[randomIndex]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
for (let i = passwordChars.length - 1; i > 0; i--) {
|
|
502
|
+
const randomIndex = values[i] % (i + 1);
|
|
503
|
+
const temp = passwordChars[i];
|
|
504
|
+
passwordChars[i] = passwordChars[randomIndex];
|
|
505
|
+
passwordChars[randomIndex] = temp;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return passwordChars.slice(0, length).join('');
|
|
509
|
+
}
|
|
510
|
+
|
|
427
511
|
private getUserIncludeClause() {
|
|
428
512
|
return {
|
|
429
513
|
user_account: true,
|