@hed-hog/core 0.0.385 → 0.0.386
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/app-command/app-command.registry.d.ts +2 -0
- package/dist/app-command/app-command.registry.d.ts.map +1 -1
- package/dist/app-command/app-command.registry.js.map +1 -1
- package/dist/auth/auth.controller.d.ts +22 -0
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +27 -0
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +50 -0
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +108 -0
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/dto/complete-account.dto.d.ts +9 -0
- package/dist/auth/dto/complete-account.dto.d.ts.map +1 -0
- package/dist/auth/dto/complete-account.dto.js +43 -0
- package/dist/auth/dto/complete-account.dto.js.map +1 -0
- package/dist/authorize/authorize.controller.d.ts +45 -0
- package/dist/authorize/authorize.controller.d.ts.map +1 -0
- package/dist/authorize/authorize.controller.js +120 -0
- package/dist/authorize/authorize.controller.js.map +1 -0
- package/dist/authorize/authorize.module.d.ts +8 -0
- package/dist/authorize/authorize.module.d.ts.map +1 -0
- package/dist/authorize/authorize.module.js +36 -0
- package/dist/authorize/authorize.module.js.map +1 -0
- package/dist/authorize/authorize.service.d.ts +79 -0
- package/dist/authorize/authorize.service.d.ts.map +1 -0
- package/dist/authorize/authorize.service.js +196 -0
- package/dist/authorize/authorize.service.js.map +1 -0
- package/dist/authorize/dto/authorize-info.dto.d.ts +8 -0
- package/dist/authorize/dto/authorize-info.dto.d.ts.map +1 -0
- package/dist/authorize/dto/authorize-info.dto.js +42 -0
- package/dist/authorize/dto/authorize-info.dto.js.map +1 -0
- package/dist/authorize/dto/consent.dto.d.ts +8 -0
- package/dist/authorize/dto/consent.dto.d.ts.map +1 -0
- package/dist/authorize/dto/consent.dto.js +42 -0
- package/dist/authorize/dto/consent.dto.js.map +1 -0
- package/dist/authorize/dto/token.dto.d.ts +8 -0
- package/dist/authorize/dto/token.dto.d.ts.map +1 -0
- package/dist/authorize/dto/token.dto.js +42 -0
- package/dist/authorize/dto/token.dto.js.map +1 -0
- package/dist/challenge/challenge.service.d.ts +7 -0
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +22 -0
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +2 -0
- package/dist/core.module.js.map +1 -1
- package/dist/file/file.service.d.ts +7 -0
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +16 -0
- package/dist/file/file.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts +6 -2
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +11 -2
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +18 -2
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +10 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +24 -1
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
- package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.d.ts +26 -0
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +94 -0
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +39 -1
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +115 -7
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +37 -6
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +1 -1
- package/dist/profile/profile.service.d.ts +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +7 -1
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +8 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/mail.yaml +39 -0
- package/hedhog/data/route.yaml +12 -0
- package/hedhog/data/setting_group.yaml +15 -1
- package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +11 -7
- package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +9 -7
- package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +6 -2
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +130 -34
- package/hedhog/frontend/messages/en.json +33 -1
- package/hedhog/frontend/messages/pt.json +33 -1
- package/package.json +10 -5
- package/src/app-command/app-command.registry.ts +2 -0
- package/src/auth/auth.controller.ts +21 -0
- package/src/auth/auth.service.ts +133 -0
- package/src/auth/dto/complete-account.dto.ts +24 -0
- package/src/authorize/authorize.controller.ts +99 -0
- package/src/authorize/authorize.module.ts +23 -0
- package/src/authorize/authorize.service.spec.ts +307 -0
- package/src/authorize/authorize.service.ts +245 -0
- package/src/authorize/dto/authorize-info.dto.ts +23 -0
- package/src/authorize/dto/consent.dto.ts +23 -0
- package/src/authorize/dto/token.dto.ts +23 -0
- package/src/challenge/challenge.service.ts +28 -0
- package/src/core.module.ts +2 -0
- package/src/file/file.service.ts +19 -0
- package/src/integration-profile/integration-profile.secrets.spec.ts +114 -0
- package/src/integration-profile/integration-profile.secrets.ts +11 -2
- package/src/integration-profile/integration-profile.service.ts +19 -0
- package/src/integration-profile/integration-profile.utils.ts +40 -0
- package/src/oauth/interfaces/OAuthProvider.ts +5 -1
- package/src/oauth/oauth.controller.ts +89 -0
- package/src/oauth/oauth.service.ts +190 -8
- package/src/oauth/providers/microsoft-entra-id.provider.ts +45 -5
- package/src/webhook-integration/webhook-integration.service.ts +18 -2
|
@@ -1399,6 +1399,7 @@
|
|
|
1399
1399
|
"shareDashboardError": "Could not share the dashboard.",
|
|
1400
1400
|
"shareRemoved": "Sharing removed.",
|
|
1401
1401
|
"revokeShareError": "Could not remove the sharing.",
|
|
1402
|
+
"tabActions": "Tab actions",
|
|
1402
1403
|
"addWidgets": "Add widgets",
|
|
1403
1404
|
"setAsHome": "Set as home",
|
|
1404
1405
|
"rename": "Rename",
|
|
@@ -2425,6 +2426,7 @@
|
|
|
2425
2426
|
"fieldAccessKeyId": "Access Key ID",
|
|
2426
2427
|
"fieldSecretAccessKey": "Secret Access Key",
|
|
2427
2428
|
"fieldRegion": "Region",
|
|
2429
|
+
"fieldDomain": "Domain",
|
|
2428
2430
|
"fieldToken": "Token",
|
|
2429
2431
|
"fieldInstanceName": "Instance Name",
|
|
2430
2432
|
"fieldPhoneNumberId": "Phone Number ID",
|
|
@@ -2481,6 +2483,36 @@
|
|
|
2481
2483
|
"nodePoolPlaceholder": "Select a node pool",
|
|
2482
2484
|
"nodePoolNoneFound": "No node pools found for this cluster.",
|
|
2483
2485
|
"nodePoolLoadError": "Failed to load node pools. Check the API token and cluster ID.",
|
|
2484
|
-
"nodePoolNeedCreds": "Fill in the API token and cluster ID first, then load the pools."
|
|
2486
|
+
"nodePoolNeedCreds": "Fill in the API token and cluster ID first, then load the pools.",
|
|
2487
|
+
"fieldEnvironment": "Environment",
|
|
2488
|
+
"optionHomologation": "Homologation (testing)",
|
|
2489
|
+
"optionProduction": "Production (real invoices)",
|
|
2490
|
+
"fieldTaxId": "Issuer tax ID (CNPJ)",
|
|
2491
|
+
"fieldLegalName": "Legal name",
|
|
2492
|
+
"fieldTradeName": "Trade name",
|
|
2493
|
+
"fieldMunicipalRegistration": "Municipal registration",
|
|
2494
|
+
"fieldIbgeCityCode": "IBGE city code",
|
|
2495
|
+
"fieldAddressStreet": "Street",
|
|
2496
|
+
"fieldAddressNumber": "Number",
|
|
2497
|
+
"fieldAddressComplement": "Complement",
|
|
2498
|
+
"fieldAddressDistrict": "District",
|
|
2499
|
+
"fieldAddressZip": "ZIP code",
|
|
2500
|
+
"fieldAddressState": "State",
|
|
2501
|
+
"fieldEmail": "Email",
|
|
2502
|
+
"fieldPhone": "Phone",
|
|
2503
|
+
"fieldTaxRegime": "Tax regime",
|
|
2504
|
+
"optionSimplesNacional": "Simples Nacional",
|
|
2505
|
+
"optionSimplesNacionalExcesso": "Simples Nacional (over the cap)",
|
|
2506
|
+
"optionRegimeNormal": "Regular regime",
|
|
2507
|
+
"optionMei": "MEI",
|
|
2508
|
+
"fieldSpecialTaxRegime": "Special tax regime",
|
|
2509
|
+
"fieldCulturalIncentive": "Cultural incentive",
|
|
2510
|
+
"fieldCertificatePfx": "A1 certificate (.pfx/.p12)",
|
|
2511
|
+
"fieldCertificatePassword": "Certificate password",
|
|
2512
|
+
"fieldBaseUrl": "API base URL",
|
|
2513
|
+
"fieldTimeoutMs": "Timeout (ms)",
|
|
2514
|
+
"fieldMaxRetries": "Max retries",
|
|
2515
|
+
"fileStored": "A file is stored and encrypted. Click to replace it.",
|
|
2516
|
+
"fileSelected": "New file selected. It will be encrypted on save."
|
|
2485
2517
|
}
|
|
2486
2518
|
}
|
|
@@ -1402,6 +1402,7 @@
|
|
|
1402
1402
|
"shareDashboardError": "Não foi possível compartilhar o dashboard.",
|
|
1403
1403
|
"shareRemoved": "Compartilhamento removido.",
|
|
1404
1404
|
"revokeShareError": "Não foi possível remover o compartilhamento.",
|
|
1405
|
+
"tabActions": "Ações da aba",
|
|
1405
1406
|
"addWidgets": "Adicionar widgets",
|
|
1406
1407
|
"setAsHome": "Definir como home",
|
|
1407
1408
|
"rename": "Renomear",
|
|
@@ -2480,6 +2481,7 @@
|
|
|
2480
2481
|
"fieldAccessKeyId": "Access Key ID",
|
|
2481
2482
|
"fieldSecretAccessKey": "Secret Access Key",
|
|
2482
2483
|
"fieldRegion": "Região",
|
|
2484
|
+
"fieldDomain": "Domínio",
|
|
2483
2485
|
"fieldToken": "Token",
|
|
2484
2486
|
"fieldInstanceName": "Nome da Instância",
|
|
2485
2487
|
"fieldPhoneNumberId": "ID do Número de Telefone",
|
|
@@ -2536,6 +2538,36 @@
|
|
|
2536
2538
|
"nodePoolPlaceholder": "Selecione um node pool",
|
|
2537
2539
|
"nodePoolNoneFound": "Nenhum node pool encontrado para este cluster.",
|
|
2538
2540
|
"nodePoolLoadError": "Falha ao carregar os node pools. Verifique o token da API e o ID do cluster.",
|
|
2539
|
-
"nodePoolNeedCreds": "Preencha o token da API e o ID do cluster primeiro, depois carregue os pools."
|
|
2541
|
+
"nodePoolNeedCreds": "Preencha o token da API e o ID do cluster primeiro, depois carregue os pools.",
|
|
2542
|
+
"fieldEnvironment": "Ambiente",
|
|
2543
|
+
"optionHomologation": "Homologação (testes)",
|
|
2544
|
+
"optionProduction": "Produção (notas reais)",
|
|
2545
|
+
"fieldTaxId": "CNPJ do emitente",
|
|
2546
|
+
"fieldLegalName": "Razão social",
|
|
2547
|
+
"fieldTradeName": "Nome fantasia",
|
|
2548
|
+
"fieldMunicipalRegistration": "Inscrição municipal",
|
|
2549
|
+
"fieldIbgeCityCode": "Código IBGE do município",
|
|
2550
|
+
"fieldAddressStreet": "Logradouro",
|
|
2551
|
+
"fieldAddressNumber": "Número",
|
|
2552
|
+
"fieldAddressComplement": "Complemento",
|
|
2553
|
+
"fieldAddressDistrict": "Bairro",
|
|
2554
|
+
"fieldAddressZip": "CEP",
|
|
2555
|
+
"fieldAddressState": "Estado",
|
|
2556
|
+
"fieldEmail": "E-mail",
|
|
2557
|
+
"fieldPhone": "Telefone",
|
|
2558
|
+
"fieldTaxRegime": "Regime tributário",
|
|
2559
|
+
"optionSimplesNacional": "Simples Nacional",
|
|
2560
|
+
"optionSimplesNacionalExcesso": "Simples Nacional (excesso de sublimite)",
|
|
2561
|
+
"optionRegimeNormal": "Regime normal",
|
|
2562
|
+
"optionMei": "MEI",
|
|
2563
|
+
"fieldSpecialTaxRegime": "Regime especial de tributação",
|
|
2564
|
+
"fieldCulturalIncentive": "Incentivador cultural",
|
|
2565
|
+
"fieldCertificatePfx": "Certificado A1 (.pfx/.p12)",
|
|
2566
|
+
"fieldCertificatePassword": "Senha do certificado",
|
|
2567
|
+
"fieldBaseUrl": "URL base da API",
|
|
2568
|
+
"fieldTimeoutMs": "Timeout (ms)",
|
|
2569
|
+
"fieldMaxRetries": "Máximo de tentativas",
|
|
2570
|
+
"fileStored": "Arquivo armazenado e cifrado. Clique para substituir.",
|
|
2571
|
+
"fileSelected": "Novo arquivo selecionado. Será cifrado ao salvar."
|
|
2540
2572
|
}
|
|
2541
2573
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.386",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"speakeasy": "^2.0.0",
|
|
42
42
|
"ua-parser-js": "^2.0.10",
|
|
43
43
|
"uuid": "^11.1.0",
|
|
44
|
-
"@hed-hog/api-
|
|
45
|
-
"@hed-hog/api-locale": "0.0.14",
|
|
44
|
+
"@hed-hog/api-mail": "0.0.9",
|
|
46
45
|
"@hed-hog/api-types": "0.0.1",
|
|
46
|
+
"@hed-hog/api-prisma": "0.0.6",
|
|
47
47
|
"@hed-hog/api": "0.0.8",
|
|
48
|
-
"@hed-hog/api-
|
|
48
|
+
"@hed-hog/api-locale": "0.0.14",
|
|
49
49
|
"@hed-hog/api-pagination": "0.0.7"
|
|
50
50
|
},
|
|
51
51
|
"exports": {
|
|
@@ -77,13 +77,18 @@
|
|
|
77
77
|
"hedhog"
|
|
78
78
|
],
|
|
79
79
|
"devDependencies": {
|
|
80
|
+
"@jest/globals": "^29.7.0",
|
|
80
81
|
"@types/cookie-parser": "^1.4.10",
|
|
81
82
|
"@types/express": "^4.17.24",
|
|
82
83
|
"@types/geoip-lite": "^1.4.4",
|
|
84
|
+
"@types/jest": "^29.5.14",
|
|
83
85
|
"@types/multer": "^2.0.0",
|
|
84
86
|
"@types/pako": "^2.0.4",
|
|
85
87
|
"@types/ua-parser-js": "^0.7.39",
|
|
86
|
-
"
|
|
88
|
+
"jest": "^29.7.0",
|
|
89
|
+
"rimraf": "^6.1.0",
|
|
90
|
+
"ts-jest": "^29.2.5",
|
|
91
|
+
"@hed-hog/jest-config": "0.0.0"
|
|
87
92
|
},
|
|
88
93
|
"scripts": {
|
|
89
94
|
"_dev": "pnpm build --watch",
|
|
@@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
|
|
|
3
3
|
export interface AppCommandContext extends Record<string, unknown> {
|
|
4
4
|
webhookBody?: unknown;
|
|
5
5
|
webhookHeaders?: Record<string, unknown>;
|
|
6
|
+
/** Raw pre-parse request body, needed for byte-exact HMAC signature verification (e.g. Stripe). Undefined on replayed/retried calls. */
|
|
7
|
+
webhookRawBody?: Buffer;
|
|
6
8
|
remoteIp?: string | null;
|
|
7
9
|
publicUuid?: string;
|
|
8
10
|
integrationId?: number;
|
|
@@ -28,6 +28,7 @@ import { LoginWithRecoveryCodeDTO } from './dto/login-with-recovery-code';
|
|
|
28
28
|
import { LoginDTO } from './dto/login.dto';
|
|
29
29
|
import { ResendMfaCodeDTO } from './dto/resend-mfa-code.dto';
|
|
30
30
|
import { ResetDTO } from './dto/reset.dto';
|
|
31
|
+
import { CompleteAccountDTO, VerifyAccountInviteDTO } from './dto/complete-account.dto';
|
|
31
32
|
|
|
32
33
|
@Controller('auth')
|
|
33
34
|
export class AuthController {
|
|
@@ -269,4 +270,24 @@ export class AuthController {
|
|
|
269
270
|
) {
|
|
270
271
|
return this.service.forgotResetPassword(locale, password, code, ipAddress, userAgent);
|
|
271
272
|
}
|
|
273
|
+
|
|
274
|
+
@Public()
|
|
275
|
+
@Post('complete-account/verify')
|
|
276
|
+
async verifyAccountInvite(
|
|
277
|
+
@Locale() locale: string,
|
|
278
|
+
@Body() { code }: VerifyAccountInviteDTO,
|
|
279
|
+
) {
|
|
280
|
+
return this.service.verifyAccountInvite(locale, code);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@Public()
|
|
284
|
+
@Post('complete-account')
|
|
285
|
+
async completeAccount(
|
|
286
|
+
@Locale() locale: string,
|
|
287
|
+
@Body() { password, code, name }: CompleteAccountDTO,
|
|
288
|
+
@Ip() ipAddress: string,
|
|
289
|
+
@Headers('user-agent') userAgent: string,
|
|
290
|
+
) {
|
|
291
|
+
return this.service.completeAccount(locale, { password, code, name }, ipAddress, userAgent);
|
|
292
|
+
}
|
|
272
293
|
}
|
package/src/auth/auth.service.ts
CHANGED
|
@@ -558,6 +558,139 @@ export class AuthService {
|
|
|
558
558
|
return { accessToken, refreshToken, session }
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Garante que exista um usuário com o identificador de email informado e cria
|
|
563
|
+
* um challenge seguro de criação de conta (link de convite). Não envia email —
|
|
564
|
+
* o remetente (ex.: LMS enterprise) monta o link e dispara o template. Reutiliza
|
|
565
|
+
* um usuário existente por `userId` ou pelo identificador de email; caso não
|
|
566
|
+
* exista, cria um novo usuário apenas com o nome.
|
|
567
|
+
*/
|
|
568
|
+
async prepareAccountInvite(
|
|
569
|
+
locale: string,
|
|
570
|
+
{ email, name, userId }: { email: string; name?: string; userId?: number },
|
|
571
|
+
) {
|
|
572
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
573
|
+
|
|
574
|
+
let targetUserId = userId;
|
|
575
|
+
if (!targetUserId) {
|
|
576
|
+
const existing = await this.user.findUserByEmail(locale, normalizedEmail);
|
|
577
|
+
targetUserId = existing?.id;
|
|
578
|
+
}
|
|
579
|
+
if (!targetUserId) {
|
|
580
|
+
const createdUser = await this.prisma.user.create({
|
|
581
|
+
data: { name: name?.trim() || normalizedEmail },
|
|
582
|
+
});
|
|
583
|
+
targetUserId = createdUser.id;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let identifier = await this.prisma.user_identifier.findFirst({
|
|
587
|
+
where: { user_id: targetUserId, type: 'email', value: normalizedEmail },
|
|
588
|
+
});
|
|
589
|
+
if (!identifier) {
|
|
590
|
+
identifier = await this.prisma.user_identifier.create({
|
|
591
|
+
data: {
|
|
592
|
+
user_id: targetUserId,
|
|
593
|
+
type: 'email',
|
|
594
|
+
value: normalizedEmail,
|
|
595
|
+
enabled: true,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
} else if (!identifier.enabled) {
|
|
599
|
+
identifier = await this.prisma.user_identifier.update({
|
|
600
|
+
where: { id: identifier.id },
|
|
601
|
+
data: { enabled: true },
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const passwordCount = await this.prisma.user_credential.count({
|
|
606
|
+
where: {
|
|
607
|
+
user_id: targetUserId,
|
|
608
|
+
type: 'password',
|
|
609
|
+
revoked_at: null,
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const token = await this.challenge.createAccountInviteChallenge(identifier.id);
|
|
614
|
+
|
|
615
|
+
return { userId: targetUserId, token, hasPassword: passwordCount > 0 };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Valida (sem consumir) um token de convite de criação de conta e retorna os
|
|
620
|
+
* dados básicos para pré-preencher o formulário no app de treinamentos.
|
|
621
|
+
*/
|
|
622
|
+
async verifyAccountInvite(locale: string, code: string) {
|
|
623
|
+
const challenge = await this.challenge.verifyCodePassword(locale, code);
|
|
624
|
+
return {
|
|
625
|
+
valid: true,
|
|
626
|
+
name: challenge.user_identifier.user?.name ?? null,
|
|
627
|
+
email: challenge.user_identifier.value,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Conclui a criação de conta a partir de um token de convite: cria/atualiza a
|
|
633
|
+
* credencial de senha, marca o identificador como verificado, atualiza o nome
|
|
634
|
+
* (se enviado), consome o challenge e autentica o usuário (auto-login).
|
|
635
|
+
*/
|
|
636
|
+
async completeAccount(
|
|
637
|
+
locale: string,
|
|
638
|
+
{ password, code, name }: { password: string; code: string; name?: string },
|
|
639
|
+
ipAddress: string,
|
|
640
|
+
userAgent: string,
|
|
641
|
+
) {
|
|
642
|
+
const challenge = await this.challenge.verifyCodePassword(locale, code);
|
|
643
|
+
const userIdentifier = challenge.user_identifier;
|
|
644
|
+
const passwordHash = await this.security.hashArgon2(password);
|
|
645
|
+
|
|
646
|
+
const existingCredential = await this.prisma.user_credential.findFirst({
|
|
647
|
+
where: { user_id: userIdentifier.user_id, type: 'password' },
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
if (existingCredential) {
|
|
651
|
+
await this.prisma.user_credential.updateMany({
|
|
652
|
+
where: { user_id: userIdentifier.user_id, type: 'password' },
|
|
653
|
+
data: { hash: passwordHash, requires_reset: false, revoked_at: null },
|
|
654
|
+
});
|
|
655
|
+
} else {
|
|
656
|
+
await this.prisma.user_credential.create({
|
|
657
|
+
data: {
|
|
658
|
+
user_id: userIdentifier.user_id,
|
|
659
|
+
type: 'password',
|
|
660
|
+
hash: passwordHash,
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
await this.prisma.user_identifier.update({
|
|
666
|
+
where: { id: userIdentifier.id },
|
|
667
|
+
data: { verified_at: new Date(), enabled: true },
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (name?.trim()) {
|
|
671
|
+
await this.prisma.user.update({
|
|
672
|
+
where: { id: userIdentifier.user_id },
|
|
673
|
+
data: { name: name.trim() },
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Consome o challenge para impedir reuso do link (invites têm validade longa).
|
|
678
|
+
await this.prisma.user_identifier_challenge.updateMany({
|
|
679
|
+
where: { id: challenge.id },
|
|
680
|
+
data: { verified_at: new Date(), expires_at: new Date() },
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const { accessToken, refreshToken, session } =
|
|
684
|
+
await this.getAuthenticationPayload(
|
|
685
|
+
locale,
|
|
686
|
+
userIdentifier.user_id,
|
|
687
|
+
ipAddress,
|
|
688
|
+
userAgent,
|
|
689
|
+
);
|
|
690
|
+
await this.user.registerUserActivity(userIdentifier.id, 'completeAccount');
|
|
691
|
+
return { accessToken, refreshToken, session };
|
|
692
|
+
}
|
|
693
|
+
|
|
561
694
|
async verifyMfaCode(locale: string, mfaToken: string, code: string, ipAddress: string, userAgent: string, methodType?: 'totp' | 'email' | 'recovery', browserId?: string | null) {
|
|
562
695
|
const payload = await this.token.verifyMfaChallengeToken(mfaToken);
|
|
563
696
|
const user = await this.prisma.user.findUnique({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
|
+
import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
export class CompleteAccountDTO {
|
|
5
|
+
@IsNotEmpty({
|
|
6
|
+
message: (args) => getLocaleText('reset.password.required', args.value),
|
|
7
|
+
})
|
|
8
|
+
@MinLength(8, {
|
|
9
|
+
message: (args) => getLocaleText('reset.password.minLength', args.value),
|
|
10
|
+
})
|
|
11
|
+
password: string;
|
|
12
|
+
|
|
13
|
+
@IsNotEmpty({ message: (args) => getLocaleText('reset.code.required', args.value) })
|
|
14
|
+
code: string;
|
|
15
|
+
|
|
16
|
+
@IsOptional()
|
|
17
|
+
@IsString()
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class VerifyAccountInviteDTO {
|
|
22
|
+
@IsNotEmpty({ message: (args) => getLocaleText('reset.code.required', args.value) })
|
|
23
|
+
code: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { NoRole, Public, User } from '@hed-hog/api';
|
|
2
|
+
import { Locale } from '@hed-hog/api-locale';
|
|
3
|
+
import {
|
|
4
|
+
Body,
|
|
5
|
+
Controller,
|
|
6
|
+
Get,
|
|
7
|
+
Headers,
|
|
8
|
+
Inject,
|
|
9
|
+
Ip,
|
|
10
|
+
Post,
|
|
11
|
+
Query,
|
|
12
|
+
UseGuards,
|
|
13
|
+
forwardRef,
|
|
14
|
+
} from '@nestjs/common';
|
|
15
|
+
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
|
16
|
+
import { AuthorizeService } from './authorize.service';
|
|
17
|
+
import { AuthorizeInfoDTO } from './dto/authorize-info.dto';
|
|
18
|
+
import { ConsentDTO } from './dto/consent.dto';
|
|
19
|
+
import { TokenDTO } from './dto/token.dto';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Endpoints do fluxo Authorization Code + PKCE.
|
|
23
|
+
*
|
|
24
|
+
* Note o que NÃO existe aqui: um `GET /auth/authorize` que redireciona. O
|
|
25
|
+
* redirect final é feito pelo browser, na página de consent do admin, depois do
|
|
26
|
+
* POST /auth/authorize/consent. Assim toda a validação fica no servidor e o
|
|
27
|
+
* `code` não passa pelo mecanismo de redirect do Nest (nem pelos seus logs).
|
|
28
|
+
*/
|
|
29
|
+
@Controller('auth')
|
|
30
|
+
export class AuthorizeController {
|
|
31
|
+
constructor(
|
|
32
|
+
@Inject(forwardRef(() => AuthorizeService))
|
|
33
|
+
private readonly service: AuthorizeService,
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Metadados para a tela de consent. Público: a página precisa validar os
|
|
38
|
+
* parâmetros e mostrar de quem é o app ANTES de exigir login.
|
|
39
|
+
*/
|
|
40
|
+
@Public()
|
|
41
|
+
@UseGuards(ThrottlerGuard)
|
|
42
|
+
@Throttle({ default: { limit: 30, ttl: 60000 } })
|
|
43
|
+
@Get('authorize/info')
|
|
44
|
+
async authorizeInfo(@Query() query: AuthorizeInfoDTO) {
|
|
45
|
+
return this.service.getAuthorizationInfo({
|
|
46
|
+
clientId: query.client_id,
|
|
47
|
+
redirectUri: query.redirect_uri,
|
|
48
|
+
scope: query.scope,
|
|
49
|
+
codeChallenge: query.code_challenge,
|
|
50
|
+
codeChallengeMethod: query.code_challenge_method,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** O usuário logado aprova. O code sai vinculado a ele. */
|
|
55
|
+
@NoRole()
|
|
56
|
+
@UseGuards(ThrottlerGuard)
|
|
57
|
+
@Throttle({ default: { limit: 10, ttl: 60000 } })
|
|
58
|
+
@Post('authorize/consent')
|
|
59
|
+
async consent(@User() { id }, @Body() body: ConsentDTO) {
|
|
60
|
+
const code = await this.service.issueCode({
|
|
61
|
+
userId: id,
|
|
62
|
+
clientId: body.client_id,
|
|
63
|
+
redirectUri: body.redirect_uri,
|
|
64
|
+
scope: body.scope,
|
|
65
|
+
codeChallenge: body.code_challenge,
|
|
66
|
+
codeChallengeMethod: body.code_challenge_method,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return { code };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Troca o code por tokens. Público por definição (o cliente ainda não tem
|
|
74
|
+
* token) — quem autentica é a posse do code + do code_verifier.
|
|
75
|
+
*
|
|
76
|
+
* Sem @Res() e sem cookie `rt`: o refreshToken vai no body. Um Set-Cookie aqui
|
|
77
|
+
* seria ruído para um cliente não-browser e um vetor de CSRF a mais.
|
|
78
|
+
*/
|
|
79
|
+
@Public()
|
|
80
|
+
@UseGuards(ThrottlerGuard)
|
|
81
|
+
@Throttle({ default: { limit: 20, ttl: 60000 } })
|
|
82
|
+
@Post('token')
|
|
83
|
+
async token(
|
|
84
|
+
@Body() body: TokenDTO,
|
|
85
|
+
@Locale() locale: string,
|
|
86
|
+
@Ip() ipAddress: string,
|
|
87
|
+
@Headers('user-agent') userAgent: string,
|
|
88
|
+
) {
|
|
89
|
+
return this.service.exchangeCode({
|
|
90
|
+
code: body.code,
|
|
91
|
+
codeVerifier: body.code_verifier,
|
|
92
|
+
clientId: body.client_id,
|
|
93
|
+
redirectUri: body.redirect_uri,
|
|
94
|
+
locale,
|
|
95
|
+
ipAddress,
|
|
96
|
+
userAgent,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PrismaModule } from '@hed-hog/api-prisma';
|
|
2
|
+
import { Module, forwardRef } from '@nestjs/common';
|
|
3
|
+
import { AuthModule } from '../auth/auth.module';
|
|
4
|
+
import { SecurityModule } from '../security/security.module';
|
|
5
|
+
import { AuthorizeController } from './authorize.controller';
|
|
6
|
+
import { AuthorizeService } from './authorize.service';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Separado de OAuthModule de propósito: lá o HedHog é *client* de IdPs externos
|
|
10
|
+
* (Google, Apple...). Aqui ele é o *provider*. Misturar os dois confundiria dois
|
|
11
|
+
* papéis opostos no mesmo arquivo.
|
|
12
|
+
*/
|
|
13
|
+
@Module({
|
|
14
|
+
imports: [
|
|
15
|
+
forwardRef(() => PrismaModule),
|
|
16
|
+
forwardRef(() => SecurityModule),
|
|
17
|
+
forwardRef(() => AuthModule),
|
|
18
|
+
],
|
|
19
|
+
controllers: [AuthorizeController],
|
|
20
|
+
providers: [AuthorizeService],
|
|
21
|
+
exports: [AuthorizeService],
|
|
22
|
+
})
|
|
23
|
+
export class AuthorizeModule {}
|