@hed-hog/core 0.0.384 → 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.
Files changed (123) 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 +23 -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 +56 -6
  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/integration-profile/integration-profile.secrets.d.ts +6 -2
  52. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  53. package/dist/integration-profile/integration-profile.secrets.js +11 -2
  54. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  55. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  56. package/dist/integration-profile/integration-profile.service.js +18 -2
  57. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  58. package/dist/integration-profile/integration-profile.utils.d.ts +10 -0
  59. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  60. package/dist/integration-profile/integration-profile.utils.js +24 -1
  61. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  62. package/dist/notification/notification.controller.d.ts +12 -12
  63. package/dist/notification/notification.service.d.ts +12 -12
  64. package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
  65. package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
  66. package/dist/oauth/oauth.controller.d.ts +26 -0
  67. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  68. package/dist/oauth/oauth.controller.js +94 -0
  69. package/dist/oauth/oauth.controller.js.map +1 -1
  70. package/dist/oauth/oauth.service.d.ts +39 -1
  71. package/dist/oauth/oauth.service.d.ts.map +1 -1
  72. package/dist/oauth/oauth.service.js +115 -7
  73. package/dist/oauth/oauth.service.js.map +1 -1
  74. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -1
  75. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  76. package/dist/oauth/providers/microsoft-entra-id.provider.js +37 -6
  77. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  78. package/dist/profile/profile.controller.d.ts +1 -1
  79. package/dist/profile/profile.service.d.ts +1 -1
  80. package/dist/session/session.controller.d.ts +1 -1
  81. package/dist/session/session.service.d.ts +3 -3
  82. package/dist/user/user.controller.d.ts +1 -1
  83. package/dist/user/user.service.d.ts +3 -3
  84. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  85. package/dist/webhook-integration/webhook-integration.service.js +7 -1
  86. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  87. package/hedhog/data/integration_provider.yaml +8 -0
  88. package/hedhog/data/integration_type.yaml +6 -0
  89. package/hedhog/data/mail.yaml +39 -0
  90. package/hedhog/data/route.yaml +12 -0
  91. package/hedhog/data/setting_group.yaml +15 -1
  92. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +11 -7
  93. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +9 -7
  94. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +6 -2
  95. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +130 -34
  96. package/hedhog/frontend/app/roles/page.tsx.ejs +13 -12
  97. package/hedhog/frontend/messages/en.json +35 -2
  98. package/hedhog/frontend/messages/pt.json +35 -2
  99. package/package.json +10 -5
  100. package/src/app-command/app-command.registry.ts +2 -0
  101. package/src/auth/auth.controller.ts +21 -0
  102. package/src/auth/auth.service.ts +133 -0
  103. package/src/auth/dto/complete-account.dto.ts +24 -0
  104. package/src/authorize/authorize.controller.ts +99 -0
  105. package/src/authorize/authorize.module.ts +23 -0
  106. package/src/authorize/authorize.service.spec.ts +307 -0
  107. package/src/authorize/authorize.service.ts +245 -0
  108. package/src/authorize/dto/authorize-info.dto.ts +23 -0
  109. package/src/authorize/dto/consent.dto.ts +23 -0
  110. package/src/authorize/dto/token.dto.ts +23 -0
  111. package/src/challenge/challenge.service.ts +28 -0
  112. package/src/core.module.ts +2 -0
  113. package/src/file/file.service.ts +19 -0
  114. package/src/integration-profile/integration-profile.secrets.spec.ts +114 -0
  115. package/src/integration-profile/integration-profile.secrets.ts +11 -2
  116. package/src/integration-profile/integration-profile.service.ts +19 -0
  117. package/src/integration-profile/integration-profile.utils.ts +40 -0
  118. package/src/mcp/decorators/allow-mcp-token.decorator.ts +7 -7
  119. package/src/oauth/interfaces/OAuthProvider.ts +5 -1
  120. package/src/oauth/oauth.controller.ts +89 -0
  121. package/src/oauth/oauth.service.ts +190 -8
  122. package/src/oauth/providers/microsoft-entra-id.provider.ts +45 -5
  123. package/src/webhook-integration/webhook-integration.service.ts +18 -2
@@ -28,6 +28,13 @@ import {
28
28
  ContextMenuSeparator,
29
29
  ContextMenuTrigger,
30
30
  } from '@/components/ui/context-menu';
31
+ import {
32
+ DropdownMenu,
33
+ DropdownMenuContent,
34
+ DropdownMenuItem,
35
+ DropdownMenuSeparator,
36
+ DropdownMenuTrigger,
37
+ } from '@/components/ui/dropdown-menu';
31
38
  import { Input } from '@/components/ui/input';
32
39
  import { ScrollArea } from '@/components/ui/scroll-area';
33
40
  import {
@@ -51,6 +58,7 @@ import {
51
58
  Home,
52
59
  LayoutDashboard,
53
60
  Loader2,
61
+ MoreHorizontal,
54
62
  Palette,
55
63
  Pencil,
56
64
  Plus,
@@ -1032,42 +1040,130 @@ export function DashboardHomeTabs() {
1032
1040
  {tabContent}
1033
1041
  </div>
1034
1042
  ) : (
1035
- <TabsTrigger
1036
- value={dashboard.slug}
1037
- draggable={!isBusy}
1038
- onDragStart={(event) =>
1039
- handleTabDragStart(event, dashboard.slug)
1040
- }
1041
- onDragOver={handleTabDragOver}
1042
- onDrop={(event) =>
1043
- void handleTabDrop(event, dashboard.slug)
1044
- }
1045
- onDragEnd={handleTabDragEnd}
1046
- className={cn(
1047
- 'dashboard-tab-trigger -mb-px mt-1 inline-flex h-9 shrink-0 cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-b-none rounded-t-lg border-none px-3 py-2 shadow-none data-[state=active]:shadow-none',
1048
- draggingDashboardSlug === dashboard.slug &&
1049
- 'opacity-70'
1050
- )}
1051
- style={
1052
- isActiveTab
1053
- ? {
1054
- backgroundColor: 'var(--background)',
1055
- color: 'var(--primary)',
1043
+ <div className="group relative mt-1 shrink-0">
1044
+ <TabsTrigger
1045
+ value={dashboard.slug}
1046
+ draggable={!isBusy}
1047
+ onDragStart={(event) =>
1048
+ handleTabDragStart(event, dashboard.slug)
1049
+ }
1050
+ onDragOver={handleTabDragOver}
1051
+ onDrop={(event) =>
1052
+ void handleTabDrop(event, dashboard.slug)
1053
+ }
1054
+ onDragEnd={handleTabDragEnd}
1055
+ className={cn(
1056
+ 'dashboard-tab-trigger -mb-px inline-flex h-9 shrink-0 cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-b-none rounded-t-lg border-none pr-9 pl-3 py-2 shadow-none data-[state=active]:shadow-none',
1057
+ draggingDashboardSlug === dashboard.slug &&
1058
+ 'opacity-70'
1059
+ )}
1060
+ style={
1061
+ isActiveTab
1062
+ ? {
1063
+ backgroundColor: 'var(--background)',
1064
+ color: 'var(--primary)',
1065
+ }
1066
+ : {
1067
+ backgroundColor: 'transparent',
1068
+ color: 'var(--muted-foreground)',
1069
+ }
1070
+ }
1071
+ onDoubleClick={() =>
1072
+ startRenameDashboard(dashboard)
1073
+ }
1074
+ onContextMenu={() =>
1075
+ setActiveSlug(dashboard.slug)
1076
+ }
1077
+ >
1078
+ {tabContent}
1079
+ </TabsTrigger>
1080
+ <DropdownMenu>
1081
+ <DropdownMenuTrigger asChild>
1082
+ <Button
1083
+ type="button"
1084
+ variant="ghost"
1085
+ size="icon"
1086
+ className={cn(
1087
+ 'absolute top-1/2 right-1 z-10 size-7 -translate-y-1/2 rounded-md text-muted-foreground hover:text-foreground',
1088
+ isActiveTab
1089
+ ? 'bg-background/80'
1090
+ : 'bg-transparent hover:bg-background/60'
1091
+ )}
1092
+ onClick={(event) => {
1093
+ event.stopPropagation();
1094
+ setActiveSlug(dashboard.slug);
1095
+ }}
1096
+ onPointerDown={(event) =>
1097
+ event.stopPropagation()
1056
1098
  }
1057
- : {
1058
- backgroundColor: 'transparent',
1059
- color: 'var(--muted-foreground)',
1099
+ aria-label={t('tabActions')}
1100
+ title={t('tabActions')}
1101
+ >
1102
+ <MoreHorizontal className="size-4" />
1103
+ </Button>
1104
+ </DropdownMenuTrigger>
1105
+ <DropdownMenuContent
1106
+ align="end"
1107
+ className="w-56"
1108
+ onCloseAutoFocus={(event) =>
1109
+ event.preventDefault()
1110
+ }
1111
+ >
1112
+ <DropdownMenuItem
1113
+ onSelect={() =>
1114
+ openAddWidgetsForDashboard(dashboard)
1060
1115
  }
1061
- }
1062
- onDoubleClick={() =>
1063
- startRenameDashboard(dashboard)
1064
- }
1065
- onContextMenu={() =>
1066
- setActiveSlug(dashboard.slug)
1067
- }
1068
- >
1069
- {tabContent}
1070
- </TabsTrigger>
1116
+ >
1117
+ <Plus className="size-4" />
1118
+ {t('addWidgets')}
1119
+ </DropdownMenuItem>
1120
+ {!dashboard.is_home ? (
1121
+ <DropdownMenuItem
1122
+ onSelect={() =>
1123
+ void handleSetHomeDashboard(dashboard)
1124
+ }
1125
+ >
1126
+ <Home className="size-4" />
1127
+ {t('setAsHome')}
1128
+ </DropdownMenuItem>
1129
+ ) : null}
1130
+ <DropdownMenuItem
1131
+ onSelect={() =>
1132
+ startRenameDashboard(dashboard)
1133
+ }
1134
+ >
1135
+ <Pencil className="size-4" />
1136
+ {t('rename')}
1137
+ </DropdownMenuItem>
1138
+ <DropdownMenuItem
1139
+ onSelect={() =>
1140
+ openIconForDashboard(dashboard)
1141
+ }
1142
+ >
1143
+ <Palette className="size-4" />
1144
+ {t('changeIcon')}
1145
+ </DropdownMenuItem>
1146
+ <DropdownMenuItem
1147
+ onSelect={() =>
1148
+ openShareForDashboard(dashboard)
1149
+ }
1150
+ >
1151
+ <Share2 className="size-4" />
1152
+ {t('share')}
1153
+ </DropdownMenuItem>
1154
+ <DropdownMenuSeparator />
1155
+ <DropdownMenuItem
1156
+ variant="destructive"
1157
+ onSelect={() =>
1158
+ openDeleteForDashboard(dashboard)
1159
+ }
1160
+ >
1161
+ <Trash2 className="size-4" />
1162
+ {t('delete')}
1163
+ </DropdownMenuItem>
1164
+ </DropdownMenuContent>
1165
+ </DropdownMenu>
1166
+ </div>
1071
1167
  )}
1072
1168
  </ContextMenuTrigger>
1073
1169
  <ContextMenuContent className="w-56">
@@ -6,7 +6,6 @@ import {
6
6
  PageHeader,
7
7
  PaginationFooter,
8
8
  SearchBar,
9
- StatsCards,
10
9
  } from '@/components/entity-list';
11
10
  import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
12
11
  import {
@@ -36,6 +35,7 @@ import {
36
35
  FormMessage,
37
36
  } from '@/components/ui/form';
38
37
  import { Input } from '@/components/ui/input';
38
+ import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
39
39
  import {
40
40
  Select,
41
41
  SelectContent,
@@ -594,17 +594,18 @@ export default function RolePage() {
594
594
  description={t('description')}
595
595
  />
596
596
 
597
- <StatsCards
598
- className="sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-1"
599
- stats={[
600
- {
601
- title: t('totalRoles'),
602
- value: String(stats.totalRoles),
603
- icon: <ShieldCheck className="h-5 w-5" />,
604
- iconBgColor: 'bg-purple-50',
605
- iconColor: 'text-purple-600',
606
- },
607
- ]}
597
+ <KpiCardsGrid
598
+ className="grid-cols-1 md:grid-cols-1 xl:grid-cols-1"
599
+ items={
600
+ [
601
+ {
602
+ key: 'totalRoles',
603
+ title: t('totalRoles'),
604
+ value: stats.totalRoles,
605
+ icon: ShieldCheck,
606
+ },
607
+ ] satisfies KpiCardItem[]
608
+ }
608
609
  />
609
610
 
610
611
  <SearchBar
@@ -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",
@@ -2138,7 +2139,8 @@
2138
2139
  "breadcrumbDashboard": "Dashboard",
2139
2140
  "breadcrumbManagement": "Management",
2140
2141
  "breadcrumbCurrent": "MCP Chat",
2141
- "mcpDisabled": "MCP Server is disabled. Enable it in Settings → MCP."
2142
+ "mcpDisabled": "MCP Server is disabled. Enable it in Settings → MCP.",
2143
+ "resizePanel": "Resize panel"
2142
2144
  },
2143
2145
  "McpTokens": {
2144
2146
  "title": "MCP API Keys",
@@ -2424,6 +2426,7 @@
2424
2426
  "fieldAccessKeyId": "Access Key ID",
2425
2427
  "fieldSecretAccessKey": "Secret Access Key",
2426
2428
  "fieldRegion": "Region",
2429
+ "fieldDomain": "Domain",
2427
2430
  "fieldToken": "Token",
2428
2431
  "fieldInstanceName": "Instance Name",
2429
2432
  "fieldPhoneNumberId": "Phone Number ID",
@@ -2480,6 +2483,36 @@
2480
2483
  "nodePoolPlaceholder": "Select a node pool",
2481
2484
  "nodePoolNoneFound": "No node pools found for this cluster.",
2482
2485
  "nodePoolLoadError": "Failed to load node pools. Check the API token and cluster ID.",
2483
- "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."
2484
2517
  }
2485
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",
@@ -2193,7 +2194,8 @@
2193
2194
  "breadcrumbDashboard": "Painel",
2194
2195
  "breadcrumbManagement": "Gerenciamento",
2195
2196
  "breadcrumbCurrent": "Chat MCP",
2196
- "mcpDisabled": "O Servidor MCP está desabilitado. Habilite-o em Configurações → MCP."
2197
+ "mcpDisabled": "O Servidor MCP está desabilitado. Habilite-o em Configurações → MCP.",
2198
+ "resizePanel": "Redimensionar painel"
2197
2199
  },
2198
2200
  "McpTokens": {
2199
2201
  "title": "Chaves de API MCP",
@@ -2479,6 +2481,7 @@
2479
2481
  "fieldAccessKeyId": "Access Key ID",
2480
2482
  "fieldSecretAccessKey": "Secret Access Key",
2481
2483
  "fieldRegion": "Região",
2484
+ "fieldDomain": "Domínio",
2482
2485
  "fieldToken": "Token",
2483
2486
  "fieldInstanceName": "Nome da Instância",
2484
2487
  "fieldPhoneNumberId": "ID do Número de Telefone",
@@ -2535,6 +2538,36 @@
2535
2538
  "nodePoolPlaceholder": "Selecione um node pool",
2536
2539
  "nodePoolNoneFound": "Nenhum node pool encontrado para este cluster.",
2537
2540
  "nodePoolLoadError": "Falha ao carregar os node pools. Verifique o token da API e o ID do cluster.",
2538
- "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."
2539
2572
  }
2540
2573
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.384",
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-prisma": "0.0.6",
45
44
  "@hed-hog/api-mail": "0.0.9",
46
- "@hed-hog/api-locale": "0.0.14",
47
- "@hed-hog/api": "0.0.8",
48
45
  "@hed-hog/api-types": "0.0.1",
46
+ "@hed-hog/api-prisma": "0.0.6",
47
+ "@hed-hog/api": "0.0.8",
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
- "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;
@@ -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
  }
@@ -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
+ }