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,136 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { randomUUID } from 'crypto';
3
+ import { AppError } from '../../../domain/shared/AppError';
4
+ import { InMemoryTokenBlacklist } from './token.blacklist';
5
+ import type { ITokenBlacklist } from '../../../application/ports/token-blacklist.port';
6
+ import type { ITokenService, TokenPayload } from '../../../application/ports/token.port';
7
+ import type { PrismaClient } from '@prisma/client';
8
+
9
+ export class TokenService implements ITokenService {
10
+ // in-memory fallback used in tests (no DB)
11
+ private readonly resetTokensFallback = new Map<string, string>();
12
+
13
+ constructor(
14
+ private readonly blacklist: ITokenBlacklist = new InMemoryTokenBlacklist(),
15
+ private readonly prismaClient?: PrismaClient,
16
+ ) {}
17
+
18
+ private get jwtSecret(): string {
19
+ const s = process.env['JWT_SECRET'];
20
+ if (!s) throw new Error('JWT_SECRET is not set');
21
+ return s;
22
+ }
23
+
24
+ private get jwtRefreshSecret(): string {
25
+ const s = process.env['JWT_REFRESH_SECRET'];
26
+ if (!s) throw new Error('JWT_REFRESH_SECRET is not set');
27
+ return s;
28
+ }
29
+
30
+ private get jwtResetSecret(): string {
31
+ const s = process.env['JWT_RESET_SECRET'] ?? process.env['JWT_SECRET'];
32
+ if (!s) throw new Error('JWT_RESET_SECRET is not set');
33
+ return s;
34
+ }
35
+
36
+ generateAccessToken(payload: TokenPayload): string {
37
+ return jwt.sign(payload, this.jwtSecret, {
38
+ expiresIn: (process.env['JWT_EXPIRES_IN'] as jwt.SignOptions['expiresIn']) ?? '15m',
39
+ });
40
+ }
41
+
42
+ generateRefreshToken(payload: TokenPayload): string {
43
+ return jwt.sign(
44
+ { ...payload, jti: randomUUID() },
45
+ this.jwtRefreshSecret,
46
+ {
47
+ expiresIn:
48
+ (process.env['JWT_REFRESH_EXPIRES_IN'] as jwt.SignOptions['expiresIn']) ?? '7d',
49
+ },
50
+ );
51
+ }
52
+
53
+ verifyAccessToken(token: string): TokenPayload {
54
+ try {
55
+ return jwt.verify(token, this.jwtSecret) as TokenPayload;
56
+ } catch {
57
+ throw new AppError('Token inválido ou expirado', 401);
58
+ }
59
+ }
60
+
61
+ verifyRefreshToken(token: string): TokenPayload {
62
+ return jwt.verify(token, this.jwtRefreshSecret) as TokenPayload;
63
+ }
64
+
65
+ async invalidateRefreshToken(token: string): Promise<undefined> {
66
+ const decoded = jwt.decode(token) as (TokenPayload & { exp?: number }) | null;
67
+ const jti = decoded?.jti;
68
+ if (!jti) return undefined;
69
+ const exp = decoded?.exp ?? 0;
70
+ const ttlSeconds = Math.max(0, exp - Math.floor(Date.now() / 1000));
71
+ await this.blacklist.add(jti, ttlSeconds);
72
+ return undefined;
73
+ }
74
+
75
+ async isRefreshTokenBlacklisted(token: string): Promise<boolean> {
76
+ const decoded = jwt.decode(token) as (TokenPayload & { exp?: number }) | null;
77
+ const jti = decoded?.jti;
78
+ if (!jti) return false;
79
+ return this.blacklist.has(jti);
80
+ }
81
+
82
+ async generatePasswordResetToken(userId: string): Promise<string> {
83
+ const token = jwt.sign({ sub: userId }, this.jwtResetSecret, { expiresIn: '1h' });
84
+ if (this.prismaClient) {
85
+ // Invalidate any existing tokens for this user, then create a new one
86
+ await this.prismaClient.passwordResetToken.deleteMany({ where: { userId } });
87
+ await this.prismaClient.passwordResetToken.create({
88
+ data: {
89
+ token,
90
+ userId,
91
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
92
+ },
93
+ });
94
+ } else {
95
+ this.resetTokensFallback.set(token, userId);
96
+ }
97
+ return token;
98
+ }
99
+
100
+ async verifyPasswordResetToken(token: string): Promise<string> {
101
+ try {
102
+ const payload = jwt.verify(token, this.jwtResetSecret) as { sub: string };
103
+ if (this.prismaClient) {
104
+ const record = await this.prismaClient.passwordResetToken.findUnique({
105
+ where: { token },
106
+ });
107
+ if (!record || record.usedAt !== null) {
108
+ throw new AppError('Token de recuperação inválido ou já utilizado', 400);
109
+ }
110
+ if (record.expiresAt < new Date()) {
111
+ throw new AppError('Token de recuperação expirado', 400);
112
+ }
113
+ return record.userId;
114
+ } else {
115
+ const storedUserId = this.resetTokensFallback.get(token);
116
+ if (!storedUserId) throw new Error('Token not found');
117
+ return payload.sub;
118
+ }
119
+ } catch (err) {
120
+ if (err instanceof AppError) throw err;
121
+ throw new AppError('Token de recuperação inválido ou expirado', 400);
122
+ }
123
+ }
124
+
125
+ async invalidatePasswordResetToken(token: string): Promise<undefined> {
126
+ if (this.prismaClient) {
127
+ await this.prismaClient.passwordResetToken.updateMany({
128
+ where: { token },
129
+ data: { usedAt: new Date() },
130
+ });
131
+ } else {
132
+ this.resetTokensFallback.delete(token);
133
+ }
134
+ return undefined;
135
+ }
136
+ }
@@ -0,0 +1,250 @@
1
+ import request from 'supertest';
2
+ import bcrypt from 'bcryptjs';
3
+ import app from '../../../app';
4
+ import { userRepository } from '../../auth/auth.registry';
5
+ import { orderRepository } from '../../checkout/checkout.registry';
6
+ import { UserEntity } from '../../auth/user.entity';
7
+ import { OrderEntity } from '../../checkout/order.entity';
8
+
9
+ describe('Admin Routes — Integration', () => {
10
+ let adminToken: string;
11
+ let adminId: string;
12
+ let customerToken: string;
13
+ let customer1Id: string;
14
+ let customer2Id: string;
15
+
16
+ beforeAll(async () => {
17
+ // ── Users ──────────────────────────────────────────────────────────────
18
+ const adminHash = await bcrypt.hash('admin123', 1);
19
+ const admin = UserEntity.create({
20
+ name: 'Admin',
21
+ email: 'admin6@test.com',
22
+ passwordHash: adminHash,
23
+ role: 'ADMIN',
24
+ });
25
+ await userRepository.create(admin);
26
+ adminId = admin.id;
27
+
28
+ const adminRes = await request(app)
29
+ .post('/api/auth/login')
30
+ .send({ email: 'admin6@test.com', password: 'admin123' });
31
+ adminToken = adminRes.body.accessToken as string;
32
+
33
+ const c1Hash = await bcrypt.hash('pass123', 1);
34
+ const customer1 = UserEntity.create({
35
+ name: 'Alice',
36
+ email: 'alice6@test.com',
37
+ passwordHash: c1Hash,
38
+ });
39
+ await userRepository.create(customer1);
40
+ customer1Id = customer1.id;
41
+
42
+ const c1Res = await request(app)
43
+ .post('/api/auth/login')
44
+ .send({ email: 'alice6@test.com', password: 'pass123' });
45
+ customerToken = c1Res.body.accessToken as string;
46
+
47
+ const c2Hash = await bcrypt.hash('pass456', 1);
48
+ const customer2 = UserEntity.create({
49
+ name: 'Bob',
50
+ email: 'bob6@test.com',
51
+ passwordHash: c2Hash,
52
+ });
53
+ await userRepository.create(customer2);
54
+ customer2Id = customer2.id;
55
+
56
+ // ── Seed orders ────────────────────────────────────────────────────────
57
+ // Order 1: PAID, customer1, total=100, prod-1 qty=3
58
+ const order1 = OrderEntity.create({
59
+ userId: customer1Id,
60
+ items: [
61
+ {
62
+ id: 'i1',
63
+ productId: 'prod-1',
64
+ variantId: 'v1',
65
+ name: 'Camiseta',
66
+ sku: 'CAM-1',
67
+ price: 33.33,
68
+ qty: 3,
69
+ image: null,
70
+ },
71
+ ],
72
+ subtotal: 100,
73
+ discount: 0,
74
+ shippingCost: 0,
75
+ tax: 0,
76
+ total: 100,
77
+ couponCode: null,
78
+ paymentIntentId: 'pi_adm1',
79
+ shippingAddress: null,
80
+ }).pay();
81
+ await orderRepository.create(order1);
82
+
83
+ // Order 2: SHIPPED, customer2, total=200, prod-2 qty=1
84
+ const order2 = OrderEntity.create({
85
+ userId: customer2Id,
86
+ items: [
87
+ {
88
+ id: 'i2',
89
+ productId: 'prod-2',
90
+ variantId: 'v2',
91
+ name: 'Calça',
92
+ sku: 'CAL-1',
93
+ price: 200,
94
+ qty: 1,
95
+ image: null,
96
+ },
97
+ ],
98
+ subtotal: 200,
99
+ discount: 0,
100
+ shippingCost: 0,
101
+ tax: 0,
102
+ total: 200,
103
+ couponCode: null,
104
+ paymentIntentId: 'pi_adm2',
105
+ shippingAddress: null,
106
+ })
107
+ .pay()
108
+ .ship();
109
+ await orderRepository.create(order2);
110
+
111
+ // Order 3: PENDING (should NOT count for revenue)
112
+ const order3 = OrderEntity.create({
113
+ userId: customer1Id,
114
+ items: [
115
+ {
116
+ id: 'i3',
117
+ productId: 'prod-1',
118
+ variantId: 'v1',
119
+ name: 'Camiseta',
120
+ sku: 'CAM-1',
121
+ price: 50,
122
+ qty: 1,
123
+ image: null,
124
+ },
125
+ ],
126
+ subtotal: 50,
127
+ discount: 0,
128
+ shippingCost: 0,
129
+ tax: 0,
130
+ total: 50,
131
+ couponCode: null,
132
+ paymentIntentId: null,
133
+ shippingAddress: null,
134
+ });
135
+ await orderRepository.create(order3);
136
+ });
137
+
138
+ // ── GET /api/admin/dashboard/stats ────────────────────────────────────────
139
+ describe('GET /api/admin/dashboard/stats', () => {
140
+ it('403: deve bloquear não-admins', async () => {
141
+ const res = await request(app)
142
+ .get('/api/admin/dashboard/stats')
143
+ .set('Authorization', `Bearer ${customerToken}`);
144
+ expect(res.status).toBe(403);
145
+ });
146
+
147
+ it('deve retornar totalRevenue, totalOrders, totalCustomers, avgOrderValue', async () => {
148
+ const res = await request(app)
149
+ .get('/api/admin/dashboard/stats')
150
+ .set('Authorization', `Bearer ${adminToken}`);
151
+ expect(res.status).toBe(200);
152
+ expect(res.body).toMatchObject({
153
+ totalRevenue: 300, // PAID(100) + SHIPPED(200)
154
+ totalOrders: 3, // all 3 orders
155
+ totalCustomers: 2, // customer1 + customer2 have revenue orders
156
+ avgOrderValue: 150, // 300 / 2 revenue orders
157
+ });
158
+ });
159
+
160
+ it('deve aceitar filtro period=today', async () => {
161
+ const res = await request(app)
162
+ .get('/api/admin/dashboard/stats?period=today')
163
+ .set('Authorization', `Bearer ${adminToken}`);
164
+ expect(res.status).toBe(200);
165
+ // All orders were created during this test run (today)
166
+ expect(res.body.totalOrders).toBe(3);
167
+ });
168
+
169
+ it('deve aceitar filtro period=month', async () => {
170
+ const res = await request(app)
171
+ .get('/api/admin/dashboard/stats?period=month')
172
+ .set('Authorization', `Bearer ${adminToken}`);
173
+ expect(res.status).toBe(200);
174
+ expect(res.body.totalRevenue).toBe(300);
175
+ });
176
+ });
177
+
178
+ // ── GET /api/admin/dashboard/top-products ─────────────────────────────────
179
+ describe('GET /api/admin/dashboard/top-products', () => {
180
+ it('deve retornar produtos mais vendidos ordenados por qty desc', async () => {
181
+ const res = await request(app)
182
+ .get('/api/admin/dashboard/top-products')
183
+ .set('Authorization', `Bearer ${adminToken}`);
184
+ expect(res.status).toBe(200);
185
+ expect(Array.isArray(res.body.items)).toBe(true);
186
+ // prod-1: qty=3 (order1 PAID); prod-2: qty=1 (order2 SHIPPED)
187
+ // order3 is PENDING — must NOT be counted
188
+ expect(res.body.items[0]).toMatchObject({ productId: 'prod-1', totalQty: 3 });
189
+ expect(res.body.items[1]).toMatchObject({ productId: 'prod-2', totalQty: 1 });
190
+ });
191
+ });
192
+
193
+ // ── GET /api/admin/users ──────────────────────────────────────────────────
194
+ describe('GET /api/admin/users', () => {
195
+ it('403: deve bloquear não-admins', async () => {
196
+ const res = await request(app)
197
+ .get('/api/admin/users')
198
+ .set('Authorization', `Bearer ${customerToken}`);
199
+ expect(res.status).toBe(403);
200
+ });
201
+
202
+ it('200: deve listar usuários com paginação', async () => {
203
+ const res = await request(app)
204
+ .get('/api/admin/users')
205
+ .set('Authorization', `Bearer ${adminToken}`);
206
+ expect(res.status).toBe(200);
207
+ expect(Array.isArray(res.body.items)).toBe(true);
208
+ expect(res.body.items.length).toBeGreaterThanOrEqual(3); // admin + 2 customers
209
+ expect(res.body).toHaveProperty('nextCursor');
210
+ });
211
+
212
+ it('deve filtrar por role=CUSTOMER', async () => {
213
+ const res = await request(app)
214
+ .get('/api/admin/users?role=CUSTOMER')
215
+ .set('Authorization', `Bearer ${adminToken}`);
216
+ expect(res.status).toBe(200);
217
+ expect(
218
+ res.body.items.every((u: { role: string }) => u.role === 'CUSTOMER'),
219
+ ).toBe(true);
220
+ expect(res.body.items.length).toBeGreaterThanOrEqual(2);
221
+ });
222
+ });
223
+
224
+ // ── PATCH /api/admin/users/:id ────────────────────────────────────────────
225
+ describe('PATCH /api/admin/users/:id', () => {
226
+ it('200: deve atualizar role de CUSTOMER para ADMIN', async () => {
227
+ const res = await request(app)
228
+ .patch(`/api/admin/users/${customer1Id}`)
229
+ .set('Authorization', `Bearer ${adminToken}`)
230
+ .send({ role: 'ADMIN' });
231
+ expect(res.status).toBe(200);
232
+ expect(res.body).toMatchObject({ message: expect.any(String) });
233
+
234
+ // Verify the change persisted
235
+ const listRes = await request(app)
236
+ .get('/api/admin/users?role=ADMIN')
237
+ .set('Authorization', `Bearer ${adminToken}`);
238
+ const ids = listRes.body.items.map((u: { id: string }) => u.id);
239
+ expect(ids).toContain(customer1Id);
240
+ });
241
+
242
+ it('403: deve bloquear admin de alterar o próprio role', async () => {
243
+ const res = await request(app)
244
+ .patch(`/api/admin/users/${adminId}`)
245
+ .set('Authorization', `Bearer ${adminToken}`)
246
+ .send({ role: 'CUSTOMER' });
247
+ expect(res.status).toBe(403);
248
+ });
249
+ });
250
+ });
@@ -0,0 +1,116 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { z } from 'zod';
3
+ import type { DashboardService } from './admin.service';
4
+ import type { AdminUserService } from './admin.user.service';
5
+ import { createCouponSchema, CouponAdminService } from '../cart/coupon.service';
6
+
7
+ // ── Zod schemas ───────────────────────────────────────────────────────────────
8
+ const periodSchema = z.object({
9
+ period: z.enum(['today', 'week', 'month', 'custom']).optional(),
10
+ from: z.string().optional(),
11
+ to: z.string().optional(),
12
+ limit: z.coerce.number().int().min(1).max(100).optional(),
13
+ });
14
+
15
+ const userListSchema = z.object({
16
+ role: z.enum(['ADMIN', 'CUSTOMER']).optional(),
17
+ cursor: z.string().optional(),
18
+ limit: z.coerce.number().int().min(1).max(100).optional(),
19
+ });
20
+
21
+ const updateRoleSchema = z.object({
22
+ role: z.enum(['ADMIN', 'CUSTOMER']),
23
+ });
24
+
25
+ // ── AdminController ───────────────────────────────────────────────────────────
26
+ export class AdminController {
27
+ constructor(
28
+ private readonly dashboardService: DashboardService,
29
+ private readonly adminUserService: AdminUserService,
30
+ private readonly couponAdminService: CouponAdminService,
31
+ ) {}
32
+
33
+ getStats = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
34
+ try {
35
+ const query = periodSchema.parse(req.query);
36
+ const stats = await this.dashboardService.getStats(query);
37
+ res.json(stats);
38
+ } catch (err) {
39
+ next(err);
40
+ }
41
+ };
42
+
43
+ getTopProducts = async (
44
+ req: Request,
45
+ res: Response,
46
+ next: NextFunction,
47
+ ): Promise<void> => {
48
+ try {
49
+ const query = periodSchema.parse(req.query);
50
+ const products = await this.dashboardService.getTopProducts(query);
51
+ res.json({ items: products });
52
+ } catch (err) {
53
+ next(err);
54
+ }
55
+ };
56
+
57
+ listUsers = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
58
+ try {
59
+ const query = userListSchema.parse(req.query);
60
+ const result = await this.adminUserService.listUsers(query);
61
+ res.json({
62
+ items: result.items.map((u) => u.toPublic()),
63
+ nextCursor: result.nextCursor,
64
+ });
65
+ } catch (err) {
66
+ next(err);
67
+ }
68
+ };
69
+
70
+ updateUserRole = async (
71
+ req: Request,
72
+ res: Response,
73
+ next: NextFunction,
74
+ ): Promise<void> => {
75
+ try {
76
+ const { id } = req.params;
77
+ const { role } = updateRoleSchema.parse(req.body);
78
+ const requesterId = (req.user as { id: string }).id;
79
+ await this.adminUserService.updateUserRole(id, role, requesterId);
80
+ res.json({ message: 'Role atualizado com sucesso' });
81
+ } catch (err) {
82
+ next(err);
83
+ }
84
+ };
85
+
86
+ // ── Coupon handlers ───────────────────────────────────────────────────────
87
+
88
+ createCoupon = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
89
+ try {
90
+ const dto = createCouponSchema.parse(req.body);
91
+ const coupon = await this.couponAdminService.create(dto);
92
+ res.status(201).json(coupon.toRecord());
93
+ } catch (err) {
94
+ next(err);
95
+ }
96
+ };
97
+
98
+ listCoupons = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
99
+ try {
100
+ const coupons = await this.couponAdminService.list();
101
+ res.json({ items: coupons.map((c) => c.toRecord()) });
102
+ } catch (err) {
103
+ next(err);
104
+ }
105
+ };
106
+
107
+ deleteCoupon = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
108
+ try {
109
+ const { id } = req.params;
110
+ await this.couponAdminService.delete(id);
111
+ res.status(204).send();
112
+ } catch (err) {
113
+ next(err);
114
+ }
115
+ };
116
+ }
@@ -0,0 +1 @@
1
+ export * from '../../infrastructure/config/registry/admin.registry';
@@ -0,0 +1,21 @@
1
+ import { Router } from 'express';
2
+ import { authenticate } from '../../shared/middlewares/authenticate';
3
+ import { authorize } from '../../shared/middlewares/authorize';
4
+ import { validateUuidParam } from '../../shared/validators/uuidParam';
5
+ import { adminController } from './admin.registry';
6
+
7
+ const adminRoutes = Router();
8
+
9
+ adminRoutes.use(authenticate, authorize('ADMIN'));
10
+
11
+ adminRoutes.get('/dashboard/stats', adminController.getStats);
12
+ adminRoutes.get('/dashboard/top-products', adminController.getTopProducts);
13
+ adminRoutes.get('/users', adminController.listUsers);
14
+ adminRoutes.patch('/users/:id', validateUuidParam(), adminController.updateUserRole);
15
+
16
+ // Coupon management (Phase 13)
17
+ adminRoutes.post('/coupons', adminController.createCoupon);
18
+ adminRoutes.get('/coupons', adminController.listCoupons);
19
+ adminRoutes.delete('/coupons/:id', validateUuidParam(), adminController.deleteCoupon);
20
+
21
+ export { adminRoutes };
@@ -0,0 +1 @@
1
+ export * from '../../application/admin/dashboard.service';
@@ -0,0 +1 @@
1
+ export * from '../../application/admin/admin-user.service';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Redis-backed logout integration tests.
3
+ * Verifies that JTIs are stored in Redis upon logout and that blacklisted
4
+ * tokens are rejected by the /api/auth/refresh endpoint.
5
+ *
6
+ * Requires PostgreSQL + Redis running (docker-compose up).
7
+ * Run with: npm run test:db
8
+ */
9
+ import request from 'supertest';
10
+ import Redis from 'ioredis';
11
+ import app from '../../../app';
12
+
13
+ const REDIS_URL = process.env['REDIS_URL'] ?? 'redis://localhost:6379';
14
+
15
+ const USER = {
16
+ name: 'Redis Logout User',
17
+ email: 'redis.logout@email.com',
18
+ password: 'senha1234',
19
+ };
20
+
21
+ describe('Auth Logout — Redis Integration', () => {
22
+ let redis: Redis;
23
+
24
+ beforeAll(async () => {
25
+ redis = new Redis(REDIS_URL, { lazyConnect: true, maxRetriesPerRequest: 3 });
26
+ await redis.connect();
27
+ // Register user once for all tests
28
+ await request(app).post('/api/auth/register').send(USER);
29
+ });
30
+
31
+ afterAll(async () => {
32
+ // Clean up blacklist keys created during tests
33
+ const keys = await redis.keys('blacklist:*');
34
+ if (keys.length > 0) await redis.del(...keys);
35
+ // Close the local client AND the shared registry Redis singleton
36
+ const { redis: sharedRedis } = await import('../../../shared/infra/redis');
37
+ await Promise.allSettled([redis.quit(), sharedRedis.quit()]);
38
+ });
39
+
40
+ it('deve adicionar jti do refreshToken ao Redis após logout', async () => {
41
+ const loginRes = await request(app)
42
+ .post('/api/auth/login')
43
+ .send({ email: USER.email, password: USER.password });
44
+
45
+ const cookies: string[] = loginRes.headers['set-cookie'] as unknown as string[];
46
+ const cookieHeader = cookies.find((c) => c.startsWith('refreshToken=')) ?? '';
47
+ const cookieValue = cookieHeader.split(';')[0] ?? '';
48
+
49
+ const logoutRes = await request(app)
50
+ .post('/api/auth/logout')
51
+ .set('Cookie', cookieValue);
52
+
53
+ expect(logoutRes.status).toBe(204);
54
+
55
+ // At least one blacklist:* key should now exist in Redis
56
+ const keys = await redis.keys('blacklist:*');
57
+ expect(keys.length).toBeGreaterThan(0);
58
+ });
59
+
60
+ it('deve rejeitar refreshToken com jti na blacklist com 401', async () => {
61
+ const loginRes = await request(app)
62
+ .post('/api/auth/login')
63
+ .send({ email: USER.email, password: USER.password });
64
+
65
+ const cookies: string[] = loginRes.headers['set-cookie'] as unknown as string[];
66
+ const cookieHeader = cookies.find((c) => c.startsWith('refreshToken=')) ?? '';
67
+ const cookieValue = cookieHeader.split(';')[0] ?? '';
68
+
69
+ // Logout — JTI enters Redis blacklist
70
+ await request(app).post('/api/auth/logout').set('Cookie', cookieValue);
71
+
72
+ // Re-use the same cookie → should be rejected
73
+ const refreshRes = await request(app)
74
+ .post('/api/auth/refresh')
75
+ .set('Cookie', cookieValue);
76
+
77
+ expect(refreshRes.status).toBe(401);
78
+ });
79
+
80
+ it('token de novo login deve ser aceito mesmo após logout do anterior', async () => {
81
+ // First login + logout
82
+ const firstLogin = await request(app)
83
+ .post('/api/auth/login')
84
+ .send({ email: USER.email, password: USER.password });
85
+ const firstCookies: string[] = firstLogin.headers['set-cookie'] as unknown as string[];
86
+ const firstCookieHeader = firstCookies.find((c) => c.startsWith('refreshToken=')) ?? '';
87
+ const firstCookieValue = firstCookieHeader.split(';')[0] ?? '';
88
+ await request(app).post('/api/auth/logout').set('Cookie', firstCookieValue);
89
+
90
+ // Second login — new JTI, should be accepted
91
+ const secondLogin = await request(app)
92
+ .post('/api/auth/login')
93
+ .send({ email: USER.email, password: USER.password });
94
+ const secondCookies: string[] = secondLogin.headers['set-cookie'] as unknown as string[];
95
+ const secondCookieHeader = secondCookies.find((c) => c.startsWith('refreshToken=')) ?? '';
96
+ const secondCookieValue = secondCookieHeader.split(';')[0] ?? '';
97
+
98
+ const refreshRes = await request(app)
99
+ .post('/api/auth/refresh')
100
+ .set('Cookie', secondCookieValue);
101
+
102
+ expect(refreshRes.status).toBe(200);
103
+ });
104
+ });