@hed-hog/core 0.0.390 → 0.0.393

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 (528) hide show
  1. package/README.md +39 -0
  2. package/dist/access-log/access-log.controller.d.ts.map +1 -1
  3. package/dist/access-log/access-log.controller.js +1 -1
  4. package/dist/access-log/access-log.controller.js.map +1 -1
  5. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  6. package/dist/access-log/access-log.interceptor.js +8 -5
  7. package/dist/access-log/access-log.interceptor.js.map +1 -1
  8. package/dist/access-log/access-log.service.d.ts +4 -0
  9. package/dist/access-log/access-log.service.d.ts.map +1 -1
  10. package/dist/access-log/access-log.service.js +31 -4
  11. package/dist/access-log/access-log.service.js.map +1 -1
  12. package/dist/access-log/dto/query-access-log.dto.d.ts +2 -0
  13. package/dist/access-log/dto/query-access-log.dto.d.ts.map +1 -1
  14. package/dist/access-log/dto/query-access-log.dto.js +5 -0
  15. package/dist/access-log/dto/query-access-log.dto.js.map +1 -1
  16. package/dist/ai/ai.controller.d.ts +12 -8
  17. package/dist/ai/ai.controller.d.ts.map +1 -1
  18. package/dist/ai/ai.controller.js +17 -2
  19. package/dist/ai/ai.controller.js.map +1 -1
  20. package/dist/ai/ai.module.d.ts.map +1 -1
  21. package/dist/ai/ai.module.js +3 -2
  22. package/dist/ai/ai.module.js.map +1 -1
  23. package/dist/ai/ai.service.d.ts +30 -1
  24. package/dist/ai/ai.service.d.ts.map +1 -1
  25. package/dist/ai/ai.service.js +97 -3
  26. package/dist/ai/ai.service.js.map +1 -1
  27. package/dist/ai/dto/richtext-edit-html.dto.d.ts +5 -0
  28. package/dist/ai/dto/richtext-edit-html.dto.d.ts.map +1 -0
  29. package/dist/ai/dto/richtext-edit-html.dto.js +27 -0
  30. package/dist/ai/dto/richtext-edit-html.dto.js.map +1 -0
  31. package/dist/ai/richtext-ai.prompts.d.ts +5 -0
  32. package/dist/ai/richtext-ai.prompts.d.ts.map +1 -0
  33. package/dist/ai/richtext-ai.prompts.js +57 -0
  34. package/dist/ai/richtext-ai.prompts.js.map +1 -0
  35. package/dist/ai/richtext-ai.service.d.ts +34 -0
  36. package/dist/ai/richtext-ai.service.d.ts.map +1 -0
  37. package/dist/ai/richtext-ai.service.js +95 -0
  38. package/dist/ai/richtext-ai.service.js.map +1 -0
  39. package/dist/ai-instruction/ai-instruction.controller.d.ts +5 -5
  40. package/dist/auth/auth.controller.d.ts +66 -7
  41. package/dist/auth/auth.controller.d.ts.map +1 -1
  42. package/dist/auth/auth.controller.js +67 -23
  43. package/dist/auth/auth.controller.js.map +1 -1
  44. package/dist/auth/auth.module.d.ts.map +1 -1
  45. package/dist/auth/auth.module.js +2 -0
  46. package/dist/auth/auth.module.js.map +1 -1
  47. package/dist/auth/auth.service.d.ts +136 -4
  48. package/dist/auth/auth.service.d.ts.map +1 -1
  49. package/dist/auth/auth.service.js +246 -48
  50. package/dist/auth/auth.service.js.map +1 -1
  51. package/dist/auth/dto/confirm-email.dto.d.ts +4 -0
  52. package/dist/auth/dto/confirm-email.dto.d.ts.map +1 -0
  53. package/dist/auth/dto/confirm-email.dto.js +22 -0
  54. package/dist/auth/dto/confirm-email.dto.js.map +1 -0
  55. package/dist/auth/dto/resend-confirmation.dto.d.ts +8 -0
  56. package/dist/auth/dto/resend-confirmation.dto.d.ts.map +1 -0
  57. package/dist/auth/dto/resend-confirmation.dto.js +36 -0
  58. package/dist/auth/dto/resend-confirmation.dto.js.map +1 -0
  59. package/dist/authorize/authorize.controller.d.ts +1 -1
  60. package/dist/authorize/authorize.controller.d.ts.map +1 -1
  61. package/dist/authorize/authorize.controller.js +4 -2
  62. package/dist/authorize/authorize.controller.js.map +1 -1
  63. package/dist/authorize/authorize.service.d.ts +7 -0
  64. package/dist/authorize/authorize.service.d.ts.map +1 -1
  65. package/dist/authorize/authorize.service.js +1 -1
  66. package/dist/authorize/authorize.service.js.map +1 -1
  67. package/dist/challenge/challenge.service.d.ts +18 -5
  68. package/dist/challenge/challenge.service.d.ts.map +1 -1
  69. package/dist/challenge/challenge.service.js +50 -3
  70. package/dist/challenge/challenge.service.js.map +1 -1
  71. package/dist/core.module.d.ts.map +1 -1
  72. package/dist/core.module.js +17 -0
  73. package/dist/core.module.js.map +1 -1
  74. package/dist/dashboard/dashboard/dashboard.controller.d.ts +6 -6
  75. package/dist/dashboard/dashboard/dashboard.service.d.ts +6 -6
  76. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +4 -4
  77. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +4 -4
  78. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -1
  79. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -1
  80. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +2 -2
  81. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
  82. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +2 -2
  83. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +2 -2
  84. package/dist/file/file.controller.d.ts +8 -1
  85. package/dist/file/file.controller.d.ts.map +1 -1
  86. package/dist/file/file.controller.js +45 -7
  87. package/dist/file/file.controller.js.map +1 -1
  88. package/dist/file/file.service.d.ts +128 -1
  89. package/dist/file/file.service.d.ts.map +1 -1
  90. package/dist/file/file.service.js +319 -8
  91. package/dist/file/file.service.js.map +1 -1
  92. package/dist/file/image-optimization.service.d.ts +43 -0
  93. package/dist/file/image-optimization.service.d.ts.map +1 -1
  94. package/dist/file/image-optimization.service.js +154 -0
  95. package/dist/file/image-optimization.service.js.map +1 -1
  96. package/dist/file/provider/abstract.provider.d.ts +16 -0
  97. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  98. package/dist/file/provider/abstract.provider.js +4 -0
  99. package/dist/file/provider/abstract.provider.js.map +1 -1
  100. package/dist/file/provider/gcs.provider.d.ts +2 -0
  101. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  102. package/dist/file/provider/gcs.provider.js +10 -0
  103. package/dist/file/provider/gcs.provider.js.map +1 -1
  104. package/dist/file/provider/local.provider.d.ts +1 -0
  105. package/dist/file/provider/local.provider.d.ts.map +1 -1
  106. package/dist/file/provider/local.provider.js +13 -0
  107. package/dist/file/provider/local.provider.js.map +1 -1
  108. package/dist/file/provider/s3.provider.d.ts +5 -0
  109. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  110. package/dist/file/provider/s3.provider.js +15 -0
  111. package/dist/file/provider/s3.provider.js.map +1 -1
  112. package/dist/file/provider/spaces.provider.d.ts +5 -0
  113. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  114. package/dist/file/provider/spaces.provider.js +16 -0
  115. package/dist/file/provider/spaces.provider.js.map +1 -1
  116. package/dist/impersonation/dto/redeem-impersonation.dto.d.ts +4 -0
  117. package/dist/impersonation/dto/redeem-impersonation.dto.d.ts.map +1 -0
  118. package/dist/impersonation/dto/redeem-impersonation.dto.js +23 -0
  119. package/dist/impersonation/dto/redeem-impersonation.dto.js.map +1 -0
  120. package/dist/impersonation/dto/start-impersonation.dto.d.ts +7 -0
  121. package/dist/impersonation/dto/start-impersonation.dto.d.ts.map +1 -0
  122. package/dist/impersonation/dto/start-impersonation.dto.js +27 -0
  123. package/dist/impersonation/dto/start-impersonation.dto.js.map +1 -0
  124. package/dist/impersonation/impersonation-restrictions.d.ts +26 -0
  125. package/dist/impersonation/impersonation-restrictions.d.ts.map +1 -0
  126. package/dist/impersonation/impersonation-restrictions.js +76 -0
  127. package/dist/impersonation/impersonation-restrictions.js.map +1 -0
  128. package/dist/impersonation/impersonation.controller.d.ts +90 -0
  129. package/dist/impersonation/impersonation.controller.d.ts.map +1 -0
  130. package/dist/impersonation/impersonation.controller.js +135 -0
  131. package/dist/impersonation/impersonation.controller.js.map +1 -0
  132. package/dist/impersonation/impersonation.module.d.ts +8 -0
  133. package/dist/impersonation/impersonation.module.d.ts.map +1 -0
  134. package/dist/impersonation/impersonation.module.js +44 -0
  135. package/dist/impersonation/impersonation.module.js.map +1 -0
  136. package/dist/impersonation/impersonation.service.d.ts +164 -0
  137. package/dist/impersonation/impersonation.service.d.ts.map +1 -0
  138. package/dist/impersonation/impersonation.service.js +408 -0
  139. package/dist/impersonation/impersonation.service.js.map +1 -0
  140. package/dist/index.d.ts +17 -0
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +19 -0
  143. package/dist/index.js.map +1 -1
  144. package/dist/integration/services/outbox-polling.coordinator.d.ts +2 -0
  145. package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -1
  146. package/dist/integration/services/outbox-polling.coordinator.js +50 -30
  147. package/dist/integration/services/outbox-polling.coordinator.js.map +1 -1
  148. package/dist/integration/services/outbox.service.d.ts.map +1 -1
  149. package/dist/integration/services/outbox.service.js +5 -1
  150. package/dist/integration/services/outbox.service.js.map +1 -1
  151. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  152. package/dist/integration-profile/integration-profile.secrets.js +3 -0
  153. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  154. package/dist/integration-profile/integration-profile.utils.d.ts +14 -0
  155. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  156. package/dist/integration-profile/integration-profile.utils.js +26 -0
  157. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  158. package/dist/integration-profile/testers/email.testers.d.ts.map +1 -1
  159. package/dist/integration-profile/testers/email.testers.js +28 -0
  160. package/dist/integration-profile/testers/email.testers.js.map +1 -1
  161. package/dist/language/en.json +11 -0
  162. package/dist/language/pt.json +11 -0
  163. package/dist/mail/dto/send.dto.d.ts +38 -1
  164. package/dist/mail/dto/send.dto.d.ts.map +1 -1
  165. package/dist/mail/dto/send.dto.js +10 -0
  166. package/dist/mail/dto/send.dto.js.map +1 -1
  167. package/dist/mail/mail.controller.d.ts +1 -0
  168. package/dist/mail/mail.controller.d.ts.map +1 -1
  169. package/dist/mail/mail.module.d.ts.map +1 -1
  170. package/dist/mail/mail.module.js +2 -0
  171. package/dist/mail/mail.module.js.map +1 -1
  172. package/dist/mail/mail.service.d.ts +48 -4
  173. package/dist/mail/mail.service.d.ts.map +1 -1
  174. package/dist/mail/mail.service.js +187 -35
  175. package/dist/mail/mail.service.js.map +1 -1
  176. package/dist/mail-delivery/mail-delivery-provision.service.d.ts +21 -0
  177. package/dist/mail-delivery/mail-delivery-provision.service.d.ts.map +1 -0
  178. package/dist/mail-delivery/mail-delivery-provision.service.js +64 -0
  179. package/dist/mail-delivery/mail-delivery-provision.service.js.map +1 -0
  180. package/dist/mail-delivery/mail-delivery.constants.d.ts +17 -0
  181. package/dist/mail-delivery/mail-delivery.constants.d.ts.map +1 -0
  182. package/dist/mail-delivery/mail-delivery.constants.js +31 -0
  183. package/dist/mail-delivery/mail-delivery.constants.js.map +1 -0
  184. package/dist/mail-delivery/mail-delivery.controller.d.ts +19 -0
  185. package/dist/mail-delivery/mail-delivery.controller.d.ts.map +1 -0
  186. package/dist/mail-delivery/mail-delivery.controller.js +53 -0
  187. package/dist/mail-delivery/mail-delivery.controller.js.map +1 -0
  188. package/dist/mail-delivery/mail-delivery.module.d.ts +7 -0
  189. package/dist/mail-delivery/mail-delivery.module.d.ts.map +1 -0
  190. package/dist/mail-delivery/mail-delivery.module.js +36 -0
  191. package/dist/mail-delivery/mail-delivery.module.js.map +1 -0
  192. package/dist/mail-delivery/mail-delivery.service.d.ts +38 -0
  193. package/dist/mail-delivery/mail-delivery.service.d.ts.map +1 -0
  194. package/dist/mail-delivery/mail-delivery.service.js +140 -0
  195. package/dist/mail-delivery/mail-delivery.service.js.map +1 -0
  196. package/dist/mail-delivery/ses-event.parser.d.ts +27 -0
  197. package/dist/mail-delivery/ses-event.parser.d.ts.map +1 -0
  198. package/dist/mail-delivery/ses-event.parser.js +131 -0
  199. package/dist/mail-delivery/ses-event.parser.js.map +1 -0
  200. package/dist/mail-delivery/ses-webhook.service.d.ts +58 -0
  201. package/dist/mail-delivery/ses-webhook.service.d.ts.map +1 -0
  202. package/dist/mail-delivery/ses-webhook.service.js +214 -0
  203. package/dist/mail-delivery/ses-webhook.service.js.map +1 -0
  204. package/dist/mail-delivery/sns-message.util.d.ts +38 -0
  205. package/dist/mail-delivery/sns-message.util.d.ts.map +1 -0
  206. package/dist/mail-delivery/sns-message.util.js +109 -0
  207. package/dist/mail-delivery/sns-message.util.js.map +1 -0
  208. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +5 -0
  209. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -1
  210. package/dist/mail-sent/dto/query-mail-sent-list.dto.js +6 -0
  211. package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -1
  212. package/dist/mail-sent/mail-sent.controller.d.ts +39 -0
  213. package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
  214. package/dist/mail-sent/mail-sent.service.d.ts +39 -0
  215. package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
  216. package/dist/mail-sent/mail-sent.service.js +45 -4
  217. package/dist/mail-sent/mail-sent.service.js.map +1 -1
  218. package/dist/mailbox/mailbox-fetcher.registry.d.ts +100 -0
  219. package/dist/mailbox/mailbox-fetcher.registry.d.ts.map +1 -0
  220. package/dist/mailbox/mailbox-fetcher.registry.js +59 -0
  221. package/dist/mailbox/mailbox-fetcher.registry.js.map +1 -0
  222. package/dist/mailbox/mailbox.module.d.ts +8 -0
  223. package/dist/mailbox/mailbox.module.d.ts.map +1 -0
  224. package/dist/mailbox/mailbox.module.js +27 -0
  225. package/dist/mailbox/mailbox.module.js.map +1 -0
  226. package/dist/meeting/meeting-provider.registry.d.ts +155 -0
  227. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -0
  228. package/dist/meeting/meeting-provider.registry.js +53 -0
  229. package/dist/meeting/meeting-provider.registry.js.map +1 -0
  230. package/dist/meeting/meeting.constants.d.ts +47 -0
  231. package/dist/meeting/meeting.constants.d.ts.map +1 -0
  232. package/dist/meeting/meeting.constants.js +21 -0
  233. package/dist/meeting/meeting.constants.js.map +1 -0
  234. package/dist/meeting/meeting.module.d.ts +7 -0
  235. package/dist/meeting/meeting.module.d.ts.map +1 -0
  236. package/dist/meeting/meeting.module.js +26 -0
  237. package/dist/meeting/meeting.module.js.map +1 -0
  238. package/dist/middlewares/locale-injection.middleware.d.ts +12 -0
  239. package/dist/middlewares/locale-injection.middleware.d.ts.map +1 -1
  240. package/dist/middlewares/locale-injection.middleware.js +20 -4
  241. package/dist/middlewares/locale-injection.middleware.js.map +1 -1
  242. package/dist/oauth/providers/google.provider.d.ts +7 -0
  243. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  244. package/dist/oauth/providers/google.provider.js +43 -13
  245. package/dist/oauth/providers/google.provider.js.map +1 -1
  246. package/dist/observability/monitored-cron.d.ts +34 -0
  247. package/dist/observability/monitored-cron.d.ts.map +1 -0
  248. package/dist/observability/monitored-cron.js +118 -0
  249. package/dist/observability/monitored-cron.js.map +1 -0
  250. package/dist/pairing/dto/approve-reverse-pairing.dto.d.ts +12 -0
  251. package/dist/pairing/dto/approve-reverse-pairing.dto.d.ts.map +1 -0
  252. package/dist/pairing/dto/approve-reverse-pairing.dto.js +30 -0
  253. package/dist/pairing/dto/approve-reverse-pairing.dto.js.map +1 -0
  254. package/dist/pairing/dto/claim-pairing.dto.d.ts +11 -0
  255. package/dist/pairing/dto/claim-pairing.dto.d.ts.map +1 -0
  256. package/dist/pairing/dto/claim-pairing.dto.js +35 -0
  257. package/dist/pairing/dto/claim-pairing.dto.js.map +1 -0
  258. package/dist/pairing/dto/confirm-pairing.dto.d.ts +10 -0
  259. package/dist/pairing/dto/confirm-pairing.dto.d.ts.map +1 -0
  260. package/dist/pairing/dto/confirm-pairing.dto.js +24 -0
  261. package/dist/pairing/dto/confirm-pairing.dto.js.map +1 -0
  262. package/dist/pairing/dto/poll-reverse-pairing.dto.d.ts +5 -0
  263. package/dist/pairing/dto/poll-reverse-pairing.dto.d.ts.map +1 -0
  264. package/dist/pairing/dto/poll-reverse-pairing.dto.js +24 -0
  265. package/dist/pairing/dto/poll-reverse-pairing.dto.js.map +1 -0
  266. package/dist/pairing/dto/start-pairing.dto.d.ts +11 -0
  267. package/dist/pairing/dto/start-pairing.dto.d.ts.map +1 -0
  268. package/dist/pairing/dto/start-pairing.dto.js +30 -0
  269. package/dist/pairing/dto/start-pairing.dto.js.map +1 -0
  270. package/dist/pairing/dto/start-reverse-pairing.dto.d.ts +24 -0
  271. package/dist/pairing/dto/start-reverse-pairing.dto.d.ts.map +1 -0
  272. package/dist/pairing/dto/start-reverse-pairing.dto.js +47 -0
  273. package/dist/pairing/dto/start-reverse-pairing.dto.js.map +1 -0
  274. package/dist/pairing/pairing.controller.d.ts +118 -0
  275. package/dist/pairing/pairing.controller.d.ts.map +1 -0
  276. package/dist/pairing/pairing.controller.js +296 -0
  277. package/dist/pairing/pairing.controller.js.map +1 -0
  278. package/dist/pairing/pairing.module.d.ts +10 -0
  279. package/dist/pairing/pairing.module.d.ts.map +1 -0
  280. package/dist/pairing/pairing.module.js +40 -0
  281. package/dist/pairing/pairing.module.js.map +1 -0
  282. package/dist/pairing/pairing.service.d.ts +238 -0
  283. package/dist/pairing/pairing.service.d.ts.map +1 -0
  284. package/dist/pairing/pairing.service.js +495 -0
  285. package/dist/pairing/pairing.service.js.map +1 -0
  286. package/dist/pdf/pdf.service.d.ts +3 -6
  287. package/dist/pdf/pdf.service.d.ts.map +1 -1
  288. package/dist/pdf/pdf.service.js +8 -20
  289. package/dist/pdf/pdf.service.js.map +1 -1
  290. package/dist/profile/profile.controller.d.ts +11 -0
  291. package/dist/profile/profile.controller.d.ts.map +1 -1
  292. package/dist/profile/profile.service.d.ts +11 -0
  293. package/dist/profile/profile.service.d.ts.map +1 -1
  294. package/dist/profile/profile.service.js +20 -11
  295. package/dist/profile/profile.service.js.map +1 -1
  296. package/dist/redis/redis-pubsub.d.ts.map +1 -1
  297. package/dist/redis/redis-pubsub.js +44 -0
  298. package/dist/redis/redis-pubsub.js.map +1 -1
  299. package/dist/role/guards/role.guard.d.ts +5 -0
  300. package/dist/role/guards/role.guard.d.ts.map +1 -1
  301. package/dist/role/guards/role.guard.js +41 -39
  302. package/dist/role/guards/role.guard.js.map +1 -1
  303. package/dist/session/session.controller.d.ts +2 -0
  304. package/dist/session/session.controller.d.ts.map +1 -1
  305. package/dist/session/session.service.d.ts +39 -0
  306. package/dist/session/session.service.d.ts.map +1 -1
  307. package/dist/session/session.service.js +91 -8
  308. package/dist/session/session.service.js.map +1 -1
  309. package/dist/setting/brand-asset.service.d.ts +48 -0
  310. package/dist/setting/brand-asset.service.d.ts.map +1 -0
  311. package/dist/setting/brand-asset.service.js +130 -0
  312. package/dist/setting/brand-asset.service.js.map +1 -0
  313. package/dist/setting/setting.module.d.ts.map +1 -1
  314. package/dist/setting/setting.module.js +3 -2
  315. package/dist/setting/setting.module.js.map +1 -1
  316. package/dist/setting/setting.service.d.ts +33 -0
  317. package/dist/setting/setting.service.d.ts.map +1 -1
  318. package/dist/setting/setting.service.js +88 -0
  319. package/dist/setting/setting.service.js.map +1 -1
  320. package/dist/token/token.service.d.ts +23 -2
  321. package/dist/token/token.service.d.ts.map +1 -1
  322. package/dist/token/token.service.js +39 -4
  323. package/dist/token/token.service.js.map +1 -1
  324. package/dist/user/user.controller.d.ts +8 -22
  325. package/dist/user/user.controller.d.ts.map +1 -1
  326. package/dist/user/user.service.d.ts +44 -24
  327. package/dist/user/user.service.d.ts.map +1 -1
  328. package/dist/user/user.service.js +89 -6
  329. package/dist/user/user.service.js.map +1 -1
  330. package/dist/user-device/user-device.controller.d.ts +54 -0
  331. package/dist/user-device/user-device.controller.d.ts.map +1 -0
  332. package/dist/user-device/user-device.controller.js +103 -0
  333. package/dist/user-device/user-device.controller.js.map +1 -0
  334. package/dist/user-device/user-device.module.d.ts +3 -0
  335. package/dist/user-device/user-device.module.d.ts.map +1 -0
  336. package/dist/user-device/user-device.module.js +26 -0
  337. package/dist/user-device/user-device.module.js.map +1 -0
  338. package/dist/user-device/user-device.service.d.ts +71 -0
  339. package/dist/user-device/user-device.service.d.ts.map +1 -0
  340. package/dist/user-device/user-device.service.js +158 -0
  341. package/dist/user-device/user-device.service.js.map +1 -0
  342. package/dist/utils/device-key.d.ts +23 -0
  343. package/dist/utils/device-key.d.ts.map +1 -0
  344. package/dist/utils/device-key.js +31 -0
  345. package/dist/utils/device-key.js.map +1 -0
  346. package/dist/utils/geo-ip.d.ts +38 -0
  347. package/dist/utils/geo-ip.d.ts.map +1 -0
  348. package/dist/utils/geo-ip.js +122 -0
  349. package/dist/utils/geo-ip.js.map +1 -0
  350. package/dist/utils/locale-context.d.ts +10 -7
  351. package/dist/utils/locale-context.d.ts.map +1 -1
  352. package/dist/utils/locale-context.js +14 -8
  353. package/dist/utils/locale-context.js.map +1 -1
  354. package/dist/utils/security-event-vars.d.ts +56 -0
  355. package/dist/utils/security-event-vars.d.ts.map +1 -0
  356. package/dist/utils/security-event-vars.js +109 -0
  357. package/dist/utils/security-event-vars.js.map +1 -0
  358. package/dist/utils/user-agent.d.ts +19 -0
  359. package/dist/utils/user-agent.d.ts.map +1 -0
  360. package/dist/utils/user-agent.js +36 -0
  361. package/dist/utils/user-agent.js.map +1 -0
  362. package/dist/webhook-integration/webhook-integration.service.d.ts +5 -0
  363. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  364. package/dist/webhook-integration/webhook-integration.service.js +50 -4
  365. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  366. package/dist/webhook-integration/webhook-protocol.registry.d.ts +6 -0
  367. package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -1
  368. package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -1
  369. package/hedhog/data/integration_provider.yaml +9 -0
  370. package/hedhog/data/mail.yaml +198 -62
  371. package/hedhog/data/mail_layout.yaml +43 -33
  372. package/hedhog/data/menu.yaml +2 -0
  373. package/hedhog/data/role.yaml +16 -0
  374. package/hedhog/data/route.yaml +172 -3
  375. package/hedhog/data/setting_group.yaml +178 -2
  376. package/hedhog/data/setting_subgroup.yaml +8 -0
  377. package/hedhog/frontend/app/_lib/hooks/use-core-access.test.tsx.ejs +125 -0
  378. package/hedhog/frontend/app/_lib/hooks/use-core-access.ts.ejs +29 -0
  379. package/hedhog/frontend/app/_lib/hooks/use-impersonation-access.test.ts.ejs +71 -0
  380. package/hedhog/frontend/app/_lib/hooks/use-impersonation-access.ts.ejs +32 -0
  381. package/hedhog/frontend/app/access-log/page.tsx.ejs +96 -22
  382. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +17 -1
  383. package/hedhog/frontend/app/account/components/known-devices.tsx.ejs +89 -0
  384. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +2 -1
  385. package/hedhog/frontend/app/account/components/user-devices-list.tsx.ejs +186 -0
  386. package/hedhog/frontend/app/account/devices/page.tsx.ejs +5 -0
  387. package/hedhog/frontend/app/account/layout.tsx.ejs +2 -0
  388. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +298 -0
  389. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +376 -1236
  390. package/hedhog/frontend/app/mail/log/page.tsx.ejs +210 -3
  391. package/hedhog/frontend/app/mail/template/page.tsx.ejs +190 -132
  392. package/hedhog/frontend/app/users/active-session.tsx.ejs +58 -11
  393. package/hedhog/frontend/app/users/page.tsx.ejs +69 -10
  394. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +202 -0
  395. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +170 -0
  396. package/hedhog/frontend/app/users/user-activity-timeline.tsx.ejs +19 -0
  397. package/hedhog/frontend/app/users/user-devices.tsx.ejs +89 -0
  398. package/hedhog/frontend/app/users/user-logs.tsx.ejs +13 -1
  399. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +46 -24
  400. package/hedhog/frontend/messages/en.json +146 -48
  401. package/hedhog/frontend/messages/pt.json +146 -48
  402. package/hedhog/table/access_log.yaml +7 -0
  403. package/hedhog/table/file.yaml +8 -0
  404. package/hedhog/table/mail_delivery_event.yaml +76 -0
  405. package/hedhog/table/mail_sent.yaml +23 -0
  406. package/hedhog/table/user_activity.yaml +9 -0
  407. package/hedhog/table/user_device.yaml +52 -0
  408. package/hedhog/table/user_session.yaml +20 -1
  409. package/package.json +6 -5
  410. package/src/access-log/access-log.controller.ts +1 -0
  411. package/src/access-log/access-log.interceptor.ts +5 -1
  412. package/src/access-log/access-log.service.spec.ts +33 -2
  413. package/src/access-log/access-log.service.ts +38 -0
  414. package/src/access-log/dto/query-access-log.dto.ts +5 -0
  415. package/src/ai/ai.controller.ts +9 -0
  416. package/src/ai/ai.module.ts +3 -2
  417. package/src/ai/ai.service.ts +150 -13
  418. package/src/ai/dto/richtext-edit-html.dto.ts +11 -0
  419. package/src/ai/richtext-ai.prompts.ts +57 -0
  420. package/src/ai/richtext-ai.service.ts +109 -0
  421. package/src/auth/auth.controller.ts +47 -8
  422. package/src/auth/auth.module.ts +2 -0
  423. package/src/auth/auth.service.spec.ts +383 -6
  424. package/src/auth/auth.service.ts +345 -56
  425. package/src/auth/dto/confirm-email.dto.ts +7 -0
  426. package/src/auth/dto/resend-confirmation.dto.ts +20 -0
  427. package/src/authorize/authorize.controller.ts +2 -0
  428. package/src/authorize/authorize.service.spec.ts +23 -0
  429. package/src/authorize/authorize.service.ts +8 -0
  430. package/src/challenge/challenge.service.spec.ts +73 -0
  431. package/src/challenge/challenge.service.ts +62 -3
  432. package/src/core.module.ts +19 -0
  433. package/src/file/file.controller.ts +54 -4
  434. package/src/file/file.service.spec.ts +275 -105
  435. package/src/file/file.service.ts +409 -14
  436. package/src/file/image-optimization.service.spec.ts +92 -0
  437. package/src/file/image-optimization.service.ts +202 -0
  438. package/src/file/provider/abstract.provider.ts +23 -0
  439. package/src/file/provider/gcs.provider.ts +17 -0
  440. package/src/file/provider/local.provider.ts +21 -0
  441. package/src/file/provider/s3.provider.ts +25 -0
  442. package/src/file/provider/spaces.provider.ts +26 -0
  443. package/src/impersonation/dto/redeem-impersonation.dto.ts +8 -0
  444. package/src/impersonation/dto/start-impersonation.dto.ts +13 -0
  445. package/src/impersonation/impersonation-restrictions.ts +84 -0
  446. package/src/impersonation/impersonation.controller.ts +109 -0
  447. package/src/impersonation/impersonation.module.ts +31 -0
  448. package/src/impersonation/impersonation.service.spec.ts +427 -0
  449. package/src/impersonation/impersonation.service.ts +577 -0
  450. package/src/index.ts +21 -0
  451. package/src/integration/services/outbox-polling.coordinator.spec.ts +90 -0
  452. package/src/integration/services/outbox-polling.coordinator.ts +62 -40
  453. package/src/integration/services/outbox.service.spec.ts +8 -0
  454. package/src/integration/services/outbox.service.ts +19 -13
  455. package/src/integration-profile/integration-profile.secrets.ts +3 -0
  456. package/src/integration-profile/integration-profile.utils.ts +38 -0
  457. package/src/integration-profile/testers/email.testers.ts +34 -1
  458. package/src/language/en.json +11 -0
  459. package/src/language/pt.json +11 -0
  460. package/src/mail/dto/send.dto.ts +46 -2
  461. package/src/mail/mail-template.spec.ts +79 -2
  462. package/src/mail/mail.module.ts +2 -0
  463. package/src/mail/mail.service.spec.ts +307 -6
  464. package/src/mail/mail.service.ts +227 -30
  465. package/src/mail-delivery/mail-delivery-provision.service.ts +66 -0
  466. package/src/mail-delivery/mail-delivery.constants.ts +31 -0
  467. package/src/mail-delivery/mail-delivery.controller.ts +38 -0
  468. package/src/mail-delivery/mail-delivery.module.ts +23 -0
  469. package/src/mail-delivery/mail-delivery.service.spec.ts +136 -0
  470. package/src/mail-delivery/mail-delivery.service.ts +152 -0
  471. package/src/mail-delivery/ses-event.parser.spec.ts +140 -0
  472. package/src/mail-delivery/ses-event.parser.ts +191 -0
  473. package/src/mail-delivery/ses-webhook.service.spec.ts +170 -0
  474. package/src/mail-delivery/ses-webhook.service.ts +240 -0
  475. package/src/mail-delivery/sns-message.util.spec.ts +163 -0
  476. package/src/mail-delivery/sns-message.util.ts +125 -0
  477. package/src/mail-sent/dto/query-mail-sent-list.dto.ts +15 -0
  478. package/src/mail-sent/mail-sent.service.ts +46 -2
  479. package/src/mailbox/mailbox-fetcher.registry.ts +132 -0
  480. package/src/mailbox/mailbox.module.ts +14 -0
  481. package/src/meeting/meeting-provider.registry.ts +193 -0
  482. package/src/meeting/meeting.constants.ts +53 -0
  483. package/src/meeting/meeting.module.ts +13 -0
  484. package/src/middlewares/locale-injection.middleware.ts +25 -6
  485. package/src/oauth/providers/google.provider.ts +49 -15
  486. package/src/observability/monitored-cron.spec.ts +102 -0
  487. package/src/observability/monitored-cron.ts +110 -0
  488. package/src/pairing/dto/approve-reverse-pairing.dto.ts +20 -0
  489. package/src/pairing/dto/claim-pairing.dto.ts +19 -0
  490. package/src/pairing/dto/confirm-pairing.dto.ts +14 -0
  491. package/src/pairing/dto/poll-reverse-pairing.dto.ts +9 -0
  492. package/src/pairing/dto/start-pairing.dto.ts +19 -0
  493. package/src/pairing/dto/start-reverse-pairing.dto.ts +45 -0
  494. package/src/pairing/pairing.controller.ts +246 -0
  495. package/src/pairing/pairing.module.ts +27 -0
  496. package/src/pairing/pairing.service.spec.ts +462 -0
  497. package/src/pairing/pairing.service.ts +614 -0
  498. package/src/pdf/pdf.service.ts +6 -24
  499. package/src/profile/profile.service.ts +26 -8
  500. package/src/redis/redis-pubsub.ts +11 -0
  501. package/src/role/guards/role.guard.spec.ts +75 -0
  502. package/src/role/guards/role.guard.ts +64 -46
  503. package/src/session/session.service.spec.ts +81 -0
  504. package/src/session/session.service.ts +93 -6
  505. package/src/setting/brand-asset.service.ts +137 -0
  506. package/src/setting/setting.module.ts +3 -2
  507. package/src/setting/setting.service.spec.ts +79 -1
  508. package/src/setting/setting.service.ts +109 -1
  509. package/src/token/token.service.spec.ts +62 -0
  510. package/src/token/token.service.ts +57 -3
  511. package/src/user/user.service.spec.ts +33 -2
  512. package/src/user/user.service.ts +108 -6
  513. package/src/user-device/user-device.controller.ts +77 -0
  514. package/src/user-device/user-device.module.ts +13 -0
  515. package/src/user-device/user-device.service.spec.ts +234 -0
  516. package/src/user-device/user-device.service.ts +168 -0
  517. package/src/utils/device-key.spec.ts +57 -0
  518. package/src/utils/device-key.ts +32 -0
  519. package/src/utils/geo-ip.spec.ts +134 -0
  520. package/src/utils/geo-ip.ts +93 -0
  521. package/src/utils/locale-context.spec.ts +27 -1
  522. package/src/utils/locale-context.ts +15 -12
  523. package/src/utils/security-event-vars.spec.ts +161 -0
  524. package/src/utils/security-event-vars.ts +153 -0
  525. package/src/utils/user-agent.spec.ts +37 -0
  526. package/src/utils/user-agent.ts +46 -0
  527. package/src/webhook-integration/webhook-integration.service.ts +57 -3
  528. package/src/webhook-integration/webhook-protocol.registry.ts +6 -0
@@ -19,6 +19,7 @@ import { Switch } from '@/components/ui/switch';
19
19
  import { TagsInput } from '@/components/ui/tags-input';
20
20
  import { ByteSizeInput } from './byte-size-input';
21
21
  import { DurationInput } from './duration-input';
22
+ import { buildAbsoluteImageUrl } from '@/lib/build-image-url';
22
23
  import { revalidateSystemIcon } from '@/lib/revalidate-icon';
23
24
  import { cn } from '@/lib/utils';
24
25
  import {
@@ -43,21 +44,6 @@ type ApiPaginated<T> = {
43
44
  lastPage: number;
44
45
  };
45
46
 
46
- type IntegrationTypeOption = {
47
- id: number;
48
- slug: string;
49
- };
50
-
51
- type IntegrationProviderOption = {
52
- id: number;
53
- slug: string;
54
- type_id: number;
55
- integration_provider_locale?: {
56
- name: string;
57
- locale: { code: string };
58
- }[];
59
- };
60
-
61
47
  type IntegrationProfileOption = {
62
48
  id: number;
63
49
  slug: string;
@@ -91,118 +77,243 @@ function getLocalizedIntegrationName(
91
77
  );
92
78
  }
93
79
 
94
- const MAIL_INTEGRATION_PROFILE_SETTING_SLUG = 'mail-integration-profile-id';
95
- const SCALING_INFRA_PROFILE_SETTING_SLUG = 'scaling.video.integration_profile_id';
96
- const HEDHOG_K8S_INFRA_PROFILE_SETTING_SLUG =
97
- 'hedhog.k8s_integration_profile_id';
98
- const COMMERCE_PAYMENT_PROFILE_SETTING_SLUG =
99
- 'commerce-default-payment-profile-id';
80
+ type IntegrationProfileSettingConfig = {
81
+ /**
82
+ * Tipo de perfil aceito pela setting. Filtra a listagem do picker e trava o
83
+ * sheet de criação. Ausente só na rede de segurança, onde listamos tudo.
84
+ */
85
+ typeSlug?: string;
86
+ /** Estreita para um provider específico (OAuth, Kubernetes). */
87
+ providerSlug?: string;
88
+ /** Providers a esconder mesmo sendo do tipo certo. */
89
+ excludeProviders?: string[];
90
+ /** Substantivo usado nos rótulos dos botões editar/criar. */
91
+ profileNameKey?: string;
92
+ };
93
+
100
94
  /**
101
- * Providers de pagamento que existem no sistema mas não servem como gateway
102
- * padrão do checkout web. O Google Play só cobra por compra dentro do app
103
- * Android (`commerce-android-checkout-mode`), então escolhê-lo aqui quebraria o
104
- * checkout do site. O backend também recusa — isto é só para não oferecer.
95
+ * Settings cujo valor é o SLUG de um integration_profile — o sufixo `-id` é
96
+ * histórico, nenhuma delas guarda o id numérico. Uma entrada aqui é tudo que
97
+ * uma setting nova precisa para ganhar o picker completo; sem ela o campo cai
98
+ * na rede de segurança de `getIntegrationProfileSettingConfig` (picker sem
99
+ * filtro de tipo).
100
+ *
101
+ * Deve espelhar `SELECT slug FROM setting WHERE component = 'entity-picker'`.
105
102
  */
106
- const NON_WEB_CHECKOUT_PAYMENT_PROVIDERS = new Set(['google_play']);
107
- const DAILY_REPORT_EMAIL_INTEGRATION_SETTING_SLUG =
108
- 'operations.daily-report.email-integration-profile-id';
109
- const DAILY_REPORT_WHATSAPP_INTEGRATION_SETTING_SLUG =
110
- 'operations.daily-report.whatsapp-integration-profile-id';
111
- const AI_PROFILE_SETTING_SLUGS = [
112
- 'ai-gemini-profile-id',
113
- 'ai-openai-profile-id',
114
- 'ai-deepseek-profile-id',
115
- 'mcp-ai-profile-id',
116
- ];
117
- const OAUTH_PROFILE_SLUG_TO_PROVIDER: Record<string, string> = {
118
- 'oauth-google-profile-id': 'google-oauth',
119
- 'oauth-facebook-profile-id': 'facebook-oauth',
120
- 'oauth-github-profile-id': 'github-oauth',
121
- 'oauth-microsoft-profile-id': 'microsoft-oauth',
122
- 'oauth-microsoft-entra-id-profile-id': 'microsoft-entra-id-oauth',
123
- 'oauth-apple-profile-id': 'apple-oauth',
124
- 'oauth-linkedin-profile-id': 'linkedin-oauth',
103
+ const INTEGRATION_PROFILE_SETTINGS: Record<
104
+ string,
105
+ IntegrationProfileSettingConfig
106
+ > = {
107
+ // IA
108
+ 'ai-openai-profile-id': { typeSlug: 'ai' },
109
+ 'ai-gemini-profile-id': { typeSlug: 'ai' },
110
+ 'ai-deepseek-profile-id': { typeSlug: 'ai' },
111
+ 'mcp-ai-profile-id': { typeSlug: 'ai' },
112
+ 'richtext-ai-profile-id': { typeSlug: 'ai' },
113
+ 'lms-ai-course-generation-profile-id': { typeSlug: 'ai' },
114
+ 'lms-ai-lesson-xp-profile-id': { typeSlug: 'ai' },
115
+ 'lms-ai-transcription-profile-id': { typeSlug: 'ai' },
116
+ 'lms-ai-course-image-profile-id': { typeSlug: 'ai' },
117
+ 'lms-ai-discussion-reply-profile-id': { typeSlug: 'ai' },
118
+ 'sac.ai.integration-profile-id': { typeSlug: 'ai' },
119
+ // Transcrição de áudio só existe na OpenAI; oferecer Gemini ou Claude aqui
120
+ // deixaria escolher um perfil que o worker recusa.
121
+ 'sac.whatsapp.transcription-profile-id': {
122
+ typeSlug: 'ai',
123
+ providerSlug: 'openai',
124
+ },
125
+ 'campaign.ai.integration-profile-id': { typeSlug: 'ai' },
126
+
127
+ // E-mail. O IMAP é do tipo `email` mas só serve para receber:
128
+ // `buildMailConfigFromIntegration` recusa usá-lo no envio, então oferecê-lo
129
+ // aqui seria deixar escolher um perfil que o backend rejeita depois.
130
+ 'mail-integration-profile-id': {
131
+ typeSlug: 'email',
132
+ excludeProviders: ['imap'],
133
+ },
134
+ 'lms-discussion-notify-email-profile-id': {
135
+ typeSlug: 'email',
136
+ excludeProviders: ['imap'],
137
+ },
138
+ 'lms-digest-notify-email-profile-id': {
139
+ typeSlug: 'email',
140
+ excludeProviders: ['imap'],
141
+ },
142
+ 'lms-celebration-notify-email-profile-id': {
143
+ typeSlug: 'email',
144
+ excludeProviders: ['imap'],
145
+ },
146
+ 'lms-class-reminder-notify-email-profile-id': {
147
+ typeSlug: 'email',
148
+ excludeProviders: ['imap'],
149
+ },
150
+ 'operations.daily-report.email-integration-profile-id': {
151
+ typeSlug: 'email',
152
+ excludeProviders: ['imap'],
153
+ },
154
+ 'operations.approval-notifications.email-integration-profile-id': {
155
+ typeSlug: 'email',
156
+ excludeProviders: ['imap'],
157
+ },
158
+
159
+ // WhatsApp
160
+ 'lms-discussion-notify-whatsapp-profile-id': { typeSlug: 'whatsapp' },
161
+ 'lms-digest-notify-whatsapp-profile-id': { typeSlug: 'whatsapp' },
162
+ 'lms-celebration-notify-whatsapp-profile-id': { typeSlug: 'whatsapp' },
163
+ 'lms-class-reminder-notify-whatsapp-profile-id': { typeSlug: 'whatsapp' },
164
+ 'operations.daily-report.whatsapp-integration-profile-id': {
165
+ typeSlug: 'whatsapp',
166
+ },
167
+ 'operations.approval-notifications.whatsapp-integration-profile-id': {
168
+ typeSlug: 'whatsapp',
169
+ },
170
+ 'sac.notify-whatsapp-profile-id': { typeSlug: 'whatsapp' },
171
+
172
+ // Armazenamento
173
+ 'file-storage-profile-id': { typeSlug: 'storage' },
174
+ 'lms-bulk-upload-storage-profile-id': { typeSlug: 'storage' },
175
+
176
+ // OAuth — um provider por setting
177
+ 'oauth-google-profile-id': {
178
+ typeSlug: 'oauth',
179
+ providerSlug: 'google-oauth',
180
+ },
181
+ 'oauth-facebook-profile-id': {
182
+ typeSlug: 'oauth',
183
+ providerSlug: 'facebook-oauth',
184
+ },
185
+ 'oauth-github-profile-id': {
186
+ typeSlug: 'oauth',
187
+ providerSlug: 'github-oauth',
188
+ },
189
+ 'oauth-microsoft-profile-id': {
190
+ typeSlug: 'oauth',
191
+ providerSlug: 'microsoft-oauth',
192
+ },
193
+ 'oauth-microsoft-entra-id-profile-id': {
194
+ typeSlug: 'oauth',
195
+ providerSlug: 'microsoft-entra-id-oauth',
196
+ },
197
+ 'oauth-apple-profile-id': { typeSlug: 'oauth', providerSlug: 'apple-oauth' },
198
+ 'oauth-linkedin-profile-id': {
199
+ typeSlug: 'oauth',
200
+ providerSlug: 'linkedin-oauth',
201
+ },
202
+
203
+ // Pagamento. O Google Play só cobra por compra dentro do app Android
204
+ // (`commerce-android-checkout-mode`), então escolhê-lo como gateway padrão
205
+ // quebraria o checkout do site. O backend também recusa — isto é só para não
206
+ // oferecer.
207
+ 'commerce-default-payment-profile-id': {
208
+ typeSlug: 'payment',
209
+ excludeProviders: ['google_play'],
210
+ },
211
+
212
+ // Infraestrutura
213
+ 'scaling.video.integration_profile_id': { typeSlug: 'infrastructure' },
214
+ 'hedhog.k8s_integration_profile_id': {
215
+ typeSlug: 'infrastructure',
216
+ providerSlug: 'kubernetes',
217
+ profileNameKey: 'profileTypeNames.kubernetes',
218
+ },
125
219
  };
126
- // Settings cujo valor é o SLUG de um integration_profile. O picker continua sendo
127
- // selecionado/exibido pelo nome; apenas o valor persistido é o slug.
128
- const ALL_INTEGRATION_PROFILE_SETTING_SLUGS = [
129
- MAIL_INTEGRATION_PROFILE_SETTING_SLUG,
130
- SCALING_INFRA_PROFILE_SETTING_SLUG,
131
- HEDHOG_K8S_INFRA_PROFILE_SETTING_SLUG,
132
- COMMERCE_PAYMENT_PROFILE_SETTING_SLUG,
133
- DAILY_REPORT_EMAIL_INTEGRATION_SETTING_SLUG,
134
- DAILY_REPORT_WHATSAPP_INTEGRATION_SETTING_SLUG,
135
- 'file-storage-profile-id',
136
- 'lms-bulk-upload-storage-profile-id',
137
- ...AI_PROFILE_SETTING_SLUGS,
138
- ...Object.keys(OAUTH_PROFILE_SLUG_TO_PROVIDER),
139
- ];
140
-
141
- function toSlug(value: string) {
142
- return (value || 'profile')
143
- .toLowerCase()
144
- .normalize('NFD')
145
- .replace(/[\u0300-\u036f]/g, '')
146
- .replace(/[^a-z0-9]+/g, '-')
147
- .replace(/^-+|-+$/g, '');
148
- }
149
220
 
150
- function getDailyReportIntegrationTypeSlug(settingSlug: string) {
151
- if (settingSlug === DAILY_REPORT_EMAIL_INTEGRATION_SETTING_SLUG) {
152
- return 'email';
153
- }
221
+ /** Chaves de tradução do picker, por tipo de perfil. */
222
+ const PROFILE_TYPE_TRANSLATION_KEYS: Record<
223
+ string,
224
+ { placeholder: string; emptyState: string; name: string }
225
+ > = {
226
+ ai: {
227
+ placeholder: 'selectAiProfile',
228
+ emptyState: 'aiProfileEmptyState',
229
+ name: 'profileTypeNames.ai',
230
+ },
231
+ email: {
232
+ placeholder: 'selectMailProfile',
233
+ emptyState: 'mailProfileEmptyState',
234
+ name: 'profileTypeNames.email',
235
+ },
236
+ whatsapp: {
237
+ placeholder: 'selectWhatsAppProfile',
238
+ emptyState: 'whatsappProfileEmptyState',
239
+ name: 'profileTypeNames.whatsapp',
240
+ },
241
+ storage: {
242
+ placeholder: 'selectStorageProfile',
243
+ emptyState: 'storageProfileEmptyState',
244
+ name: 'profileTypeNames.storage',
245
+ },
246
+ oauth: {
247
+ placeholder: 'selectOAuthProfile',
248
+ emptyState: 'oauthProfileEmptyState',
249
+ name: 'profileTypeNames.oauth',
250
+ },
251
+ payment: {
252
+ placeholder: 'selectPaymentProfile',
253
+ emptyState: 'paymentProfileEmptyState',
254
+ name: 'profileTypeNames.payment',
255
+ },
256
+ infrastructure: {
257
+ placeholder: 'selectInfraProfile',
258
+ emptyState: 'infraProfileEmptyState',
259
+ name: 'profileTypeNames.infrastructure',
260
+ },
261
+ };
154
262
 
155
- if (settingSlug === DAILY_REPORT_WHATSAPP_INTEGRATION_SETTING_SLUG) {
156
- return 'whatsapp';
157
- }
263
+ const GENERIC_PROFILE_TRANSLATION_KEYS = {
264
+ placeholder: 'selectIntegrationProfile',
265
+ emptyState: 'integrationProfileEmptyState',
266
+ name: 'profileTypeNames.generic',
267
+ };
158
268
 
159
- return null;
269
+ /**
270
+ * Convenção de nome das settings que guardam um perfil: `...-profile-id`,
271
+ * `...integration_profile_id` e variantes.
272
+ */
273
+ const PROFILE_SETTING_SLUG_PATTERN =
274
+ /(^|[.\-_])(integration[-_])?profile[-_]id$/;
275
+
276
+ const warnedUnmappedProfileSettings = new Set<string>();
277
+
278
+ /** Settings cujo valor exibido precisa ser resolvido a partir do slug salvo. */
279
+ function isIntegrationProfileSetting(settingSlug: string) {
280
+ return (
281
+ settingSlug in INTEGRATION_PROFILE_SETTINGS ||
282
+ PROFILE_SETTING_SLUG_PATTERN.test(settingSlug)
283
+ );
160
284
  }
161
285
 
162
- function buildIntegrationProfileConfig(
163
- providerSlug: string,
164
- values: Record<string, string>
165
- ) {
166
- const trim = (key: string) => String(values[key] ?? '').trim();
167
-
168
- if (providerSlug === 'smtp') {
169
- return {
170
- host: trim('host'),
171
- port: Number(trim('port') || 587),
172
- username: trim('username'),
173
- password: trim('password'),
174
- secure: values.secure === 'true',
175
- from_email: trim('from_email'),
176
- from_name: trim('from_name'),
177
- reply_to_email: trim('reply_to_email'),
178
- };
286
+ /**
287
+ * Config do picker para uma setting `entity-picker`. Slug fora do registry mas
288
+ * com cara de campo de perfil ainda ganha um picker — sem filtro de tipo, mas
289
+ * melhor do que obrigar o administrador a digitar o slug à mão (era o que
290
+ * acontecia com `sac.ai.integration-profile-id`). `null` = não é campo de
291
+ * perfil, renderiza como texto.
292
+ */
293
+ function getIntegrationProfileSettingConfig(
294
+ settingSlug: string
295
+ ): IntegrationProfileSettingConfig | null {
296
+ const config = INTEGRATION_PROFILE_SETTINGS[settingSlug];
297
+ if (config) {
298
+ return config;
179
299
  }
180
300
 
181
- if (providerSlug === 'gmail') {
182
- return {
183
- client_id: trim('client_id'),
184
- client_secret: trim('client_secret'),
185
- refresh_token: trim('refresh_token'),
186
- from_email: trim('from_email'),
187
- from_name: trim('from_name'),
188
- };
301
+ if (!PROFILE_SETTING_SLUG_PATTERN.test(settingSlug)) {
302
+ return null;
189
303
  }
190
304
 
191
- if (providerSlug === 'ses') {
192
- return {
193
- access_key_id: trim('access_key_id'),
194
- secret_access_key: trim('secret_access_key'),
195
- region: trim('region'),
196
- from_email: trim('from_email'),
197
- from_name: trim('from_name'),
198
- };
305
+ if (
306
+ process.env.NODE_ENV !== 'production' &&
307
+ !warnedUnmappedProfileSettings.has(settingSlug)
308
+ ) {
309
+ warnedUnmappedProfileSettings.add(settingSlug);
310
+ console.warn(
311
+ `[setting-field] "${settingSlug}" não está em INTEGRATION_PROFILE_SETTINGS; ` +
312
+ 'listando perfis de integração de todos os tipos.'
313
+ );
199
314
  }
200
315
 
201
- return {
202
- host: trim('host'),
203
- token: trim('token'),
204
- instance_name: trim('instance_name'),
205
- };
316
+ return {};
206
317
  }
207
318
 
208
319
  export const SettingField = ({ setting, className }: SettingFieldProps) => {
@@ -300,14 +411,16 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
300
411
  }, [localValue]);
301
412
 
302
413
  useEffect(() => {
303
- if (!ALL_INTEGRATION_PROFILE_SETTING_SLUGS.includes(setting.slug)) {
414
+ if (!isIntegrationProfileSetting(setting.slug)) {
304
415
  setStorageProfileSelectedLabel('');
305
416
  setResolvedProfileId(null);
306
417
  return;
307
418
  }
308
419
 
420
+ // `'0'` é o default herdado das settings antigas que nomeavam o campo como
421
+ // "profile id": não é slug de perfil nenhum, e buscá-lo devolveria vazio.
309
422
  const profileSlug = String(localValue ?? '').trim();
310
- if (!profileSlug) {
423
+ if (!profileSlug || profileSlug === '0') {
311
424
  setStorageProfileSelectedLabel('');
312
425
  setResolvedProfileId(null);
313
426
  return;
@@ -503,28 +616,24 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
503
616
  },
504
617
  });
505
618
 
506
- const fileId = response.data?.id;
619
+ const fileId = Number(response.data?.id);
507
620
 
508
621
  if (!fileId) {
509
622
  throw new Error(t('fileIdNotReturned'));
510
623
  }
511
624
 
512
- const openResponse = await request<{ url?: string }>({
513
- url: `/file/open/${fileId}`,
514
- method: 'PUT',
515
- });
625
+ // `/file/image/:id` é público, imutável (Cache-Control + ETag) e não
626
+ // expira. A URL de `PUT /file/open/:id`, gravada aqui antes, carrega
627
+ // um JWT de 3600s: a setting virava link morto uma hora depois do
628
+ // upload. Absoluta de propósito — o mesmo valor é lido pelo <img> da
629
+ // sidebar, pelo favicon dinâmico, pelo metadata do layout e pelo
630
+ // e-mail transacional, que roda fora do browser.
631
+ const fileUrl = buildAbsoluteImageUrl(fileId);
516
632
 
517
- const openUrl = openResponse?.data?.url;
518
-
519
- if (!openUrl) {
520
- throw new Error(t('temporaryOpenUrlNotReturned'));
633
+ if (!fileUrl) {
634
+ throw new Error(t('apiBaseUrlNotConfigured'));
521
635
  }
522
636
 
523
- const fileUrl =
524
- typeof openUrl === 'string' && openUrl.startsWith('http')
525
- ? openUrl
526
- : `${String(process.env.NEXT_PUBLIC_API_BASE_URL || '')}${openUrl}`;
527
-
528
637
  handleChangeValue(formatValue(fileUrl));
529
638
  showToastHandler('success', t('fileUploaded'));
530
639
  } catch (error) {
@@ -741,789 +850,11 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
741
850
  );
742
851
  }
743
852
 
744
- case SettingComponentEnum.ENTITY_PICKER:
745
- const dailyReportIntegrationTypeSlug = getDailyReportIntegrationTypeSlug(
746
- setting.slug
747
- );
748
-
749
- if (
750
- setting.slug === 'file-storage-profile-id' ||
751
- setting.slug === 'lms-bulk-upload-storage-profile-id'
752
- ) {
753
- const loadStorageTypeId = async () => {
754
- const typesResponse = await request<IntegrationTypeOption[]>({
755
- url: '/integration-profile/types',
756
- method: 'GET',
757
- });
758
- const types = (typesResponse.data ?? []) as IntegrationTypeOption[];
759
- const typeId = types.find((type) => type.slug === 'storage')?.id;
760
-
761
- if (!typeId) {
762
- throw new Error('Integration type "storage" not found.');
763
- }
764
-
765
- return typeId;
766
- };
853
+ case SettingComponentEnum.ENTITY_PICKER: {
854
+ const profileConfig = getIntegrationProfileSettingConfig(setting.slug);
767
855
 
768
- return (
769
- <>
770
- <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
771
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
772
- placeholder={t('selectStorageProfile')}
773
- emptyStateDescription={t('storageProfileEmptyState')}
774
- value={parsedValue || null}
775
- onChange={(slug, option) => {
776
- setStorageProfileSelectedLabel(option?.name ?? '');
777
- setResolvedProfileId(
778
- option ? Number((option as { id?: number }).id) || null : null
779
- );
780
- handleChangeValue(slug != null ? String(slug) : '', true);
781
- }}
782
- valueType="string"
783
- initialSelectedLabel={storageProfileSelectedLabel}
784
- showCreateButton={false}
785
- clearable={false}
786
- loadOptions={async ({ page, pageSize, search }) => {
787
- const typeId = await loadStorageTypeId();
788
- const qs = new URLSearchParams({
789
- page: String(page),
790
- pageSize: String(pageSize),
791
- typeId: String(typeId),
792
- });
793
- if (search) qs.set('search', search);
794
- const response = await request<
795
- ApiPaginated<
796
- IntegrationProfileOption & Record<string, unknown>
797
- >
798
- >({
799
- url: `/integration-profile?${qs}`,
800
- method: 'GET',
801
- });
802
- const paged = response.data;
803
- return {
804
- items: paged?.data ?? [],
805
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
806
- };
807
- }}
808
- getOptionValue={(opt) => opt.slug}
809
- getOptionLabel={(opt) => String(opt.name ?? '')}
810
- getOptionDescription={(opt) => {
811
- const providerSlug = String(
812
- opt.integration_provider?.slug ?? ''
813
- );
814
- const providerName = getLocalizedIntegrationName(
815
- opt.integration_provider?.integration_provider_locale,
816
- currentLocaleCode
817
- );
818
- return [providerName || providerSlug, opt.slug]
819
- .filter(Boolean)
820
- .join(' • ');
821
- }}
822
- className={cn('flex-1 min-w-0', className)}
823
- />
824
-
825
- <Button
826
- type="button"
827
- variant="outline"
828
- size="icon"
829
- className="h-10 w-10 shrink-0"
830
- onClick={() => {
831
- setStorageProfileEditingId(resolvedProfileId);
832
- setIsStorageProfileSheetOpen(true);
833
- }}
834
- disabled={!resolvedProfileId}
835
- aria-label="Editar perfil de armazenamento"
836
- title="Editar perfil de armazenamento"
837
- >
838
- <Pencil className="h-4 w-4" />
839
- </Button>
840
-
841
- <Button
842
- type="button"
843
- variant="outline"
844
- size="icon"
845
- className="h-10 w-10 shrink-0"
846
- onClick={() => {
847
- setStorageProfileEditingId(null);
848
- setIsStorageProfileSheetOpen(true);
849
- }}
850
- aria-label="Criar perfil de armazenamento"
851
- title="Criar perfil de armazenamento"
852
- >
853
- <Plus className="h-4 w-4" />
854
- </Button>
855
- </div>
856
-
857
- <IntegrationProfileSheet
858
- open={isStorageProfileSheetOpen}
859
- onOpenChange={(nextOpen) => {
860
- setIsStorageProfileSheetOpen(nextOpen);
861
- if (!nextOpen) {
862
- setStorageProfileEditingId(null);
863
- }
864
- }}
865
- profileId={storageProfileEditingId}
866
- initialTypeSlug="storage"
867
- lockedTypeSlug="storage"
868
- onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
869
- setStorageProfileSelectedLabel(profile.name ?? '');
870
- setResolvedProfileId(profile.id ?? null);
871
- handleChangeValue(String(profile.slug ?? ''), true);
872
- }}
873
- />
874
- </>
875
- );
876
- }
877
-
878
- if (AI_PROFILE_SETTING_SLUGS.includes(setting.slug)) {
879
- return (
880
- <>
881
- <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
882
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
883
- placeholder={t('selectAiProfile')}
884
- emptyStateDescription={t('aiProfileEmptyState')}
885
- value={parsedValue || null}
886
- onChange={(slug, option) => {
887
- setStorageProfileSelectedLabel(option?.name ?? '');
888
- setResolvedProfileId(
889
- option ? Number((option as { id?: number }).id) || null : null
890
- );
891
- handleChangeValue(slug != null ? String(slug) : '', true);
892
- }}
893
- valueType="string"
894
- initialSelectedLabel={storageProfileSelectedLabel}
895
- showCreateButton={false}
896
- clearable={false}
897
- loadOptions={async ({ page, pageSize, search }) => {
898
- const qs = new URLSearchParams({
899
- page: String(page),
900
- pageSize: String(pageSize),
901
- typeSlug: 'ai',
902
- });
903
- if (search) qs.set('search', search);
904
- const response = await request<
905
- ApiPaginated<
906
- IntegrationProfileOption & Record<string, unknown>
907
- >
908
- >({
909
- url: `/integration-profile?${qs}`,
910
- method: 'GET',
911
- });
912
- const paged = response.data;
913
- return {
914
- items: paged?.data ?? [],
915
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
916
- };
917
- }}
918
- getOptionValue={(opt) => opt.slug}
919
- getOptionLabel={(opt) => String(opt.name ?? '')}
920
- getOptionDescription={(opt) => {
921
- const providerSlug = String(
922
- opt.integration_provider?.slug ?? ''
923
- );
924
- const providerName = getLocalizedIntegrationName(
925
- opt.integration_provider?.integration_provider_locale,
926
- currentLocaleCode
927
- );
928
- return [providerName || providerSlug, opt.slug]
929
- .filter(Boolean)
930
- .join(' • ');
931
- }}
932
- className={cn('flex-1 min-w-0', className)}
933
- />
934
-
935
- <Button
936
- type="button"
937
- variant="outline"
938
- size="icon"
939
- className="h-10 w-10 shrink-0"
940
- onClick={() => {
941
- setStorageProfileEditingId(resolvedProfileId);
942
- setIsStorageProfileSheetOpen(true);
943
- }}
944
- disabled={!resolvedProfileId}
945
- aria-label="Editar perfil de IA"
946
- title="Editar perfil de IA"
947
- >
948
- <Pencil className="h-4 w-4" />
949
- </Button>
950
-
951
- <Button
952
- type="button"
953
- variant="outline"
954
- size="icon"
955
- className="h-10 w-10 shrink-0"
956
- onClick={() => {
957
- setStorageProfileEditingId(null);
958
- setIsStorageProfileSheetOpen(true);
959
- }}
960
- aria-label="Criar perfil de IA"
961
- title="Criar perfil de IA"
962
- >
963
- <Plus className="h-4 w-4" />
964
- </Button>
965
- </div>
966
-
967
- <IntegrationProfileSheet
968
- open={isStorageProfileSheetOpen}
969
- onOpenChange={(nextOpen) => {
970
- setIsStorageProfileSheetOpen(nextOpen);
971
- if (!nextOpen) setStorageProfileEditingId(null);
972
- }}
973
- profileId={storageProfileEditingId}
974
- initialTypeSlug="ai"
975
- lockedTypeSlug="ai"
976
- onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
977
- setStorageProfileSelectedLabel(profile.name ?? '');
978
- setResolvedProfileId(profile.id ?? null);
979
- handleChangeValue(String(profile.slug ?? ''), true);
980
- }}
981
- />
982
- </>
983
- );
984
- }
985
-
986
- const oauthProviderSlug = OAUTH_PROFILE_SLUG_TO_PROVIDER[setting.slug];
987
- if (oauthProviderSlug) {
988
- return (
989
- <>
990
- <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
991
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
992
- placeholder={t('selectOAuthProfile')}
993
- emptyStateDescription={t('oauthProfileEmptyState')}
994
- value={parsedValue || null}
995
- onChange={(slug, option) => {
996
- setStorageProfileSelectedLabel(option?.name ?? '');
997
- setResolvedProfileId(
998
- option ? Number((option as { id?: number }).id) || null : null
999
- );
1000
- handleChangeValue(slug != null ? String(slug) : '', true);
1001
- }}
1002
- valueType="string"
1003
- initialSelectedLabel={storageProfileSelectedLabel}
1004
- showCreateButton={false}
1005
- clearable={false}
1006
- loadOptions={async ({ page, pageSize, search }) => {
1007
- const qs = new URLSearchParams({
1008
- page: String(page),
1009
- pageSize: String(pageSize),
1010
- providerSlug: oauthProviderSlug,
1011
- });
1012
- if (search) qs.set('search', search);
1013
- const response = await request<
1014
- ApiPaginated<
1015
- IntegrationProfileOption & Record<string, unknown>
1016
- >
1017
- >({
1018
- url: `/integration-profile?${qs}`,
1019
- method: 'GET',
1020
- });
1021
- const paged = response.data;
1022
- return {
1023
- items: paged?.data ?? [],
1024
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1025
- };
1026
- }}
1027
- getOptionValue={(opt) => opt.slug}
1028
- getOptionLabel={(opt) => String(opt.name ?? '')}
1029
- getOptionDescription={(opt) => {
1030
- const providerSlug = String(
1031
- opt.integration_provider?.slug ?? ''
1032
- );
1033
- const providerName = getLocalizedIntegrationName(
1034
- opt.integration_provider?.integration_provider_locale,
1035
- currentLocaleCode
1036
- );
1037
- return [providerName || providerSlug, opt.slug]
1038
- .filter(Boolean)
1039
- .join(' • ');
1040
- }}
1041
- className={cn('flex-1 min-w-0', className)}
1042
- />
1043
-
1044
- <Button
1045
- type="button"
1046
- variant="outline"
1047
- size="icon"
1048
- className="h-10 w-10 shrink-0"
1049
- onClick={() => {
1050
- setStorageProfileEditingId(resolvedProfileId);
1051
- setIsStorageProfileSheetOpen(true);
1052
- }}
1053
- disabled={!resolvedProfileId}
1054
- aria-label="Editar perfil OAuth"
1055
- title="Editar perfil OAuth"
1056
- >
1057
- <Pencil className="h-4 w-4" />
1058
- </Button>
1059
-
1060
- <Button
1061
- type="button"
1062
- variant="outline"
1063
- size="icon"
1064
- className="h-10 w-10 shrink-0"
1065
- onClick={() => {
1066
- setStorageProfileEditingId(null);
1067
- setIsStorageProfileSheetOpen(true);
1068
- }}
1069
- aria-label="Criar perfil OAuth"
1070
- title="Criar perfil OAuth"
1071
- >
1072
- <Plus className="h-4 w-4" />
1073
- </Button>
1074
- </div>
1075
-
1076
- <IntegrationProfileSheet
1077
- open={isStorageProfileSheetOpen}
1078
- onOpenChange={(nextOpen) => {
1079
- setIsStorageProfileSheetOpen(nextOpen);
1080
- if (!nextOpen) setStorageProfileEditingId(null);
1081
- }}
1082
- profileId={storageProfileEditingId}
1083
- initialTypeSlug="oauth"
1084
- lockedTypeSlug="oauth"
1085
- onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
1086
- setStorageProfileSelectedLabel(profile.name ?? '');
1087
- setResolvedProfileId(profile.id ?? null);
1088
- handleChangeValue(String(profile.slug ?? ''), true);
1089
- }}
1090
- />
1091
- </>
1092
- );
1093
- }
1094
-
1095
- if (setting.slug === COMMERCE_PAYMENT_PROFILE_SETTING_SLUG) {
1096
- return (
1097
- <>
1098
- <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
1099
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
1100
- placeholder={t('selectPaymentProfile')}
1101
- emptyStateDescription={t('paymentProfileEmptyState')}
1102
- value={parsedValue || null}
1103
- onChange={(slug, option) => {
1104
- setStorageProfileSelectedLabel(option?.name ?? '');
1105
- setResolvedProfileId(
1106
- option ? Number((option as { id?: number }).id) || null : null
1107
- );
1108
- handleChangeValue(slug != null ? String(slug) : '', true);
1109
- }}
1110
- valueType="string"
1111
- initialSelectedLabel={storageProfileSelectedLabel}
1112
- showCreateButton={false}
1113
- clearable={false}
1114
- loadOptions={async ({ page, pageSize, search }) => {
1115
- const qs = new URLSearchParams({
1116
- page: String(page),
1117
- pageSize: String(pageSize),
1118
- typeSlug: 'payment',
1119
- });
1120
- if (search) qs.set('search', search);
1121
- const response = await request<
1122
- ApiPaginated<
1123
- IntegrationProfileOption & Record<string, unknown>
1124
- >
1125
- >({
1126
- url: `/integration-profile?${qs}`,
1127
- method: 'GET',
1128
- });
1129
- const paged = response.data;
1130
- return {
1131
- // O endpoint filtra só por tipo; tirar aqui os providers que
1132
- // não fazem checkout web evita uma escolha que o backend
1133
- // recusaria na hora de cobrar.
1134
- items: (paged?.data ?? []).filter(
1135
- (opt) =>
1136
- !NON_WEB_CHECKOUT_PAYMENT_PROVIDERS.has(
1137
- String(opt.integration_provider?.slug ?? '')
1138
- )
1139
- ),
1140
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1141
- };
1142
- }}
1143
- getOptionValue={(opt) => opt.slug}
1144
- getOptionLabel={(opt) => String(opt.name ?? '')}
1145
- getOptionDescription={(opt) => {
1146
- const providerSlug = String(
1147
- opt.integration_provider?.slug ?? ''
1148
- );
1149
- const providerName = getLocalizedIntegrationName(
1150
- opt.integration_provider?.integration_provider_locale,
1151
- currentLocaleCode
1152
- );
1153
- return [providerName || providerSlug, opt.slug]
1154
- .filter(Boolean)
1155
- .join(' • ');
1156
- }}
1157
- className={cn('flex-1 min-w-0', className)}
1158
- />
1159
-
1160
- <Button
1161
- type="button"
1162
- variant="outline"
1163
- size="icon"
1164
- className="h-10 w-10 shrink-0"
1165
- onClick={() => {
1166
- setStorageProfileEditingId(resolvedProfileId);
1167
- setIsStorageProfileSheetOpen(true);
1168
- }}
1169
- disabled={!resolvedProfileId}
1170
- aria-label="Editar perfil de pagamento"
1171
- title="Editar perfil de pagamento"
1172
- >
1173
- <Pencil className="h-4 w-4" />
1174
- </Button>
1175
-
1176
- <Button
1177
- type="button"
1178
- variant="outline"
1179
- size="icon"
1180
- className="h-10 w-10 shrink-0"
1181
- onClick={() => {
1182
- setStorageProfileEditingId(null);
1183
- setIsStorageProfileSheetOpen(true);
1184
- }}
1185
- aria-label="Criar perfil de pagamento"
1186
- title="Criar perfil de pagamento"
1187
- >
1188
- <Plus className="h-4 w-4" />
1189
- </Button>
1190
- </div>
1191
-
1192
- <IntegrationProfileSheet
1193
- open={isStorageProfileSheetOpen}
1194
- onOpenChange={(nextOpen) => {
1195
- setIsStorageProfileSheetOpen(nextOpen);
1196
- if (!nextOpen) setStorageProfileEditingId(null);
1197
- }}
1198
- profileId={storageProfileEditingId}
1199
- initialTypeSlug="payment"
1200
- lockedTypeSlug="payment"
1201
- onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
1202
- setStorageProfileSelectedLabel(profile.name ?? '');
1203
- setResolvedProfileId(profile.id ?? null);
1204
- handleChangeValue(String(profile.slug ?? ''), true);
1205
- }}
1206
- />
1207
- </>
1208
- );
1209
- }
1210
-
1211
- if (setting.slug === SCALING_INFRA_PROFILE_SETTING_SLUG) {
1212
- return (
1213
- <>
1214
- <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
1215
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
1216
- placeholder={t('selectInfraProfile')}
1217
- emptyStateDescription={t('infraProfileEmptyState')}
1218
- value={parsedValue || null}
1219
- onChange={(slug, option) => {
1220
- setStorageProfileSelectedLabel(option?.name ?? '');
1221
- setResolvedProfileId(
1222
- option ? Number((option as { id?: number }).id) || null : null
1223
- );
1224
- handleChangeValue(slug != null ? String(slug) : '', true);
1225
- }}
1226
- valueType="string"
1227
- initialSelectedLabel={storageProfileSelectedLabel}
1228
- showCreateButton={false}
1229
- clearable
1230
- loadOptions={async ({ page, pageSize, search }) => {
1231
- const qs = new URLSearchParams({
1232
- page: String(page),
1233
- pageSize: String(pageSize),
1234
- typeSlug: 'infrastructure',
1235
- });
1236
- if (search) qs.set('search', search);
1237
- const response = await request<
1238
- ApiPaginated<
1239
- IntegrationProfileOption & Record<string, unknown>
1240
- >
1241
- >({
1242
- url: `/integration-profile?${qs}`,
1243
- method: 'GET',
1244
- });
1245
- const paged = response.data;
1246
- return {
1247
- items: paged?.data ?? [],
1248
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1249
- };
1250
- }}
1251
- getOptionValue={(opt) => opt.slug}
1252
- getOptionLabel={(opt) => String(opt.name ?? '')}
1253
- getOptionDescription={(opt) => {
1254
- const providerSlug = String(
1255
- opt.integration_provider?.slug ?? ''
1256
- );
1257
- const providerName = getLocalizedIntegrationName(
1258
- opt.integration_provider?.integration_provider_locale,
1259
- currentLocaleCode
1260
- );
1261
- return [providerName || providerSlug, opt.slug]
1262
- .filter(Boolean)
1263
- .join(' • ');
1264
- }}
1265
- className={cn('flex-1 min-w-0', className)}
1266
- />
1267
-
1268
- <Button
1269
- type="button"
1270
- variant="outline"
1271
- size="icon"
1272
- className="h-10 w-10 shrink-0"
1273
- onClick={() => {
1274
- setStorageProfileEditingId(resolvedProfileId);
1275
- setIsStorageProfileSheetOpen(true);
1276
- }}
1277
- disabled={!resolvedProfileId}
1278
- aria-label="Editar perfil de infraestrutura"
1279
- title="Editar perfil de infraestrutura"
1280
- >
1281
- <Pencil className="h-4 w-4" />
1282
- </Button>
1283
-
1284
- <Button
1285
- type="button"
1286
- variant="outline"
1287
- size="icon"
1288
- className="h-10 w-10 shrink-0"
1289
- onClick={() => {
1290
- setStorageProfileEditingId(null);
1291
- setIsStorageProfileSheetOpen(true);
1292
- }}
1293
- aria-label="Criar perfil de infraestrutura"
1294
- title="Criar perfil de infraestrutura"
1295
- >
1296
- <Plus className="h-4 w-4" />
1297
- </Button>
1298
- </div>
1299
-
1300
- <IntegrationProfileSheet
1301
- open={isStorageProfileSheetOpen}
1302
- onOpenChange={(nextOpen) => {
1303
- setIsStorageProfileSheetOpen(nextOpen);
1304
- if (!nextOpen) setStorageProfileEditingId(null);
1305
- }}
1306
- profileId={storageProfileEditingId}
1307
- initialTypeSlug="infrastructure"
1308
- lockedTypeSlug="infrastructure"
1309
- onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
1310
- setStorageProfileSelectedLabel(profile.name ?? '');
1311
- setResolvedProfileId(profile.id ?? null);
1312
- handleChangeValue(String(profile.slug ?? ''), true);
1313
- }}
1314
- />
1315
- </>
1316
- );
1317
- }
1318
-
1319
- if (setting.slug === HEDHOG_K8S_INFRA_PROFILE_SETTING_SLUG) {
1320
- return (
1321
- <>
1322
- <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
1323
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
1324
- placeholder={t('selectK8sInfraProfile')}
1325
- emptyStateDescription={t('k8sInfraProfileEmptyState')}
1326
- value={parsedValue || null}
1327
- onChange={(slug, option) => {
1328
- setStorageProfileSelectedLabel(option?.name ?? '');
1329
- setResolvedProfileId(
1330
- option ? Number((option as { id?: number }).id) || null : null
1331
- );
1332
- handleChangeValue(slug != null ? String(slug) : '', true);
1333
- }}
1334
- valueType="string"
1335
- initialSelectedLabel={storageProfileSelectedLabel}
1336
- showCreateButton={false}
1337
- clearable
1338
- loadOptions={async ({ page, pageSize, search }) => {
1339
- const qs = new URLSearchParams({
1340
- page: String(page),
1341
- pageSize: String(pageSize),
1342
- typeSlug: 'infrastructure',
1343
- providerSlug: 'kubernetes',
1344
- });
1345
- if (search) qs.set('search', search);
1346
- const response = await request<
1347
- ApiPaginated<
1348
- IntegrationProfileOption & Record<string, unknown>
1349
- >
1350
- >({
1351
- url: `/integration-profile?${qs}`,
1352
- method: 'GET',
1353
- });
1354
- const paged = response.data;
1355
- return {
1356
- items: paged?.data ?? [],
1357
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1358
- };
1359
- }}
1360
- getOptionValue={(opt) => opt.slug}
1361
- getOptionLabel={(opt) => String(opt.name ?? '')}
1362
- getOptionDescription={(opt) => opt.slug}
1363
- className={cn('flex-1 min-w-0', className)}
1364
- />
1365
-
1366
- <Button
1367
- type="button"
1368
- variant="outline"
1369
- size="icon"
1370
- className="h-10 w-10 shrink-0"
1371
- onClick={() => {
1372
- setStorageProfileEditingId(resolvedProfileId);
1373
- setIsStorageProfileSheetOpen(true);
1374
- }}
1375
- disabled={!resolvedProfileId}
1376
- aria-label="Editar perfil Kubernetes"
1377
- title="Editar perfil Kubernetes"
1378
- >
1379
- <Pencil className="h-4 w-4" />
1380
- </Button>
1381
-
1382
- <Button
1383
- type="button"
1384
- variant="outline"
1385
- size="icon"
1386
- className="h-10 w-10 shrink-0"
1387
- onClick={() => {
1388
- setStorageProfileEditingId(null);
1389
- setIsStorageProfileSheetOpen(true);
1390
- }}
1391
- aria-label="Criar perfil Kubernetes"
1392
- title="Criar perfil Kubernetes"
1393
- >
1394
- <Plus className="h-4 w-4" />
1395
- </Button>
1396
- </div>
1397
-
1398
- <IntegrationProfileSheet
1399
- open={isStorageProfileSheetOpen}
1400
- onOpenChange={(nextOpen) => {
1401
- setIsStorageProfileSheetOpen(nextOpen);
1402
- if (!nextOpen) setStorageProfileEditingId(null);
1403
- }}
1404
- profileId={storageProfileEditingId}
1405
- initialTypeSlug="infrastructure"
1406
- lockedTypeSlug="infrastructure"
1407
- lockedProviderSlug="kubernetes"
1408
- onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
1409
- setStorageProfileSelectedLabel(profile.name ?? '');
1410
- setResolvedProfileId(profile.id ?? null);
1411
- handleChangeValue(String(profile.slug ?? ''), true);
1412
- }}
1413
- />
1414
- </>
1415
- );
1416
- }
1417
-
1418
- if (setting.slug === MAIL_INTEGRATION_PROFILE_SETTING_SLUG) {
1419
- return (
1420
- <>
1421
- <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
1422
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
1423
- placeholder={t('selectMailProfile')}
1424
- emptyStateDescription={t('mailProfileEmptyState')}
1425
- value={parsedValue || null}
1426
- onChange={(slug, option) => {
1427
- setStorageProfileSelectedLabel(option?.name ?? '');
1428
- setResolvedProfileId(
1429
- option ? Number((option as { id?: number }).id) || null : null
1430
- );
1431
- handleChangeValue(slug != null ? String(slug) : '', true);
1432
- }}
1433
- valueType="string"
1434
- initialSelectedLabel={storageProfileSelectedLabel}
1435
- showCreateButton={false}
1436
- clearable={false}
1437
- loadOptions={async ({ page, pageSize, search }) => {
1438
- const qs = new URLSearchParams({
1439
- page: String(page),
1440
- pageSize: String(pageSize),
1441
- typeSlug: 'email',
1442
- });
1443
- if (search) qs.set('search', search);
1444
- const response = await request<
1445
- ApiPaginated<
1446
- IntegrationProfileOption & Record<string, unknown>
1447
- >
1448
- >({
1449
- url: `/integration-profile?${qs}`,
1450
- method: 'GET',
1451
- });
1452
- const paged = response.data;
1453
- return {
1454
- items: paged?.data ?? [],
1455
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1456
- };
1457
- }}
1458
- getOptionValue={(opt) => opt.slug}
1459
- getOptionLabel={(opt) => String(opt.name ?? '')}
1460
- getOptionDescription={(opt) => {
1461
- const providerSlug = String(
1462
- opt.integration_provider?.slug ?? ''
1463
- );
1464
- const providerName = getLocalizedIntegrationName(
1465
- opt.integration_provider?.integration_provider_locale,
1466
- currentLocaleCode
1467
- );
1468
- return [providerName || providerSlug, opt.slug]
1469
- .filter(Boolean)
1470
- .join(' • ');
1471
- }}
1472
- className={cn('flex-1 min-w-0', className)}
1473
- />
1474
-
1475
- <Button
1476
- type="button"
1477
- variant="outline"
1478
- size="icon"
1479
- className="h-10 w-10 shrink-0"
1480
- onClick={() => {
1481
- setStorageProfileEditingId(resolvedProfileId);
1482
- setIsStorageProfileSheetOpen(true);
1483
- }}
1484
- disabled={!resolvedProfileId}
1485
- aria-label="Editar perfil de email"
1486
- title="Editar perfil de email"
1487
- >
1488
- <Pencil className="h-4 w-4" />
1489
- </Button>
1490
-
1491
- <Button
1492
- type="button"
1493
- variant="outline"
1494
- size="icon"
1495
- className="h-10 w-10 shrink-0"
1496
- onClick={() => {
1497
- setStorageProfileEditingId(null);
1498
- setIsStorageProfileSheetOpen(true);
1499
- }}
1500
- aria-label="Criar perfil de email"
1501
- title="Criar perfil de email"
1502
- >
1503
- <Plus className="h-4 w-4" />
1504
- </Button>
1505
- </div>
1506
-
1507
- <IntegrationProfileSheet
1508
- open={isStorageProfileSheetOpen}
1509
- onOpenChange={(nextOpen) => {
1510
- setIsStorageProfileSheetOpen(nextOpen);
1511
- if (!nextOpen) setStorageProfileEditingId(null);
1512
- }}
1513
- profileId={storageProfileEditingId}
1514
- initialTypeSlug="email"
1515
- lockedTypeSlug="email"
1516
- onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
1517
- setStorageProfileSelectedLabel(profile.name ?? '');
1518
- setResolvedProfileId(profile.id ?? null);
1519
- handleChangeValue(String(profile.slug ?? ''), true);
1520
- }}
1521
- />
1522
- </>
1523
- );
1524
- }
1525
-
1526
- if (!dailyReportIntegrationTypeSlug) {
856
+ if (!profileConfig) {
857
+ isTextInputRef.current = true;
1527
858
  return (
1528
859
  <Input
1529
860
  type="text"
@@ -1534,339 +865,148 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
1534
865
  );
1535
866
  }
1536
867
 
1537
- if (dailyReportIntegrationTypeSlug) {
1538
- const loadIntegrationCatalog = async () => {
1539
- const qs = new URLSearchParams({
1540
- typeSlug: dailyReportIntegrationTypeSlug,
1541
- });
1542
- const providersResponse = await request<IntegrationProviderOption[]>({
1543
- url: `/integration-profile/providers?${qs}`,
1544
- method: 'GET',
1545
- });
1546
-
1547
- return {
1548
- providers: (providersResponse.data ??
1549
- []) as IntegrationProviderOption[],
1550
- };
1551
- };
868
+ const { typeSlug, providerSlug, excludeProviders } = profileConfig;
869
+ const translationKeys =
870
+ (typeSlug && PROFILE_TYPE_TRANSLATION_KEYS[typeSlug]) ||
871
+ GENERIC_PROFILE_TRANSLATION_KEYS;
872
+ const profileName = t(
873
+ profileConfig.profileNameKey ?? translationKeys.name
874
+ );
1552
875
 
1553
- return (
1554
- <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
1555
- placeholder={
1556
- dailyReportIntegrationTypeSlug === 'email'
1557
- ? t('selectMailProfile')
1558
- : t('selectWhatsAppProfile')
1559
- }
1560
- value={parsedValue || null}
1561
- onChange={(slug) =>
1562
- handleChangeValue(slug != null ? String(slug) : '', true)
1563
- }
1564
- valueType="string"
1565
- loadOptions={async ({ page, pageSize, search }) => {
1566
- const qs = new URLSearchParams({
1567
- page: String(page),
1568
- pageSize: String(pageSize),
1569
- typeSlug: dailyReportIntegrationTypeSlug,
1570
- });
1571
- if (search) qs.set('search', search);
1572
- const response = await request<
1573
- ApiPaginated<IntegrationProfileOption & Record<string, unknown>>
1574
- >({
1575
- url: `/integration-profile?${qs}`,
1576
- method: 'GET',
1577
- });
1578
- const paged = response.data;
1579
- return {
1580
- items: paged?.data ?? [],
1581
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1582
- };
1583
- }}
1584
- getOptionValue={(opt) => opt.slug}
1585
- getOptionLabel={(opt) => String(opt.name ?? '')}
1586
- getOptionDescription={(opt) =>
1587
- String(opt.integration_provider?.slug ?? opt.slug ?? '')
1588
- }
1589
- mapSearchToCreateValues={(search) =>
1590
- dailyReportIntegrationTypeSlug === 'email'
1591
- ? {
1592
- name: search,
1593
- provider: 'smtp',
1594
- port: '587',
1595
- }
1596
- : {
1597
- name: search,
1598
- provider: 'evolution-api',
1599
- port: '',
1600
- }
1601
- }
1602
- showCreateButton
1603
- onCreate={async (values) => {
1604
- const providersCatalog = await loadIntegrationCatalog();
1605
- const providerSlug =
1606
- dailyReportIntegrationTypeSlug === 'email'
1607
- ? values.provider || 'smtp'
1608
- : 'evolution-api';
1609
- const provider = providersCatalog.providers.find(
1610
- (item) => item.slug === providerSlug
1611
- );
876
+ // O picker exibe o nome do perfil, mas a setting guarda o slug.
877
+ const applyProfile = (
878
+ profile: {
879
+ id?: number | null;
880
+ slug?: string | null;
881
+ name?: string | null;
882
+ } | null
883
+ ) => {
884
+ setStorageProfileSelectedLabel(profile?.name ?? '');
885
+ setResolvedProfileId(profile?.id ?? null);
886
+ handleChangeValue(profile?.slug ? String(profile.slug) : '', true);
887
+ };
1612
888
 
1613
- if (!provider) {
1614
- throw new Error(
1615
- `Integration provider "${providerSlug}" not found.`
1616
- );
889
+ return (
890
+ <>
891
+ <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
892
+ <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
893
+ placeholder={t(translationKeys.placeholder)}
894
+ emptyStateDescription={t(translationKeys.emptyState)}
895
+ value={parsedValue || null}
896
+ onChange={(slug, option) =>
897
+ applyProfile(
898
+ option
899
+ ? {
900
+ id: Number((option as { id?: number }).id) || null,
901
+ slug: slug != null ? String(slug) : '',
902
+ name: option.name ?? '',
903
+ }
904
+ : null
905
+ )
1617
906
  }
1618
-
1619
- const name = String(values.name || 'Profile').trim();
1620
- const response = await request<
1621
- IntegrationProfileOption & Record<string, unknown>
1622
- >({
1623
- url: '/integration-profile',
1624
- method: 'POST',
1625
- data: {
1626
- name,
1627
- slug: toSlug(name),
1628
- type_id: provider.type_id,
1629
- provider_id: provider.id,
1630
- config: buildIntegrationProfileConfig(providerSlug, values),
1631
- is_active: true,
1632
- },
1633
- });
1634
- return response.data;
1635
- }}
1636
- renderCreateContent={({ values, setValue }) => {
1637
- if (dailyReportIntegrationTypeSlug === 'whatsapp') {
1638
- return (
1639
- <div className="space-y-4">
1640
- <div className="space-y-1">
1641
- <Label>{t('whatsappProfile.nameLabel')}</Label>
1642
- <Input
1643
- value={values.name || ''}
1644
- onChange={(e) => setValue('name', e.target.value)}
1645
- placeholder={t('whatsappProfile.namePlaceholder')}
1646
- />
1647
- </div>
1648
- <div className="space-y-1">
1649
- <Label>{t('whatsappProfile.providerLabel')}</Label>
1650
- <Select
1651
- value={values.provider || 'evolution-api'}
1652
- onValueChange={(v) => setValue('provider', v)}
1653
- >
1654
- <SelectTrigger className="w-full">
1655
- <SelectValue />
1656
- </SelectTrigger>
1657
- <SelectContent>
1658
- <SelectItem value="evolution-api">
1659
- Evolution API
1660
- </SelectItem>
1661
- </SelectContent>
1662
- </Select>
1663
- </div>
1664
- <div className="space-y-1">
1665
- <Label>{t('whatsappProfile.hostLabel')}</Label>
1666
- <Input
1667
- value={values.host || ''}
1668
- onChange={(e) => setValue('host', e.target.value)}
1669
- placeholder={t('whatsappProfile.hostPlaceholder')}
1670
- />
1671
- </div>
1672
- <div className="space-y-1">
1673
- <Label>{t('whatsappProfile.instanceNameLabel')}</Label>
1674
- <Input
1675
- value={values.instance_name || ''}
1676
- onChange={(e) =>
1677
- setValue('instance_name', e.target.value)
1678
- }
1679
- placeholder={t(
1680
- 'whatsappProfile.instanceNamePlaceholder'
1681
- )}
1682
- />
1683
- </div>
1684
- <div className="space-y-1">
1685
- <Label>{t('whatsappProfile.tokenLabel')}</Label>
1686
- <Input
1687
- type="password"
1688
- value={values.token || ''}
1689
- onChange={(e) => setValue('token', e.target.value)}
1690
- placeholder={t('whatsappProfile.tokenPlaceholder')}
1691
- />
1692
- </div>
1693
- </div>
907
+ valueType="string"
908
+ initialSelectedLabel={storageProfileSelectedLabel}
909
+ showCreateButton={false}
910
+ clearable={false}
911
+ loadOptions={async ({ page, pageSize, search }) => {
912
+ const qs = new URLSearchParams({
913
+ page: String(page),
914
+ pageSize: String(pageSize),
915
+ });
916
+ if (typeSlug) qs.set('typeSlug', typeSlug);
917
+ if (providerSlug) qs.set('providerSlug', providerSlug);
918
+ if (search) qs.set('search', search);
919
+
920
+ const response = await request<
921
+ ApiPaginated<
922
+ IntegrationProfileOption & Record<string, unknown>
923
+ >
924
+ >({
925
+ url: `/integration-profile?${qs}`,
926
+ method: 'GET',
927
+ });
928
+ const paged = response.data;
929
+
930
+ return {
931
+ // O endpoint filtra por tipo e por provider; o que sobra
932
+ // (Google Play como gateway do checkout web, por exemplo) sai
933
+ // aqui para não oferecer uma escolha que o backend recusaria.
934
+ items: (paged?.data ?? []).filter(
935
+ (profile) =>
936
+ !excludeProviders?.includes(
937
+ String(profile.integration_provider?.slug ?? '')
938
+ )
939
+ ),
940
+ hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
941
+ };
942
+ }}
943
+ getOptionValue={(opt) => opt.slug}
944
+ getOptionLabel={(opt) => String(opt.name ?? '')}
945
+ getOptionDescription={(opt) => {
946
+ const optionProviderSlug = String(
947
+ opt.integration_provider?.slug ?? ''
1694
948
  );
1695
- }
949
+ const providerName = getLocalizedIntegrationName(
950
+ opt.integration_provider?.integration_provider_locale,
951
+ currentLocaleCode
952
+ );
953
+ return [providerName || optionProviderSlug, opt.slug]
954
+ .filter(Boolean)
955
+ .join(' • ');
956
+ }}
957
+ className={cn('flex-1 min-w-0', className)}
958
+ />
1696
959
 
1697
- const provider = values.provider || 'smtp';
1698
- return (
1699
- <div className="space-y-4">
1700
- <div className="space-y-1">
1701
- <Label>{t('mailProfile.nameLabel')}</Label>
1702
- <Input
1703
- value={values.name || ''}
1704
- onChange={(e) => setValue('name', e.target.value)}
1705
- placeholder={t('mailProfile.namePlaceholder')}
1706
- />
1707
- </div>
1708
- <div className="space-y-1">
1709
- <Label>{t('mailProfile.providerLabel')}</Label>
1710
- <Select
1711
- value={provider}
1712
- onValueChange={(v) => setValue('provider', v)}
1713
- >
1714
- <SelectTrigger className="w-full">
1715
- <SelectValue />
1716
- </SelectTrigger>
1717
- <SelectContent>
1718
- <SelectItem value="smtp">SMTP</SelectItem>
1719
- <SelectItem value="gmail">Gmail</SelectItem>
1720
- <SelectItem value="ses">
1721
- {t('mailProfile.providers.ses')}
1722
- </SelectItem>
1723
- </SelectContent>
1724
- </Select>
1725
- </div>
1726
- <div className="space-y-1">
1727
- <Label>{t('mailProfile.fromEmailLabel')}</Label>
1728
- <Input
1729
- type="email"
1730
- value={values.from_email || ''}
1731
- onChange={(e) => setValue('from_email', e.target.value)}
1732
- placeholder={t('mailProfile.fromEmailPlaceholder')}
1733
- />
1734
- </div>
1735
- <div className="space-y-1">
1736
- <Label>{t('mailProfile.fromNameLabel')}</Label>
1737
- <Input
1738
- value={values.from_name || ''}
1739
- onChange={(e) => setValue('from_name', e.target.value)}
1740
- placeholder={t('mailProfile.fromNamePlaceholder')}
1741
- />
1742
- </div>
1743
- {provider === 'smtp' && (
1744
- <>
1745
- <div className="space-y-1">
1746
- <Label>{t('mailProfile.smtpHostLabel')}</Label>
1747
- <Input
1748
- value={values.host || ''}
1749
- onChange={(e) => setValue('host', e.target.value)}
1750
- placeholder={t('mailProfile.smtpHostPlaceholder')}
1751
- />
1752
- </div>
1753
- <div className="space-y-1">
1754
- <Label>{t('mailProfile.smtpPortLabel')}</Label>
1755
- <Input
1756
- type="number"
1757
- value={values.port || '587'}
1758
- onChange={(e) => setValue('port', e.target.value)}
1759
- placeholder={t('mailProfile.smtpPortPlaceholder')}
1760
- />
1761
- </div>
1762
- <div className="space-y-1">
1763
- <Label>{t('mailProfile.smtpUsernameLabel')}</Label>
1764
- <Input
1765
- value={values.username || ''}
1766
- onChange={(e) => setValue('username', e.target.value)}
1767
- placeholder={t('mailProfile.smtpUsernamePlaceholder')}
1768
- />
1769
- </div>
1770
- <div className="space-y-1">
1771
- <Label>{t('mailProfile.smtpPasswordLabel')}</Label>
1772
- <Input
1773
- type="password"
1774
- value={values.password || ''}
1775
- onChange={(e) => setValue('password', e.target.value)}
1776
- />
1777
- </div>
1778
- <div className="space-y-1">
1779
- <Label>{t('mailProfile.replyToEmailLabel')}</Label>
1780
- <Input
1781
- type="email"
1782
- value={values.reply_to_email || ''}
1783
- onChange={(e) =>
1784
- setValue('reply_to_email', e.target.value)
1785
- }
1786
- placeholder={t('mailProfile.replyToEmailPlaceholder')}
1787
- />
1788
- </div>
1789
- </>
1790
- )}
1791
- {provider === 'gmail' && (
1792
- <>
1793
- <div className="space-y-1">
1794
- <Label>{t('mailProfile.clientIdLabel')}</Label>
1795
- <Input
1796
- value={values.client_id || ''}
1797
- onChange={(e) =>
1798
- setValue('client_id', e.target.value)
1799
- }
1800
- />
1801
- </div>
1802
- <div className="space-y-1">
1803
- <Label>{t('mailProfile.clientSecretLabel')}</Label>
1804
- <Input
1805
- type="password"
1806
- value={values.client_secret || ''}
1807
- onChange={(e) =>
1808
- setValue('client_secret', e.target.value)
1809
- }
1810
- />
1811
- </div>
1812
- <div className="space-y-1">
1813
- <Label>{t('mailProfile.refreshTokenLabel')}</Label>
1814
- <Input
1815
- value={values.refresh_token || ''}
1816
- onChange={(e) =>
1817
- setValue('refresh_token', e.target.value)
1818
- }
1819
- />
1820
- </div>
1821
- </>
1822
- )}
1823
- {provider === 'ses' && (
1824
- <>
1825
- <div className="space-y-1">
1826
- <Label>{t('mailProfile.accessKeyIdLabel')}</Label>
1827
- <Input
1828
- value={values.access_key_id || ''}
1829
- onChange={(e) =>
1830
- setValue('access_key_id', e.target.value)
1831
- }
1832
- />
1833
- </div>
1834
- <div className="space-y-1">
1835
- <Label>{t('mailProfile.secretAccessKeyLabel')}</Label>
1836
- <Input
1837
- type="password"
1838
- value={values.secret_access_key || ''}
1839
- onChange={(e) =>
1840
- setValue('secret_access_key', e.target.value)
1841
- }
1842
- />
1843
- </div>
1844
- <div className="space-y-1">
1845
- <Label>{t('mailProfile.regionLabel')}</Label>
1846
- <Input
1847
- value={values.region || ''}
1848
- onChange={(e) => setValue('region', e.target.value)}
1849
- placeholder={t('mailProfile.regionPlaceholder')}
1850
- />
1851
- </div>
1852
- </>
1853
- )}
1854
- </div>
1855
- );
960
+ <Button
961
+ type="button"
962
+ variant="outline"
963
+ size="icon"
964
+ className="h-10 w-10 shrink-0"
965
+ onClick={() => {
966
+ setStorageProfileEditingId(resolvedProfileId);
967
+ setIsStorageProfileSheetOpen(true);
968
+ }}
969
+ disabled={!resolvedProfileId}
970
+ aria-label={t('editProfileAria', { profile: profileName })}
971
+ title={t('editProfileAria', { profile: profileName })}
972
+ >
973
+ <Pencil className="h-4 w-4" />
974
+ </Button>
975
+
976
+ <Button
977
+ type="button"
978
+ variant="outline"
979
+ size="icon"
980
+ className="h-10 w-10 shrink-0"
981
+ onClick={() => {
982
+ setStorageProfileEditingId(null);
983
+ setIsStorageProfileSheetOpen(true);
984
+ }}
985
+ aria-label={t('createProfileAria', { profile: profileName })}
986
+ title={t('createProfileAria', { profile: profileName })}
987
+ >
988
+ <Plus className="h-4 w-4" />
989
+ </Button>
990
+ </div>
991
+
992
+ <IntegrationProfileSheet
993
+ open={isStorageProfileSheetOpen}
994
+ onOpenChange={(nextOpen) => {
995
+ setIsStorageProfileSheetOpen(nextOpen);
996
+ if (!nextOpen) setStorageProfileEditingId(null);
1856
997
  }}
1857
- className={cn(className, 'w-full min-w-0 md:min-w-50')}
998
+ profileId={storageProfileEditingId}
999
+ initialTypeSlug={typeSlug}
1000
+ lockedTypeSlug={typeSlug}
1001
+ lockedProviderSlug={providerSlug}
1002
+ excludeProviderSlugs={excludeProviders}
1003
+ onSaved={(profile: IntegrationProfileSheetSavedProfile) =>
1004
+ applyProfile(profile)
1005
+ }
1858
1006
  />
1859
- );
1860
- }
1861
-
1862
- return (
1863
- <Input
1864
- type="text"
1865
- value={parsedValue || ''}
1866
- onChange={(e) => handleChangeValue(formatValue(e.target.value))}
1867
- className={cn(className, 'bg-background')}
1868
- />
1007
+ </>
1869
1008
  );
1009
+ }
1870
1010
 
1871
1011
  default:
1872
1012
  return (