@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.
Files changed (146) hide show
  1. package/dist/app-command/app-command.registry.d.ts +2 -0
  2. package/dist/app-command/app-command.registry.d.ts.map +1 -1
  3. package/dist/app-command/app-command.registry.js.map +1 -1
  4. package/dist/auth/auth.controller.d.ts +24 -1
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +27 -0
  7. package/dist/auth/auth.controller.js.map +1 -1
  8. package/dist/auth/auth.service.d.ts +52 -1
  9. package/dist/auth/auth.service.d.ts.map +1 -1
  10. package/dist/auth/auth.service.js +108 -0
  11. package/dist/auth/auth.service.js.map +1 -1
  12. package/dist/auth/dto/complete-account.dto.d.ts +9 -0
  13. package/dist/auth/dto/complete-account.dto.d.ts.map +1 -0
  14. package/dist/auth/dto/complete-account.dto.js +43 -0
  15. package/dist/auth/dto/complete-account.dto.js.map +1 -0
  16. package/dist/authorize/authorize.controller.d.ts +45 -0
  17. package/dist/authorize/authorize.controller.d.ts.map +1 -0
  18. package/dist/authorize/authorize.controller.js +120 -0
  19. package/dist/authorize/authorize.controller.js.map +1 -0
  20. package/dist/authorize/authorize.module.d.ts +8 -0
  21. package/dist/authorize/authorize.module.d.ts.map +1 -0
  22. package/dist/authorize/authorize.module.js +36 -0
  23. package/dist/authorize/authorize.module.js.map +1 -0
  24. package/dist/authorize/authorize.service.d.ts +79 -0
  25. package/dist/authorize/authorize.service.d.ts.map +1 -0
  26. package/dist/authorize/authorize.service.js +196 -0
  27. package/dist/authorize/authorize.service.js.map +1 -0
  28. package/dist/authorize/dto/authorize-info.dto.d.ts +8 -0
  29. package/dist/authorize/dto/authorize-info.dto.d.ts.map +1 -0
  30. package/dist/authorize/dto/authorize-info.dto.js +42 -0
  31. package/dist/authorize/dto/authorize-info.dto.js.map +1 -0
  32. package/dist/authorize/dto/consent.dto.d.ts +8 -0
  33. package/dist/authorize/dto/consent.dto.d.ts.map +1 -0
  34. package/dist/authorize/dto/consent.dto.js +42 -0
  35. package/dist/authorize/dto/consent.dto.js.map +1 -0
  36. package/dist/authorize/dto/token.dto.d.ts +8 -0
  37. package/dist/authorize/dto/token.dto.d.ts.map +1 -0
  38. package/dist/authorize/dto/token.dto.js +42 -0
  39. package/dist/authorize/dto/token.dto.js.map +1 -0
  40. package/dist/challenge/challenge.service.d.ts +7 -0
  41. package/dist/challenge/challenge.service.d.ts.map +1 -1
  42. package/dist/challenge/challenge.service.js +22 -0
  43. package/dist/challenge/challenge.service.js.map +1 -1
  44. package/dist/core.module.d.ts.map +1 -1
  45. package/dist/core.module.js +2 -0
  46. package/dist/core.module.js.map +1 -1
  47. package/dist/file/file.service.d.ts +7 -0
  48. package/dist/file/file.service.d.ts.map +1 -1
  49. package/dist/file/file.service.js +16 -0
  50. package/dist/file/file.service.js.map +1 -1
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/integration-profile/integration-profile.secrets.d.ts +6 -2
  56. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  57. package/dist/integration-profile/integration-profile.secrets.js +14 -3
  58. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  59. package/dist/integration-profile/integration-profile.service.d.ts +7 -0
  60. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  61. package/dist/integration-profile/integration-profile.service.js +91 -4
  62. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  63. package/dist/integration-profile/integration-profile.utils.d.ts +28 -0
  64. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  65. package/dist/integration-profile/integration-profile.utils.js +40 -1
  66. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  67. package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
  68. package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
  69. package/dist/oauth/oauth.controller.d.ts +26 -0
  70. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  71. package/dist/oauth/oauth.controller.js +94 -0
  72. package/dist/oauth/oauth.controller.js.map +1 -1
  73. package/dist/oauth/oauth.service.d.ts +39 -1
  74. package/dist/oauth/oauth.service.d.ts.map +1 -1
  75. package/dist/oauth/oauth.service.js +115 -7
  76. package/dist/oauth/oauth.service.js.map +1 -1
  77. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -1
  78. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  79. package/dist/oauth/providers/microsoft-entra-id.provider.js +37 -6
  80. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  81. package/dist/profile/profile.controller.d.ts +4 -3
  82. package/dist/profile/profile.controller.d.ts.map +1 -1
  83. package/dist/profile/profile.controller.js.map +1 -1
  84. package/dist/profile/profile.service.d.ts +5 -4
  85. package/dist/profile/profile.service.d.ts.map +1 -1
  86. package/dist/profile/profile.service.js.map +1 -1
  87. package/dist/webhook-integration/webhook-integration.controller.d.ts +5 -8
  88. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  89. package/dist/webhook-integration/webhook-integration.controller.js +31 -8
  90. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  91. package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
  92. package/dist/webhook-integration/webhook-integration.module.js +5 -0
  93. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  94. package/dist/webhook-integration/webhook-integration.service.d.ts +33 -1
  95. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  96. package/dist/webhook-integration/webhook-integration.service.js +116 -5
  97. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  98. package/dist/webhook-integration/webhook-protocol.registry.d.ts +63 -0
  99. package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -0
  100. package/dist/webhook-integration/webhook-protocol.registry.js +51 -0
  101. package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -0
  102. package/hedhog/data/integration_provider.yaml +8 -0
  103. package/hedhog/data/integration_type.yaml +6 -0
  104. package/hedhog/data/mail.yaml +39 -0
  105. package/hedhog/data/route.yaml +12 -0
  106. package/hedhog/data/setting_group.yaml +15 -1
  107. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +11 -7
  108. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +9 -7
  109. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +6 -2
  110. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +130 -34
  111. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +16 -8
  112. package/hedhog/frontend/messages/en.json +55 -1
  113. package/hedhog/frontend/messages/pt.json +55 -1
  114. package/hedhog/table/webhook_integration.yaml +64 -43
  115. package/package.json +10 -5
  116. package/src/app-command/app-command.registry.ts +2 -0
  117. package/src/auth/auth.controller.ts +23 -1
  118. package/src/auth/auth.service.ts +135 -1
  119. package/src/auth/dto/complete-account.dto.ts +24 -0
  120. package/src/authorize/authorize.controller.ts +99 -0
  121. package/src/authorize/authorize.module.ts +23 -0
  122. package/src/authorize/authorize.service.spec.ts +307 -0
  123. package/src/authorize/authorize.service.ts +245 -0
  124. package/src/authorize/dto/authorize-info.dto.ts +23 -0
  125. package/src/authorize/dto/consent.dto.ts +23 -0
  126. package/src/authorize/dto/token.dto.ts +23 -0
  127. package/src/challenge/challenge.service.ts +28 -0
  128. package/src/core.module.ts +2 -0
  129. package/src/file/file.service.ts +19 -0
  130. package/src/index.ts +1 -0
  131. package/src/integration-profile/integration-profile.secrets.spec.ts +114 -0
  132. package/src/integration-profile/integration-profile.secrets.ts +14 -3
  133. package/src/integration-profile/integration-profile.service.ts +129 -3
  134. package/src/integration-profile/integration-profile.utils.spec.ts +75 -0
  135. package/src/integration-profile/integration-profile.utils.ts +79 -0
  136. package/src/oauth/interfaces/OAuthProvider.ts +5 -1
  137. package/src/oauth/oauth.controller.ts +89 -0
  138. package/src/oauth/oauth.service.ts +190 -8
  139. package/src/oauth/providers/microsoft-entra-id.provider.ts +45 -5
  140. package/src/profile/profile.controller.ts +6 -2
  141. package/src/profile/profile.service.ts +5 -3
  142. package/src/webhook-integration/webhook-integration.controller.ts +40 -2
  143. package/src/webhook-integration/webhook-integration.module.ts +5 -0
  144. package/src/webhook-integration/webhook-integration.service.ts +151 -2
  145. package/src/webhook-integration/webhook-protocol.registry.spec.ts +70 -0
  146. 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
- - type: created_at
33
- - type: updated_at
34
- indices:
35
- - columns:
36
- - slug
37
- isUnique: true
38
- - columns:
39
- - public_uuid
40
- isUnique: true
41
- - columns:
42
- - status
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.385",
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
- "rimraf": "^6.1.0"
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
  }
@@ -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 {}