kybernus 3.1.0 → 3.2.0

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 (287) hide show
  1. package/dist/cli/commands/ecommerce.d.ts +3 -0
  2. package/dist/cli/commands/ecommerce.d.ts.map +1 -0
  3. package/dist/cli/commands/ecommerce.js +164 -0
  4. package/dist/cli/commands/ecommerce.js.map +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/ecommerce/.env.example +10 -0
  9. package/templates/ecommerce/.github/workflows/ci.yml +102 -0
  10. package/templates/ecommerce/.github/workflows/deploy.yml +31 -0
  11. package/templates/ecommerce/.prettierrc +9 -0
  12. package/templates/ecommerce/Dockerfile +54 -0
  13. package/templates/ecommerce/README.md +295 -0
  14. package/templates/ecommerce/apps/api/.env.example +59 -0
  15. package/templates/ecommerce/apps/api/jest.config.ts +50 -0
  16. package/templates/ecommerce/apps/api/jest.integration.config.ts +45 -0
  17. package/templates/ecommerce/apps/api/package.json +59 -0
  18. package/templates/ecommerce/apps/api/prisma/migrations/20260306000137_init/migration.sql +184 -0
  19. package/templates/ecommerce/apps/api/prisma/migrations/migration_lock.toml +3 -0
  20. package/templates/ecommerce/apps/api/prisma/schema.prisma +181 -0
  21. package/templates/ecommerce/apps/api/prisma/seed.ts +159 -0
  22. package/templates/ecommerce/apps/api/src/__tests__/app.test.ts +39 -0
  23. package/templates/ecommerce/apps/api/src/__tests__/globalSetup.ts +34 -0
  24. package/templates/ecommerce/apps/api/src/__tests__/globalTeardown.ts +16 -0
  25. package/templates/ecommerce/apps/api/src/__tests__/setup.db.ts +18 -0
  26. package/templates/ecommerce/apps/api/src/__tests__/setup.env.ts +14 -0
  27. package/templates/ecommerce/apps/api/src/app.ts +133 -0
  28. package/templates/ecommerce/apps/api/src/application/admin/admin-user.service.ts +24 -0
  29. package/templates/ecommerce/apps/api/src/application/admin/dashboard.service.ts +102 -0
  30. package/templates/ecommerce/apps/api/src/application/auth/auth.service.ts +185 -0
  31. package/templates/ecommerce/apps/api/src/application/cart/cart.service.ts +151 -0
  32. package/templates/ecommerce/apps/api/src/application/cart/coupon.service.ts +51 -0
  33. package/templates/ecommerce/apps/api/src/application/catalog/catalog.service.ts +168 -0
  34. package/templates/ecommerce/apps/api/src/application/checkout/checkout.service.ts +114 -0
  35. package/templates/ecommerce/apps/api/src/application/orders/order.service.ts +93 -0
  36. package/templates/ecommerce/apps/api/src/application/ports/email.port.ts +3 -0
  37. package/templates/ecommerce/apps/api/src/application/ports/payment.port.ts +24 -0
  38. package/templates/ecommerce/apps/api/src/application/ports/shipping.port.ts +9 -0
  39. package/templates/ecommerce/apps/api/src/application/ports/storage.port.ts +3 -0
  40. package/templates/ecommerce/apps/api/src/application/ports/token-blacklist.port.ts +4 -0
  41. package/templates/ecommerce/apps/api/src/application/ports/token.port.ts +18 -0
  42. package/templates/ecommerce/apps/api/src/application/profile/profile.service.ts +76 -0
  43. package/templates/ecommerce/apps/api/src/domain/auth/user.entity.ts +109 -0
  44. package/templates/ecommerce/apps/api/src/domain/auth/user.repository.ts +11 -0
  45. package/templates/ecommerce/apps/api/src/domain/cart/cart.entity.ts +136 -0
  46. package/templates/ecommerce/apps/api/src/domain/cart/cart.repository.ts +8 -0
  47. package/templates/ecommerce/apps/api/src/domain/cart/coupon.entity.ts +58 -0
  48. package/templates/ecommerce/apps/api/src/domain/cart/coupon.repository.ts +10 -0
  49. package/templates/ecommerce/apps/api/src/domain/catalog/category.entity.ts +51 -0
  50. package/templates/ecommerce/apps/api/src/domain/catalog/category.repository.ts +10 -0
  51. package/templates/ecommerce/apps/api/src/domain/catalog/product.entity.ts +130 -0
  52. package/templates/ecommerce/apps/api/src/domain/catalog/product.repository.ts +28 -0
  53. package/templates/ecommerce/apps/api/src/domain/checkout/order.entity.ts +121 -0
  54. package/templates/ecommerce/apps/api/src/domain/checkout/order.repository.ts +11 -0
  55. package/templates/ecommerce/apps/api/src/domain/shared/AppError.ts +12 -0
  56. package/templates/ecommerce/apps/api/src/infrastructure/cache/redis.ts +16 -0
  57. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/admin.registry.ts +13 -0
  58. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/auth.registry.ts +34 -0
  59. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/cart.registry.ts +49 -0
  60. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/catalog.registry.ts +24 -0
  61. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/checkout.registry.ts +47 -0
  62. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/orders.registry.ts +6 -0
  63. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/profile.registry.ts +4 -0
  64. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/cart.memory.repository.ts +33 -0
  65. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/category.memory.repository.ts +41 -0
  66. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/coupon.memory.repository.ts +55 -0
  67. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/order.memory.repository.ts +75 -0
  68. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/product.memory.repository.ts +100 -0
  69. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/user.memory.repository.ts +54 -0
  70. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/auth/user.prisma.repository.ts +83 -0
  71. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/category.prisma.repository.ts +69 -0
  72. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/product.prisma.repository.ts +185 -0
  73. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/checkout/order.prisma.repository.ts +149 -0
  74. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma-client.ts +17 -0
  75. package/templates/ecommerce/apps/api/src/infrastructure/services/email/email.registry.ts +18 -0
  76. package/templates/ecommerce/apps/api/src/infrastructure/services/email/ethereal.email.service.ts +38 -0
  77. package/templates/ecommerce/apps/api/src/infrastructure/services/email/noop.email.service.ts +12 -0
  78. package/templates/ecommerce/apps/api/src/infrastructure/services/email/smtp.email.service.ts +36 -0
  79. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe-webhook.handler.ts +83 -0
  80. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe.adapter.ts +39 -0
  81. package/templates/ecommerce/apps/api/src/infrastructure/services/shipping/mock.shipping.service.ts +17 -0
  82. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/in-memory.storage.service.ts +11 -0
  83. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/local-disk.storage.service.ts +27 -0
  84. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/s3.storage.service.ts +52 -0
  85. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/storage.registry.ts +19 -0
  86. package/templates/ecommerce/apps/api/src/infrastructure/services/token/redis.token.blacklist.ts +23 -0
  87. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.blacklist.ts +30 -0
  88. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.service.ts +136 -0
  89. package/templates/ecommerce/apps/api/src/modules/admin/__tests__/admin.routes.integration.test.ts +250 -0
  90. package/templates/ecommerce/apps/api/src/modules/admin/admin.controller.ts +116 -0
  91. package/templates/ecommerce/apps/api/src/modules/admin/admin.registry.ts +1 -0
  92. package/templates/ecommerce/apps/api/src/modules/admin/admin.routes.ts +21 -0
  93. package/templates/ecommerce/apps/api/src/modules/admin/admin.service.ts +1 -0
  94. package/templates/ecommerce/apps/api/src/modules/admin/admin.user.service.ts +1 -0
  95. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.logout.redis.test.ts +104 -0
  96. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.routes.integration.test.ts +211 -0
  97. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.service.unit.test.ts +260 -0
  98. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/email.service.unit.test.ts +94 -0
  99. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/token.blacklist.redis.test.ts +65 -0
  100. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.entity.unit.test.ts +79 -0
  101. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.prisma.repository.test.ts +138 -0
  102. package/templates/ecommerce/apps/api/src/modules/auth/auth.controller.ts +148 -0
  103. package/templates/ecommerce/apps/api/src/modules/auth/auth.registry.ts +1 -0
  104. package/templates/ecommerce/apps/api/src/modules/auth/auth.routes.ts +17 -0
  105. package/templates/ecommerce/apps/api/src/modules/auth/auth.service.ts +1 -0
  106. package/templates/ecommerce/apps/api/src/modules/auth/redis.token.blacklist.ts +1 -0
  107. package/templates/ecommerce/apps/api/src/modules/auth/token.blacklist.ts +2 -0
  108. package/templates/ecommerce/apps/api/src/modules/auth/token.service.ts +2 -0
  109. package/templates/ecommerce/apps/api/src/modules/auth/user.entity.ts +1 -0
  110. package/templates/ecommerce/apps/api/src/modules/auth/user.prisma.repository.ts +1 -0
  111. package/templates/ecommerce/apps/api/src/modules/auth/user.repository.ts +2 -0
  112. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.entity.unit.test.ts +144 -0
  113. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.routes.integration.test.ts +242 -0
  114. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.service.unit.test.ts +151 -0
  115. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/coupon.admin.integration.test.ts +136 -0
  116. package/templates/ecommerce/apps/api/src/modules/cart/cart.controller.ts +94 -0
  117. package/templates/ecommerce/apps/api/src/modules/cart/cart.entity.ts +1 -0
  118. package/templates/ecommerce/apps/api/src/modules/cart/cart.registry.ts +1 -0
  119. package/templates/ecommerce/apps/api/src/modules/cart/cart.repository.ts +2 -0
  120. package/templates/ecommerce/apps/api/src/modules/cart/cart.routes.ts +17 -0
  121. package/templates/ecommerce/apps/api/src/modules/cart/cart.service.ts +1 -0
  122. package/templates/ecommerce/apps/api/src/modules/cart/coupon.entity.ts +1 -0
  123. package/templates/ecommerce/apps/api/src/modules/cart/coupon.repository.ts +2 -0
  124. package/templates/ecommerce/apps/api/src/modules/cart/coupon.service.ts +1 -0
  125. package/templates/ecommerce/apps/api/src/modules/cart/shipping.service.ts +2 -0
  126. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.routes.integration.test.ts +275 -0
  127. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.service.unit.test.ts +223 -0
  128. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.image.integration.test.ts +130 -0
  129. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.prisma.repository.test.ts +174 -0
  130. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.controller.ts +176 -0
  131. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.registry.ts +1 -0
  132. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.routes.ts +38 -0
  133. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.service.ts +1 -0
  134. package/templates/ecommerce/apps/api/src/modules/catalog/category.entity.ts +1 -0
  135. package/templates/ecommerce/apps/api/src/modules/catalog/category.prisma.repository.ts +1 -0
  136. package/templates/ecommerce/apps/api/src/modules/catalog/category.repository.ts +2 -0
  137. package/templates/ecommerce/apps/api/src/modules/catalog/product.entity.ts +1 -0
  138. package/templates/ecommerce/apps/api/src/modules/catalog/product.prisma.repository.ts +1 -0
  139. package/templates/ecommerce/apps/api/src/modules/catalog/product.repository.ts +2 -0
  140. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.routes.integration.test.ts +163 -0
  141. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.service.unit.test.ts +191 -0
  142. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/order.prisma.repository.test.ts +150 -0
  143. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.controller.ts +59 -0
  144. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.registry.ts +1 -0
  145. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.routes.ts +18 -0
  146. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.service.ts +1 -0
  147. package/templates/ecommerce/apps/api/src/modules/checkout/order.entity.ts +1 -0
  148. package/templates/ecommerce/apps/api/src/modules/checkout/order.prisma.repository.ts +1 -0
  149. package/templates/ecommerce/apps/api/src/modules/checkout/order.repository.ts +2 -0
  150. package/templates/ecommerce/apps/api/src/modules/checkout/tax.service.ts +9 -0
  151. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.entity.unit.test.ts +68 -0
  152. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.routes.integration.test.ts +254 -0
  153. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.service.email.unit.test.ts +142 -0
  154. package/templates/ecommerce/apps/api/src/modules/orders/order.controller.ts +96 -0
  155. package/templates/ecommerce/apps/api/src/modules/orders/order.registry.ts +1 -0
  156. package/templates/ecommerce/apps/api/src/modules/orders/order.routes.ts +17 -0
  157. package/templates/ecommerce/apps/api/src/modules/orders/order.service.ts +1 -0
  158. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe-webhook.unit.test.ts +330 -0
  159. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe.adapter.unit.test.ts +84 -0
  160. package/templates/ecommerce/apps/api/src/modules/payment/adapters/stripe.adapter.ts +1 -0
  161. package/templates/ecommerce/apps/api/src/modules/payment/payment.port.ts +1 -0
  162. package/templates/ecommerce/apps/api/src/modules/payment/stripe-webhook.handler.ts +1 -0
  163. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.routes.integration.test.ts +180 -0
  164. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.service.unit.test.ts +187 -0
  165. package/templates/ecommerce/apps/api/src/modules/profile/profile.controller.ts +92 -0
  166. package/templates/ecommerce/apps/api/src/modules/profile/profile.registry.ts +1 -0
  167. package/templates/ecommerce/apps/api/src/modules/profile/profile.routes.ts +14 -0
  168. package/templates/ecommerce/apps/api/src/modules/profile/profile.service.ts +1 -0
  169. package/templates/ecommerce/apps/api/src/presentation/middlewares/authenticate.ts +37 -0
  170. package/templates/ecommerce/apps/api/src/presentation/middlewares/authorize.ts +23 -0
  171. package/templates/ecommerce/apps/api/src/presentation/middlewares/errorHandler.ts +48 -0
  172. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.controller.ts +116 -0
  173. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.routes.ts +21 -0
  174. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.controller.ts +147 -0
  175. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.routes.ts +17 -0
  176. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.controller.ts +94 -0
  177. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.routes.ts +17 -0
  178. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.controller.ts +176 -0
  179. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.routes.ts +38 -0
  180. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.controller.ts +59 -0
  181. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.routes.ts +18 -0
  182. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.controller.ts +96 -0
  183. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.routes.ts +17 -0
  184. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.controller.ts +92 -0
  185. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.routes.ts +14 -0
  186. package/templates/ecommerce/apps/api/src/presentation/validators/uuidParam.ts +20 -0
  187. package/templates/ecommerce/apps/api/src/server.ts +47 -0
  188. package/templates/ecommerce/apps/api/src/shared/__tests__/uuid.validation.test.ts +111 -0
  189. package/templates/ecommerce/apps/api/src/shared/errors/AppError.ts +1 -0
  190. package/templates/ecommerce/apps/api/src/shared/infra/email/EtherealEmailService.ts +1 -0
  191. package/templates/ecommerce/apps/api/src/shared/infra/email/IEmailService.ts +1 -0
  192. package/templates/ecommerce/apps/api/src/shared/infra/email/NoopEmailService.ts +1 -0
  193. package/templates/ecommerce/apps/api/src/shared/infra/email/SmtpEmailService.ts +1 -0
  194. package/templates/ecommerce/apps/api/src/shared/infra/email/__tests__/ethereal.email.integration.test.ts +32 -0
  195. package/templates/ecommerce/apps/api/src/shared/infra/email/email.registry.ts +1 -0
  196. package/templates/ecommerce/apps/api/src/shared/infra/prisma.ts +1 -0
  197. package/templates/ecommerce/apps/api/src/shared/infra/redis.ts +1 -0
  198. package/templates/ecommerce/apps/api/src/shared/infra/storage/IStorageService.ts +1 -0
  199. package/templates/ecommerce/apps/api/src/shared/infra/storage/InMemoryStorageService.ts +1 -0
  200. package/templates/ecommerce/apps/api/src/shared/infra/storage/LocalDiskStorageService.ts +1 -0
  201. package/templates/ecommerce/apps/api/src/shared/infra/storage/S3StorageService.ts +1 -0
  202. package/templates/ecommerce/apps/api/src/shared/infra/storage/__tests__/s3.storage.unit.test.ts +73 -0
  203. package/templates/ecommerce/apps/api/src/shared/infra/storage/storage.registry.ts +1 -0
  204. package/templates/ecommerce/apps/api/src/shared/middlewares/authenticate.ts +1 -0
  205. package/templates/ecommerce/apps/api/src/shared/middlewares/authorize.ts +1 -0
  206. package/templates/ecommerce/apps/api/src/shared/middlewares/errorHandler.ts +1 -0
  207. package/templates/ecommerce/apps/api/src/shared/validators/uuidParam.ts +1 -0
  208. package/templates/ecommerce/apps/api/tsconfig.json +15 -0
  209. package/templates/ecommerce/apps/web/.env.example +8 -0
  210. package/templates/ecommerce/apps/web/index.html +19 -0
  211. package/templates/ecommerce/apps/web/jest.config.ts +45 -0
  212. package/templates/ecommerce/apps/web/package.json +38 -0
  213. package/templates/ecommerce/apps/web/src/App.tsx +133 -0
  214. package/templates/ecommerce/apps/web/src/__mocks__/fileMock.ts +1 -0
  215. package/templates/ecommerce/apps/web/src/__mocks__/styleMock.ts +1 -0
  216. package/templates/ecommerce/apps/web/src/index.css +159 -0
  217. package/templates/ecommerce/apps/web/src/main.tsx +13 -0
  218. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/CouponsAdminPage.test.tsx +134 -0
  219. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/DashboardPage.test.tsx +65 -0
  220. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/OrdersAdminPage.test.tsx +79 -0
  221. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/ProductsAdminPage.test.tsx +84 -0
  222. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/UsersAdminPage.test.tsx +85 -0
  223. package/templates/ecommerce/apps/web/src/modules/admin/pages/CouponsAdminPage.tsx +179 -0
  224. package/templates/ecommerce/apps/web/src/modules/admin/pages/DashboardPage.tsx +58 -0
  225. package/templates/ecommerce/apps/web/src/modules/admin/pages/OrdersAdminPage.tsx +178 -0
  226. package/templates/ecommerce/apps/web/src/modules/admin/pages/ProductsAdminPage.tsx +444 -0
  227. package/templates/ecommerce/apps/web/src/modules/admin/pages/UsersAdminPage.tsx +87 -0
  228. package/templates/ecommerce/apps/web/src/modules/auth/LoginForm.tsx +91 -0
  229. package/templates/ecommerce/apps/web/src/modules/auth/RegisterForm.tsx +109 -0
  230. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ForgotPasswordPage.test.tsx +42 -0
  231. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/LoginForm.test.tsx +76 -0
  232. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/RegisterForm.test.tsx +62 -0
  233. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ResetPasswordPage.test.tsx +66 -0
  234. package/templates/ecommerce/apps/web/src/modules/auth/pages/ForgotPasswordPage.tsx +100 -0
  235. package/templates/ecommerce/apps/web/src/modules/auth/pages/LoginPage.tsx +39 -0
  236. package/templates/ecommerce/apps/web/src/modules/auth/pages/RegisterPage.tsx +39 -0
  237. package/templates/ecommerce/apps/web/src/modules/auth/pages/ResetPasswordPage.tsx +110 -0
  238. package/templates/ecommerce/apps/web/src/modules/auth/useAuthStore.ts +141 -0
  239. package/templates/ecommerce/apps/web/src/modules/cart/__tests__/CartPage.test.tsx +111 -0
  240. package/templates/ecommerce/apps/web/src/modules/cart/pages/CartPage.tsx +313 -0
  241. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductCard.test.tsx +59 -0
  242. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductFilters.test.tsx +56 -0
  243. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductCard.tsx +78 -0
  244. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductFilters.tsx +104 -0
  245. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductDetailPage.tsx +179 -0
  246. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductListPage.tsx +100 -0
  247. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/CheckoutPage.test.tsx +159 -0
  248. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/StripePaymentForm.test.tsx +79 -0
  249. package/templates/ecommerce/apps/web/src/modules/checkout/components/StripePaymentForm.tsx +55 -0
  250. package/templates/ecommerce/apps/web/src/modules/checkout/hooks/useCheckout.ts +56 -0
  251. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutPage.tsx +344 -0
  252. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutSuccessPage.tsx +12 -0
  253. package/templates/ecommerce/apps/web/src/modules/legal/pages/PrivacyPolicyPage.tsx +207 -0
  254. package/templates/ecommerce/apps/web/src/modules/legal/pages/TermsOfServicePage.tsx +175 -0
  255. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderDetailPage.test.tsx +75 -0
  256. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderHistoryPage.test.tsx +87 -0
  257. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderDetailPage.tsx +73 -0
  258. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderHistoryPage.tsx +97 -0
  259. package/templates/ecommerce/apps/web/src/modules/profile/__tests__/ProfilePage.test.tsx +150 -0
  260. package/templates/ecommerce/apps/web/src/modules/profile/pages/ProfilePage.tsx +275 -0
  261. package/templates/ecommerce/apps/web/src/setupTests.ts +10 -0
  262. package/templates/ecommerce/apps/web/src/shared/components/CookieConsent.tsx +108 -0
  263. package/templates/ecommerce/apps/web/src/shared/components/ErrorBoundary.tsx +112 -0
  264. package/templates/ecommerce/apps/web/src/shared/components/Layout.tsx +143 -0
  265. package/templates/ecommerce/apps/web/src/shared/components/ProtectedRoute.tsx +21 -0
  266. package/templates/ecommerce/apps/web/src/shared/config/siteConfig.ts +57 -0
  267. package/templates/ecommerce/apps/web/src/shared/hooks/usePageTitle.ts +16 -0
  268. package/templates/ecommerce/apps/web/src/shared/lib/apiFetch.ts +16 -0
  269. package/templates/ecommerce/apps/web/src/shared/pages/NotFoundPage.tsx +42 -0
  270. package/templates/ecommerce/apps/web/src/shared/theme/ThemeProvider.tsx +45 -0
  271. package/templates/ecommerce/apps/web/src/shared/theme/__tests__/ThemeProvider.test.tsx +78 -0
  272. package/templates/ecommerce/apps/web/src/shared/theme/createTheme.ts +58 -0
  273. package/templates/ecommerce/apps/web/src/shared/theme/tokens.ts +81 -0
  274. package/templates/ecommerce/apps/web/src/vite-env.d.ts +1 -0
  275. package/templates/ecommerce/apps/web/tsconfig.jest.json +12 -0
  276. package/templates/ecommerce/apps/web/tsconfig.json +25 -0
  277. package/templates/ecommerce/apps/web/tsconfig.node.json +11 -0
  278. package/templates/ecommerce/apps/web/vite.config.ts +30 -0
  279. package/templates/ecommerce/docker-compose.yml +85 -0
  280. package/templates/ecommerce/package-lock.json +11255 -0
  281. package/templates/ecommerce/package.json +27 -0
  282. package/templates/ecommerce/packages/shared-types/package.json +13 -0
  283. package/templates/ecommerce/packages/shared-types/src/index.ts +3 -0
  284. package/templates/ecommerce/packages/shared-types/src/theme.ts +44 -0
  285. package/templates/ecommerce/packages/shared-types/tsconfig.json +11 -0
  286. package/templates/ecommerce/scripts/customize.sh +201 -0
  287. package/templates/ecommerce/tsconfig.json +14 -0
@@ -0,0 +1,149 @@
1
+ import type { PrismaClient, Order, OrderItem } from '@prisma/client';
2
+ import { OrderEntity } from '../../../../domain/checkout/order.entity';
3
+ import type { IOrderRepository } from '../../../../domain/checkout/order.repository';
4
+ import type { OrderStatus } from '../../../../domain/checkout/order.entity';
5
+
6
+ type OrderWithItems = Order & { items: OrderItem[] };
7
+
8
+ function toEntity(row: OrderWithItems): OrderEntity {
9
+ return OrderEntity.reconstitute({
10
+ id: row.id,
11
+ userId: row.userId,
12
+ status: row.status as OrderStatus,
13
+ items: row.items.map((i) => ({
14
+ id: i.id,
15
+ variantId: i.variantId,
16
+ productId: i.productId,
17
+ name: i.name,
18
+ sku: i.sku,
19
+ price: Number(i.price),
20
+ qty: i.qty,
21
+ image: i.image,
22
+ })),
23
+ subtotal: Number(row.subtotal),
24
+ discount: Number(row.discount),
25
+ shippingCost: Number(row.shippingCost),
26
+ tax: Number(row.tax),
27
+ total: Number(row.total),
28
+ couponCode: row.couponCode,
29
+ paymentIntentId: row.paymentIntentId,
30
+ trackingCode: row.trackingCode,
31
+ shippingAddress: row.shippingAddress as Record<string, string> | null,
32
+ createdAt: row.createdAt,
33
+ updatedAt: row.updatedAt,
34
+ });
35
+ }
36
+
37
+ const INCLUDE_ITEMS = { items: true } as const;
38
+
39
+ export class PrismaOrderRepository implements IOrderRepository {
40
+ constructor(private readonly db: PrismaClient) {}
41
+
42
+ async findById(id: string): Promise<OrderEntity | null> {
43
+ const row = await this.db.order.findUnique({
44
+ where: { id },
45
+ include: INCLUDE_ITEMS,
46
+ });
47
+ return row ? toEntity(row) : null;
48
+ }
49
+
50
+ async findByPaymentIntentId(paymentIntentId: string): Promise<OrderEntity | null> {
51
+ const row = await this.db.order.findUnique({
52
+ where: { paymentIntentId },
53
+ include: INCLUDE_ITEMS,
54
+ });
55
+ return row ? toEntity(row) : null;
56
+ }
57
+
58
+ async findByUserId(
59
+ userId: string,
60
+ opts: { cursor?: string; limit?: number } = {},
61
+ ): Promise<{ items: OrderEntity[]; nextCursor: string | null }> {
62
+ const limit = opts.limit ?? 20;
63
+
64
+ const rows = await this.db.order.findMany({
65
+ where: { userId },
66
+ orderBy: { createdAt: 'desc' },
67
+ include: INCLUDE_ITEMS,
68
+ take: limit + 1,
69
+ ...(opts.cursor ? { cursor: { id: opts.cursor }, skip: 1 } : {}),
70
+ });
71
+
72
+ const hasMore = rows.length > limit;
73
+ const items = hasMore ? rows.slice(0, limit) : rows;
74
+ const nextCursor = hasMore ? items[items.length - 1]!.id : null;
75
+
76
+ return { items: items.map(toEntity), nextCursor };
77
+ }
78
+
79
+ async findAll(
80
+ opts: { status?: OrderStatus; cursor?: string; limit?: number } = {},
81
+ ): Promise<{ items: OrderEntity[]; nextCursor: string | null }> {
82
+ const limit = opts.limit ?? 20;
83
+
84
+ const rows = await this.db.order.findMany({
85
+ where: opts.status ? { status: opts.status } : undefined,
86
+ orderBy: { createdAt: 'desc' },
87
+ include: INCLUDE_ITEMS,
88
+ take: limit + 1,
89
+ ...(opts.cursor ? { cursor: { id: opts.cursor }, skip: 1 } : {}),
90
+ });
91
+
92
+ const hasMore = rows.length > limit;
93
+ const items = hasMore ? rows.slice(0, limit) : rows;
94
+ const nextCursor = hasMore ? items[items.length - 1]!.id : null;
95
+
96
+ return { items: items.map(toEntity), nextCursor };
97
+ }
98
+
99
+ async create(order: OrderEntity): Promise<OrderEntity> {
100
+ const row = await this.db.order.create({
101
+ data: {
102
+ id: order.id,
103
+ userId: order.userId,
104
+ status: order.status,
105
+ subtotal: order.subtotal,
106
+ discount: order.discount,
107
+ shippingCost: order.shippingCost,
108
+ tax: order.tax,
109
+ total: order.total,
110
+ couponCode: order.couponCode,
111
+ paymentIntentId: order.paymentIntentId,
112
+ trackingCode: order.trackingCode,
113
+ shippingAddress: order.shippingAddress ?? undefined,
114
+ createdAt: order.createdAt,
115
+ updatedAt: order.updatedAt,
116
+ items: {
117
+ createMany: {
118
+ data: order.items.map((i) => ({
119
+ id: i.id,
120
+ variantId: i.variantId,
121
+ productId: i.productId,
122
+ name: i.name,
123
+ sku: i.sku,
124
+ price: i.price,
125
+ qty: i.qty,
126
+ image: i.image,
127
+ })),
128
+ },
129
+ },
130
+ },
131
+ include: INCLUDE_ITEMS,
132
+ });
133
+ return toEntity(row);
134
+ }
135
+
136
+ async update(order: OrderEntity): Promise<OrderEntity> {
137
+ const row = await this.db.order.update({
138
+ where: { id: order.id },
139
+ data: {
140
+ status: order.status,
141
+ paymentIntentId: order.paymentIntentId,
142
+ trackingCode: order.trackingCode,
143
+ shippingAddress: order.shippingAddress ?? undefined,
144
+ },
145
+ include: INCLUDE_ITEMS,
146
+ });
147
+ return toEntity(row);
148
+ }
149
+ }
@@ -0,0 +1,17 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ // Prevent multiple PrismaClient instances in development / hot-reload
4
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
5
+
6
+ export const prisma =
7
+ globalForPrisma.prisma ??
8
+ new PrismaClient({
9
+ log:
10
+ process.env['NODE_ENV'] === 'development'
11
+ ? ['query', 'error', 'warn']
12
+ : ['error'],
13
+ });
14
+
15
+ if (process.env['NODE_ENV'] !== 'production') {
16
+ globalForPrisma.prisma = prisma;
17
+ }
@@ -0,0 +1,18 @@
1
+ import type { IEmailService } from '../../../application/ports/email.port';
2
+ import { EtherealEmailService } from './ethereal.email.service';
3
+ import { SmtpEmailService } from './smtp.email.service';
4
+ import { NoopEmailService } from './noop.email.service';
5
+
6
+ /**
7
+ * Picks the email service implementation based on NODE_ENV.
8
+ * production → SmtpEmailService (real SMTP via env vars)
9
+ * test / test-inmemory → NoopEmailService (silent, no network calls)
10
+ * everything else → EtherealEmailService (fake SMTP, no real delivery)
11
+ */
12
+ const env = process.env['NODE_ENV'];
13
+ export const emailService: IEmailService =
14
+ env === 'production'
15
+ ? new SmtpEmailService()
16
+ : env === 'test' || env === 'test-inmemory'
17
+ ? new NoopEmailService()
18
+ : new EtherealEmailService();
@@ -0,0 +1,38 @@
1
+ import nodemailer from 'nodemailer';
2
+ import type { IEmailService } from '../../../application/ports/email.port';
3
+
4
+ /**
5
+ * Email service that uses Ethereal (https://ethereal.email) — a fake SMTP
6
+ * server that captures messages without delivering them. Perfect for
7
+ * development and testing. Logs a preview URL to the console after each send.
8
+ */
9
+ export class EtherealEmailService implements IEmailService {
10
+ private transporter: nodemailer.Transporter | null = null;
11
+
12
+ private async getTransporter(): Promise<nodemailer.Transporter> {
13
+ if (this.transporter) return this.transporter;
14
+
15
+ const testAccount = await nodemailer.createTestAccount();
16
+ this.transporter = nodemailer.createTransport({
17
+ host: 'smtp.ethereal.email',
18
+ port: 587,
19
+ auth: {
20
+ user: testAccount.user,
21
+ pass: testAccount.pass,
22
+ },
23
+ });
24
+
25
+ return this.transporter;
26
+ }
27
+
28
+ async send(to: string, subject: string, html: string): Promise<void> {
29
+ const transporter = await this.getTransporter();
30
+ const info = await transporter.sendMail({
31
+ from: process.env['EMAIL_FROM'] ?? 'noreply@example.com',
32
+ to,
33
+ subject,
34
+ html,
35
+ });
36
+ console.log(`[Ethereal Email] Preview URL: ${nodemailer.getTestMessageUrl(info)}`);
37
+ }
38
+ }
@@ -0,0 +1,12 @@
1
+ import type { IEmailService } from '../../../application/ports/email.port';
2
+
3
+ /**
4
+ * No-op email service used during tests.
5
+ * Silently discards all messages — no network calls, no side effects.
6
+ */
7
+ export class NoopEmailService implements IEmailService {
8
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
+ async send(_to: string, _subject: string, _html: string): Promise<void> {
10
+ // intentionally empty
11
+ }
12
+ }
@@ -0,0 +1,36 @@
1
+ import nodemailer from 'nodemailer';
2
+ import type { IEmailService } from '../../../application/ports/email.port';
3
+ import { AppError } from '../../../domain/shared/AppError';
4
+
5
+ /**
6
+ * Production email service that uses a real SMTP server configured via
7
+ * environment variables: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, EMAIL_FROM.
8
+ */
9
+ export class SmtpEmailService implements IEmailService {
10
+ private readonly transporter: nodemailer.Transporter;
11
+
12
+ constructor() {
13
+ this.transporter = nodemailer.createTransport({
14
+ host: process.env['SMTP_HOST'],
15
+ port: parseInt(process.env['SMTP_PORT'] ?? '587', 10),
16
+ secure: process.env['SMTP_PORT'] === '465',
17
+ auth: {
18
+ user: process.env['SMTP_USER'],
19
+ pass: process.env['SMTP_PASS'],
20
+ },
21
+ });
22
+ }
23
+
24
+ async send(to: string, subject: string, html: string): Promise<void> {
25
+ try {
26
+ await this.transporter.sendMail({
27
+ from: process.env['EMAIL_FROM'] ?? 'noreply@example.com',
28
+ to,
29
+ subject,
30
+ html,
31
+ });
32
+ } catch {
33
+ throw new AppError('Falha ao enviar email', 500);
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,83 @@
1
+ import Stripe from 'stripe';
2
+ import { AppError } from '../../../domain/shared/AppError';
3
+ import type { IOrderRepository } from '../../../domain/checkout/order.repository';
4
+ import type { ICouponRepository } from '../../../domain/cart/coupon.repository';
5
+ import type { IEmailService } from '../../../application/ports/email.port';
6
+ import type { IUserRepository } from '../../../domain/auth/user.repository';
7
+ import { OrderEntity } from '../../../domain/checkout/order.entity';
8
+
9
+ export class StripeWebhookHandler {
10
+ private readonly stripe: Stripe;
11
+
12
+ constructor(
13
+ secretKey: string,
14
+ private readonly webhookSecret: string,
15
+ private readonly orderRepo: IOrderRepository,
16
+ private readonly couponRepo?: ICouponRepository,
17
+ private readonly emailService?: IEmailService,
18
+ private readonly userRepo?: IUserRepository,
19
+ ) {
20
+ this.stripe = new Stripe(secretKey);
21
+ }
22
+
23
+ async handleEvent(payload: string, signature: string): Promise<void> {
24
+ let event: Stripe.Event;
25
+ try {
26
+ event = this.stripe.webhooks.constructEvent(payload, signature, this.webhookSecret);
27
+ } catch {
28
+ throw new AppError('Webhook Stripe: assinatura inválida', 400);
29
+ }
30
+
31
+ switch (event.type) {
32
+ case 'payment_intent.succeeded':
33
+ await this._handleSucceeded(event.data.object as Stripe.PaymentIntent);
34
+ break;
35
+ case 'payment_intent.payment_failed':
36
+ await this._handleFailed(event.data.object as Stripe.PaymentIntent);
37
+ break;
38
+ default:
39
+ // Unknown events are silently ignored
40
+ break;
41
+ }
42
+ }
43
+
44
+ private async _handleSucceeded(pi: Stripe.PaymentIntent): Promise<void> {
45
+ const order = await this.orderRepo.findByPaymentIntentId(pi.id);
46
+ if (!order) return; // Order not found — ignore
47
+ if (order.status === 'PAID') return; // Idempotent: already processed
48
+ const updated = order.pay();
49
+ await this.orderRepo.update(updated);
50
+ if (order.couponCode && this.couponRepo) {
51
+ await this.couponRepo.incrementUsage(order.couponCode);
52
+ }
53
+ await this._sendOrderConfirmedEmail(updated);
54
+ }
55
+
56
+ private async _sendOrderConfirmedEmail(order: OrderEntity): Promise<void> {
57
+ if (!this.emailService || !this.userRepo) return;
58
+ try {
59
+ const user = await this.userRepo.findById(order.userId);
60
+ if (!user) return;
61
+ const itemsHtml = order.items
62
+ .map((i) => `<li>${i.name} x${i.qty} — R$ ${(i.price * i.qty).toFixed(2)}</li>`)
63
+ .join('');
64
+ const html = `
65
+ <h2>Pedido confirmado!</h2>
66
+ <p>Olá ${user.name}, seu pedido <strong>#${order.id.slice(0, 8)}</strong> foi confirmado.</p>
67
+ <ul>${itemsHtml}</ul>
68
+ <p><strong>Total: R$ ${order.total.toFixed(2)}</strong></p>
69
+ <p>Prazo estimado de entrega: 5–10 dias úteis.</p>
70
+ `;
71
+ await this.emailService.send(user.email, 'Pedido confirmado 🎉', html);
72
+ } catch {
73
+ // Email failure must never break the webhook flow
74
+ }
75
+ }
76
+
77
+ private async _handleFailed(pi: Stripe.PaymentIntent): Promise<void> {
78
+ const order = await this.orderRepo.findByPaymentIntentId(pi.id);
79
+ if (!order) return;
80
+ if (order.status === 'FAILED') return; // Idempotent
81
+ await this.orderRepo.update(order.fail());
82
+ }
83
+ }
@@ -0,0 +1,39 @@
1
+ import Stripe from 'stripe';
2
+ import { IPaymentAdapter, PaymentError, PaymentIntentResult } from '../../../application/ports/payment.port';
3
+
4
+ export class StripeAdapter implements IPaymentAdapter {
5
+ private readonly stripe: Stripe;
6
+
7
+ constructor(secretKey: string) {
8
+ this.stripe = new Stripe(secretKey);
9
+ }
10
+
11
+ async createPaymentIntent(
12
+ amount: number,
13
+ currency: string,
14
+ metadata: Record<string, string>,
15
+ ): Promise<PaymentIntentResult> {
16
+ try {
17
+ const intent = await this.stripe.paymentIntents.create({
18
+ amount: Math.round(amount * 100), // BRL → centavos
19
+ currency,
20
+ metadata,
21
+ automatic_payment_methods: { enabled: true },
22
+ });
23
+
24
+ if (!intent.client_secret) {
25
+ throw new PaymentError('missing_client_secret', 'Stripe não retornou client_secret');
26
+ }
27
+
28
+ return { id: intent.id, clientSecret: intent.client_secret };
29
+ } catch (err) {
30
+ if (err instanceof PaymentError) throw err;
31
+ // Wrap Stripe errors in a PaymentError so callers don't depend on Stripe types
32
+ const stripeErr = err as { type?: string; code?: string; message?: string };
33
+ throw new PaymentError(
34
+ stripeErr.code ?? stripeErr.type ?? 'stripe_error',
35
+ stripeErr.message ?? 'Erro no processamento do pagamento',
36
+ );
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,17 @@
1
+ import type { IShippingService, ShippingOption } from '../../../application/ports/shipping.port';
2
+
3
+ /**
4
+ * Returns deterministic mock options — pluggable with Correios / Melhor Envio
5
+ * in a real deployment by swapping the implementation.
6
+ */
7
+ export class MockShippingService implements IShippingService {
8
+ async calculate(
9
+ _cep: string,
10
+ _items: Array<{ qty: number; weight?: number }>,
11
+ ): Promise<ShippingOption[]> {
12
+ return [
13
+ { name: 'PAC', price: 18.5, estimatedDays: 7 },
14
+ { name: 'SEDEX', price: 32.0, estimatedDays: 2 },
15
+ ];
16
+ }
17
+ }
@@ -0,0 +1,11 @@
1
+ import type { IStorageService } from '../../../application/ports/storage.port';
2
+
3
+ /**
4
+ * In-memory storage — used in tests and when S3_BUCKET is not configured.
5
+ * Files are never persisted; returns a deterministic fake URL.
6
+ */
7
+ export class InMemoryStorageService implements IStorageService {
8
+ async upload(key: string, _buffer: Buffer, _mimetype: string): Promise<string> {
9
+ return `http://storage.local/${key}`;
10
+ }
11
+ }
@@ -0,0 +1,27 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import type { IStorageService } from '../../../application/ports/storage.port';
4
+
5
+ /**
6
+ * LocalDiskStorageService — persists uploaded files to the local filesystem
7
+ * under `apps/api/public/uploads/`. Used for local development when neither
8
+ * S3 nor MinIO is configured.
9
+ *
10
+ * Files are served by the Express static middleware at /public/uploads/<key>.
11
+ */
12
+ export class LocalDiskStorageService implements IStorageService {
13
+ private readonly uploadDir: string;
14
+
15
+ constructor(uploadDir?: string) {
16
+ this.uploadDir =
17
+ uploadDir ?? path.join(__dirname, '../../../../../public/uploads');
18
+ }
19
+
20
+ async upload(key: string, buffer: Buffer, _mimetype: string): Promise<string> {
21
+ fs.mkdirSync(this.uploadDir, { recursive: true });
22
+ const filePath = path.join(this.uploadDir, key);
23
+ fs.writeFileSync(filePath, buffer);
24
+ const baseUrl = process.env['BASE_URL'] ?? 'http://localhost:3000';
25
+ return `${baseUrl}/public/uploads/${key}`;
26
+ }
27
+ }
@@ -0,0 +1,52 @@
1
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
2
+ import type { IStorageService } from '../../../application/ports/storage.port';
3
+ import { AppError } from '../../../domain/shared/AppError';
4
+
5
+ /**
6
+ * S3-compatible storage service.
7
+ * Points to AWS S3 in production.
8
+ * In dev/CI, override S3_ENDPOINT to use MinIO (http://localhost:9000).
9
+ */
10
+ export class S3StorageService implements IStorageService {
11
+ private readonly client: S3Client;
12
+ private readonly bucket: string;
13
+ private readonly publicBase: string;
14
+
15
+ constructor() {
16
+ this.bucket = process.env['S3_BUCKET'] ?? 'products';
17
+ const region = process.env['S3_REGION'] ?? 'us-east-1';
18
+ const endpoint = process.env['S3_ENDPOINT'];
19
+
20
+ this.client = new S3Client({
21
+ region,
22
+ ...(endpoint
23
+ ? { endpoint, forcePathStyle: true }
24
+ : {}),
25
+ credentials: {
26
+ accessKeyId: process.env['AWS_ACCESS_KEY_ID'] ?? '',
27
+ secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'] ?? '',
28
+ },
29
+ });
30
+
31
+ // Public URL base: MinIO-style (endpoint/bucket) or S3 virtual-hosted
32
+ this.publicBase = endpoint
33
+ ? `${endpoint}/${this.bucket}`
34
+ : `https://${this.bucket}.s3.${region}.amazonaws.com`;
35
+ }
36
+
37
+ async upload(key: string, buffer: Buffer, mimetype: string): Promise<string> {
38
+ try {
39
+ await this.client.send(
40
+ new PutObjectCommand({
41
+ Bucket: this.bucket,
42
+ Key: key,
43
+ Body: buffer,
44
+ ContentType: mimetype,
45
+ }),
46
+ );
47
+ return `${this.publicBase}/${key}`;
48
+ } catch {
49
+ throw new AppError('Falha ao fazer upload da imagem', 500);
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,19 @@
1
+ import type { IStorageService } from '../../../application/ports/storage.port';
2
+ import { S3StorageService } from './s3.storage.service';
3
+ import { InMemoryStorageService } from './in-memory.storage.service';
4
+ import { LocalDiskStorageService } from './local-disk.storage.service';
5
+
6
+ const env = process.env['NODE_ENV'];
7
+
8
+ /**
9
+ * Picks the storage implementation:
10
+ * test / test-inmemory → InMemoryStorageService (no disk I/O in tests)
11
+ * S3_BUCKET set → S3StorageService (real S3 or MinIO)
12
+ * Otherwise → LocalDiskStorageService (dev without MinIO)
13
+ */
14
+ export const storageService: IStorageService =
15
+ env === 'test' || env === 'test-inmemory'
16
+ ? new InMemoryStorageService()
17
+ : process.env['S3_BUCKET']
18
+ ? new S3StorageService()
19
+ : new LocalDiskStorageService();
@@ -0,0 +1,23 @@
1
+ import type { Redis } from 'ioredis';
2
+ import type { ITokenBlacklist } from '../../../application/ports/token-blacklist.port';
3
+
4
+ const KEY_PREFIX = 'blacklist:';
5
+
6
+ /**
7
+ * Redis-backed token blacklist.
8
+ * Uses a simple SET with EX (TTL) — the key expires automatically, so no
9
+ * manual cleanup is needed.
10
+ */
11
+ export class RedisTokenBlacklist implements ITokenBlacklist {
12
+ constructor(private readonly redis: Redis) {}
13
+
14
+ async add(jti: string, ttlSeconds: number): Promise<void> {
15
+ if (ttlSeconds <= 0) return; // token already expired — nothing to store
16
+ await this.redis.set(`${KEY_PREFIX}${jti}`, '1', 'EX', ttlSeconds);
17
+ }
18
+
19
+ async has(jti: string): Promise<boolean> {
20
+ const val = await this.redis.get(`${KEY_PREFIX}${jti}`);
21
+ return val !== null;
22
+ }
23
+ }
@@ -0,0 +1,30 @@
1
+ import type { ITokenBlacklist } from '../../../application/ports/token-blacklist.port';
2
+
3
+ /**
4
+ * In-process implementation used for unit tests and dev when Redis is not
5
+ * available. Honors TTL via timestamp comparison.
6
+ */
7
+ export class InMemoryTokenBlacklist implements ITokenBlacklist {
8
+ /** jti → expiry epoch-ms (0 = never expires) */
9
+ private readonly store = new Map<string, number>();
10
+
11
+ async add(jti: string, ttlSeconds: number): Promise<void> {
12
+ const expiry = ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : 0;
13
+ this.store.set(jti, expiry);
14
+ }
15
+
16
+ async has(jti: string): Promise<boolean> {
17
+ const expiry = this.store.get(jti);
18
+ if (expiry === undefined) return false;
19
+ if (expiry !== 0 && Date.now() > expiry) {
20
+ this.store.delete(jti);
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+
26
+ /** Used in tests to reset state between cases. */
27
+ clear(): void {
28
+ this.store.clear();
29
+ }
30
+ }