@hed-hog/core 0.0.385 → 0.0.387
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 +24 -1
- 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 +52 -1
- 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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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 +14 -3
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +7 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +91 -4
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +28 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +40 -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 +4 -3
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +5 -4
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.d.ts +5 -8
- package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.js +31 -8
- package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.js +5 -0
- package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts +33 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +116 -5
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/webhook-integration/webhook-protocol.registry.d.ts +63 -0
- package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -0
- package/dist/webhook-integration/webhook-protocol.registry.js +51 -0
- package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -0
- 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/app/integration/profiles/page.tsx.ejs +16 -8
- package/hedhog/frontend/messages/en.json +55 -1
- package/hedhog/frontend/messages/pt.json +55 -1
- package/hedhog/table/webhook_integration.yaml +64 -43
- package/package.json +10 -5
- package/src/app-command/app-command.registry.ts +2 -0
- package/src/auth/auth.controller.ts +23 -1
- package/src/auth/auth.service.ts +135 -1
- 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/index.ts +1 -0
- package/src/integration-profile/integration-profile.secrets.spec.ts +114 -0
- package/src/integration-profile/integration-profile.secrets.ts +14 -3
- package/src/integration-profile/integration-profile.service.ts +129 -3
- package/src/integration-profile/integration-profile.utils.spec.ts +75 -0
- package/src/integration-profile/integration-profile.utils.ts +79 -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/profile/profile.controller.ts +6 -2
- package/src/profile/profile.service.ts +5 -3
- package/src/webhook-integration/webhook-integration.controller.ts +40 -2
- package/src/webhook-integration/webhook-integration.module.ts +5 -0
- package/src/webhook-integration/webhook-integration.service.ts +151 -2
- package/src/webhook-integration/webhook-protocol.registry.spec.ts +70 -0
- package/src/webhook-integration/webhook-protocol.registry.ts +93 -0
|
@@ -1,43 +1,64 @@
|
|
|
1
|
-
columns:
|
|
2
|
-
- type: pk
|
|
3
|
-
- type: slug
|
|
4
|
-
- name: name
|
|
5
|
-
type: varchar
|
|
6
|
-
length: 127
|
|
7
|
-
- name: description
|
|
8
|
-
type: text
|
|
9
|
-
isNullable: true
|
|
10
|
-
- name: public_uuid
|
|
11
|
-
type: varchar
|
|
12
|
-
length: 36
|
|
13
|
-
- name: status
|
|
14
|
-
type: enum
|
|
15
|
-
enum:
|
|
16
|
-
- active
|
|
17
|
-
- inactive
|
|
18
|
-
default: active
|
|
19
|
-
- name: require_token
|
|
20
|
-
type: boolean
|
|
21
|
-
default: false
|
|
22
|
-
- name: token_hash
|
|
23
|
-
type: varchar
|
|
24
|
-
length: 128
|
|
25
|
-
isNullable: true
|
|
26
|
-
- name: allowed_ips
|
|
27
|
-
type: json
|
|
28
|
-
isNullable: true
|
|
29
|
-
- name: body_schema
|
|
30
|
-
type: json
|
|
31
|
-
isNullable: true
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
columns:
|
|
2
|
+
- type: pk
|
|
3
|
+
- type: slug
|
|
4
|
+
- name: name
|
|
5
|
+
type: varchar
|
|
6
|
+
length: 127
|
|
7
|
+
- name: description
|
|
8
|
+
type: text
|
|
9
|
+
isNullable: true
|
|
10
|
+
- name: public_uuid
|
|
11
|
+
type: varchar
|
|
12
|
+
length: 36
|
|
13
|
+
- name: status
|
|
14
|
+
type: enum
|
|
15
|
+
enum:
|
|
16
|
+
- active
|
|
17
|
+
- inactive
|
|
18
|
+
default: active
|
|
19
|
+
- name: require_token
|
|
20
|
+
type: boolean
|
|
21
|
+
default: false
|
|
22
|
+
- name: token_hash
|
|
23
|
+
type: varchar
|
|
24
|
+
length: 128
|
|
25
|
+
isNullable: true
|
|
26
|
+
- name: allowed_ips
|
|
27
|
+
type: json
|
|
28
|
+
isNullable: true
|
|
29
|
+
- name: body_schema
|
|
30
|
+
type: json
|
|
31
|
+
isNullable: true
|
|
32
|
+
# Protocolo de entrada. Nulo = webhook clássico (token + actions). Preenchido,
|
|
33
|
+
# a chamada é delegada ao adaptador registrado por outro módulo no
|
|
34
|
+
# WebhookProtocolRegistry (ex.: 'meta-whatsapp'), que faz a própria autenticação.
|
|
35
|
+
- name: protocol
|
|
36
|
+
type: varchar
|
|
37
|
+
length: 63
|
|
38
|
+
isNullable: true
|
|
39
|
+
# Perfil que guarda as credenciais do protocolo (ex.: o app_secret que assina o
|
|
40
|
+
# HMAC). Nulo nos webhooks clássicos, que não têm provedor por trás.
|
|
41
|
+
- name: integration_profile_id
|
|
42
|
+
type: fk
|
|
43
|
+
isNullable: true
|
|
44
|
+
references:
|
|
45
|
+
table: integration_profile
|
|
46
|
+
column: id
|
|
47
|
+
onDelete: CASCADE
|
|
48
|
+
onUpdate: CASCADE
|
|
49
|
+
- type: created_at
|
|
50
|
+
- type: updated_at
|
|
51
|
+
indices:
|
|
52
|
+
- columns:
|
|
53
|
+
- slug
|
|
54
|
+
isUnique: true
|
|
55
|
+
- columns:
|
|
56
|
+
- public_uuid
|
|
57
|
+
isUnique: true
|
|
58
|
+
- columns:
|
|
59
|
+
- status
|
|
60
|
+
- columns:
|
|
61
|
+
- protocol
|
|
62
|
+
- columns:
|
|
63
|
+
- integration_profile_id
|
|
64
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.387",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"ua-parser-js": "^2.0.10",
|
|
43
43
|
"uuid": "^11.1.0",
|
|
44
44
|
"@hed-hog/api-prisma": "0.0.6",
|
|
45
|
-
"@hed-hog/api-locale": "0.0.14",
|
|
46
45
|
"@hed-hog/api-types": "0.0.1",
|
|
47
|
-
"@hed-hog/api": "0.0.8",
|
|
48
46
|
"@hed-hog/api-mail": "0.0.9",
|
|
49
|
-
"@hed-hog/api-pagination": "0.0.7"
|
|
47
|
+
"@hed-hog/api-pagination": "0.0.7",
|
|
48
|
+
"@hed-hog/api-locale": "0.0.14",
|
|
49
|
+
"@hed-hog/api": "0.0.8"
|
|
50
50
|
},
|
|
51
51
|
"exports": {
|
|
52
52
|
".": {
|
|
@@ -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;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
UseGuards
|
|
17
17
|
} from '@nestjs/common';
|
|
18
18
|
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
|
19
|
+
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
|
|
19
20
|
import { TokenService } from '../token/token.service';
|
|
20
21
|
import { CreateWithEmailAndPasswordDTO } from '../user/dto/create-with-email-and-password.dto';
|
|
21
22
|
import { UserService } from '../user/user.service';
|
|
@@ -28,6 +29,7 @@ import { LoginWithRecoveryCodeDTO } from './dto/login-with-recovery-code';
|
|
|
28
29
|
import { LoginDTO } from './dto/login.dto';
|
|
29
30
|
import { ResendMfaCodeDTO } from './dto/resend-mfa-code.dto';
|
|
30
31
|
import { ResetDTO } from './dto/reset.dto';
|
|
32
|
+
import { CompleteAccountDTO, VerifyAccountInviteDTO } from './dto/complete-account.dto';
|
|
31
33
|
|
|
32
34
|
@Controller('auth')
|
|
33
35
|
export class AuthController {
|
|
@@ -205,7 +207,7 @@ export class AuthController {
|
|
|
205
207
|
@Locale() locale: string,
|
|
206
208
|
@Body() { mfaToken }: { mfaToken: string },
|
|
207
209
|
@Headers('origin') origin: string
|
|
208
|
-
) {
|
|
210
|
+
): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
209
211
|
return this.service.generateWebAuthnAuthenticationOptions(locale, mfaToken, origin);
|
|
210
212
|
}
|
|
211
213
|
|
|
@@ -269,4 +271,24 @@ export class AuthController {
|
|
|
269
271
|
) {
|
|
270
272
|
return this.service.forgotResetPassword(locale, password, code, ipAddress, userAgent);
|
|
271
273
|
}
|
|
274
|
+
|
|
275
|
+
@Public()
|
|
276
|
+
@Post('complete-account/verify')
|
|
277
|
+
async verifyAccountInvite(
|
|
278
|
+
@Locale() locale: string,
|
|
279
|
+
@Body() { code }: VerifyAccountInviteDTO,
|
|
280
|
+
) {
|
|
281
|
+
return this.service.verifyAccountInvite(locale, code);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@Public()
|
|
285
|
+
@Post('complete-account')
|
|
286
|
+
async completeAccount(
|
|
287
|
+
@Locale() locale: string,
|
|
288
|
+
@Body() { password, code, name }: CompleteAccountDTO,
|
|
289
|
+
@Ip() ipAddress: string,
|
|
290
|
+
@Headers('user-agent') userAgent: string,
|
|
291
|
+
) {
|
|
292
|
+
return this.service.completeAccount(locale, { password, code, name }, ipAddress, userAgent);
|
|
293
|
+
}
|
|
272
294
|
}
|
package/src/auth/auth.service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
|
|
4
5
|
import {
|
|
5
6
|
BadRequestException,
|
|
6
7
|
forwardRef,
|
|
@@ -558,6 +559,139 @@ export class AuthService {
|
|
|
558
559
|
return { accessToken, refreshToken, session }
|
|
559
560
|
}
|
|
560
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Garante que exista um usuário com o identificador de email informado e cria
|
|
564
|
+
* um challenge seguro de criação de conta (link de convite). Não envia email —
|
|
565
|
+
* o remetente (ex.: LMS enterprise) monta o link e dispara o template. Reutiliza
|
|
566
|
+
* um usuário existente por `userId` ou pelo identificador de email; caso não
|
|
567
|
+
* exista, cria um novo usuário apenas com o nome.
|
|
568
|
+
*/
|
|
569
|
+
async prepareAccountInvite(
|
|
570
|
+
locale: string,
|
|
571
|
+
{ email, name, userId }: { email: string; name?: string; userId?: number },
|
|
572
|
+
) {
|
|
573
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
574
|
+
|
|
575
|
+
let targetUserId = userId;
|
|
576
|
+
if (!targetUserId) {
|
|
577
|
+
const existing = await this.user.findUserByEmail(locale, normalizedEmail);
|
|
578
|
+
targetUserId = existing?.id;
|
|
579
|
+
}
|
|
580
|
+
if (!targetUserId) {
|
|
581
|
+
const createdUser = await this.prisma.user.create({
|
|
582
|
+
data: { name: name?.trim() || normalizedEmail },
|
|
583
|
+
});
|
|
584
|
+
targetUserId = createdUser.id;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let identifier = await this.prisma.user_identifier.findFirst({
|
|
588
|
+
where: { user_id: targetUserId, type: 'email', value: normalizedEmail },
|
|
589
|
+
});
|
|
590
|
+
if (!identifier) {
|
|
591
|
+
identifier = await this.prisma.user_identifier.create({
|
|
592
|
+
data: {
|
|
593
|
+
user_id: targetUserId,
|
|
594
|
+
type: 'email',
|
|
595
|
+
value: normalizedEmail,
|
|
596
|
+
enabled: true,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
} else if (!identifier.enabled) {
|
|
600
|
+
identifier = await this.prisma.user_identifier.update({
|
|
601
|
+
where: { id: identifier.id },
|
|
602
|
+
data: { enabled: true },
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const passwordCount = await this.prisma.user_credential.count({
|
|
607
|
+
where: {
|
|
608
|
+
user_id: targetUserId,
|
|
609
|
+
type: 'password',
|
|
610
|
+
revoked_at: null,
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const token = await this.challenge.createAccountInviteChallenge(identifier.id);
|
|
615
|
+
|
|
616
|
+
return { userId: targetUserId, token, hasPassword: passwordCount > 0 };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Valida (sem consumir) um token de convite de criação de conta e retorna os
|
|
621
|
+
* dados básicos para pré-preencher o formulário no app de treinamentos.
|
|
622
|
+
*/
|
|
623
|
+
async verifyAccountInvite(locale: string, code: string) {
|
|
624
|
+
const challenge = await this.challenge.verifyCodePassword(locale, code);
|
|
625
|
+
return {
|
|
626
|
+
valid: true,
|
|
627
|
+
name: challenge.user_identifier.user?.name ?? null,
|
|
628
|
+
email: challenge.user_identifier.value,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Conclui a criação de conta a partir de um token de convite: cria/atualiza a
|
|
634
|
+
* credencial de senha, marca o identificador como verificado, atualiza o nome
|
|
635
|
+
* (se enviado), consome o challenge e autentica o usuário (auto-login).
|
|
636
|
+
*/
|
|
637
|
+
async completeAccount(
|
|
638
|
+
locale: string,
|
|
639
|
+
{ password, code, name }: { password: string; code: string; name?: string },
|
|
640
|
+
ipAddress: string,
|
|
641
|
+
userAgent: string,
|
|
642
|
+
) {
|
|
643
|
+
const challenge = await this.challenge.verifyCodePassword(locale, code);
|
|
644
|
+
const userIdentifier = challenge.user_identifier;
|
|
645
|
+
const passwordHash = await this.security.hashArgon2(password);
|
|
646
|
+
|
|
647
|
+
const existingCredential = await this.prisma.user_credential.findFirst({
|
|
648
|
+
where: { user_id: userIdentifier.user_id, type: 'password' },
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (existingCredential) {
|
|
652
|
+
await this.prisma.user_credential.updateMany({
|
|
653
|
+
where: { user_id: userIdentifier.user_id, type: 'password' },
|
|
654
|
+
data: { hash: passwordHash, requires_reset: false, revoked_at: null },
|
|
655
|
+
});
|
|
656
|
+
} else {
|
|
657
|
+
await this.prisma.user_credential.create({
|
|
658
|
+
data: {
|
|
659
|
+
user_id: userIdentifier.user_id,
|
|
660
|
+
type: 'password',
|
|
661
|
+
hash: passwordHash,
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
await this.prisma.user_identifier.update({
|
|
667
|
+
where: { id: userIdentifier.id },
|
|
668
|
+
data: { verified_at: new Date(), enabled: true },
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
if (name?.trim()) {
|
|
672
|
+
await this.prisma.user.update({
|
|
673
|
+
where: { id: userIdentifier.user_id },
|
|
674
|
+
data: { name: name.trim() },
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Consome o challenge para impedir reuso do link (invites têm validade longa).
|
|
679
|
+
await this.prisma.user_identifier_challenge.updateMany({
|
|
680
|
+
where: { id: challenge.id },
|
|
681
|
+
data: { verified_at: new Date(), expires_at: new Date() },
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const { accessToken, refreshToken, session } =
|
|
685
|
+
await this.getAuthenticationPayload(
|
|
686
|
+
locale,
|
|
687
|
+
userIdentifier.user_id,
|
|
688
|
+
ipAddress,
|
|
689
|
+
userAgent,
|
|
690
|
+
);
|
|
691
|
+
await this.user.registerUserActivity(userIdentifier.id, 'completeAccount');
|
|
692
|
+
return { accessToken, refreshToken, session };
|
|
693
|
+
}
|
|
694
|
+
|
|
561
695
|
async verifyMfaCode(locale: string, mfaToken: string, code: string, ipAddress: string, userAgent: string, methodType?: 'totp' | 'email' | 'recovery', browserId?: string | null) {
|
|
562
696
|
const payload = await this.token.verifyMfaChallengeToken(mfaToken);
|
|
563
697
|
const user = await this.prisma.user.findUnique({
|
|
@@ -757,7 +891,7 @@ export class AuthService {
|
|
|
757
891
|
};
|
|
758
892
|
}
|
|
759
893
|
|
|
760
|
-
async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string, requestOrigin?: string) {
|
|
894
|
+
async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string, requestOrigin?: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
761
895
|
const { generateAuthenticationOptions } = await import('@simplewebauthn/server');
|
|
762
896
|
const payload = await this.token.verifyMfaChallengeToken(mfaToken);
|
|
763
897
|
|
|
@@ -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 {}
|