@atlashub/smartstack-cli 3.39.0 → 3.41.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 (472) hide show
  1. package/.documentation/apex.html +644 -644
  2. package/.documentation/css/styles.css +2320 -2320
  3. package/.documentation/init.html +1377 -1377
  4. package/.documentation/js/app.js +780 -780
  5. package/.documentation/prd-json-v2.0.0.md +396 -396
  6. package/.documentation/testing-ba-e2e.md +462 -462
  7. package/config/default-config.json +95 -95
  8. package/config/mcp-defaults.json +62 -62
  9. package/config/settings.json +53 -53
  10. package/config/settings.local.example.json +16 -16
  11. package/dist/index.js +6 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp-entry.mjs +6 -4
  14. package/dist/mcp-entry.mjs.map +1 -1
  15. package/package.json +115 -115
  16. package/scripts/extract-api-endpoints.ts +325 -325
  17. package/scripts/extract-business-rules.ts +440 -440
  18. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  19. package/scripts/health-check.sh +168 -168
  20. package/scripts/postinstall.js +18 -18
  21. package/templates/agents/action.md +37 -37
  22. package/templates/agents/ba-reader.md +378 -378
  23. package/templates/agents/ba-writer.md +861 -861
  24. package/templates/agents/code-reviewer.md +163 -163
  25. package/templates/agents/db-reader.md +149 -149
  26. package/templates/agents/docs-context-reader.md +143 -143
  27. package/templates/agents/docs-sync-checker.md +122 -122
  28. package/templates/agents/efcore/conflicts.md +95 -84
  29. package/templates/agents/efcore/db-deploy.md +85 -74
  30. package/templates/agents/efcore/db-reset.md +96 -85
  31. package/templates/agents/efcore/db-seed.md +72 -61
  32. package/templates/agents/efcore/db-status.md +97 -86
  33. package/templates/agents/efcore/migration.md +197 -186
  34. package/templates/agents/efcore/rebase-snapshot.md +119 -108
  35. package/templates/agents/efcore/scan.md +103 -92
  36. package/templates/agents/efcore/squash.md +172 -161
  37. package/templates/agents/explore-codebase.md +66 -66
  38. package/templates/agents/explore-docs.md +98 -98
  39. package/templates/agents/fix-grammar.md +50 -50
  40. package/templates/agents/gitflow/abort.md +45 -45
  41. package/templates/agents/gitflow/cleanup.md +96 -96
  42. package/templates/agents/gitflow/commit.md +236 -236
  43. package/templates/agents/gitflow/exec.md +48 -48
  44. package/templates/agents/gitflow/finish.md +146 -146
  45. package/templates/agents/gitflow/init-clone.md +199 -199
  46. package/templates/agents/gitflow/init-detect.md +137 -137
  47. package/templates/agents/gitflow/init-validate.md +225 -225
  48. package/templates/agents/gitflow/init.md +340 -340
  49. package/templates/agents/gitflow/merge.md +145 -145
  50. package/templates/agents/gitflow/plan.md +42 -42
  51. package/templates/agents/gitflow/pr.md +191 -191
  52. package/templates/agents/gitflow/review.md +49 -49
  53. package/templates/agents/gitflow/start.md +147 -147
  54. package/templates/agents/gitflow/status.md +95 -95
  55. package/templates/agents/mcp-healthcheck.md +163 -163
  56. package/templates/agents/snipper.md +37 -37
  57. package/templates/agents/websearch.md +46 -46
  58. package/templates/hooks/appsettings-guard.sh +76 -76
  59. package/templates/hooks/docs-drift-check.md +96 -96
  60. package/templates/hooks/ef-migration-check.md +139 -139
  61. package/templates/hooks/hooks.json +58 -58
  62. package/templates/hooks/mcp-check.md +64 -64
  63. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  64. package/templates/hooks/ralph-session-end.sh +69 -69
  65. package/templates/hooks/stop-hook.sh +177 -177
  66. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  67. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  68. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  69. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  70. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  71. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  72. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  73. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  74. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  75. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  76. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  77. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  78. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  79. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  80. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  81. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  82. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  83. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  84. package/templates/project/ExampleEntity.cs.template +116 -116
  85. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  86. package/templates/project/ExampleService.cs.template +146 -146
  87. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  88. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  89. package/templates/project/Program.cs.template +47 -47
  90. package/templates/project/README.md +79 -79
  91. package/templates/project/api.ts.template +12 -12
  92. package/templates/project/appsettings.json.template +170 -170
  93. package/templates/project/claude-settings.json.template +5 -5
  94. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  95. package/templates/project/test-frontend/msw/server.ts +25 -25
  96. package/templates/project/test-frontend/setup.ts +16 -16
  97. package/templates/project/test-frontend/test-utils.tsx +59 -59
  98. package/templates/project/test-frontend/vitest.config.ts +31 -31
  99. package/templates/ralph/README.md +93 -93
  100. package/templates/ralph/ralph.config.yaml +113 -113
  101. package/templates/scripts/setup-ralph-loop.sh +173 -173
  102. package/templates/skills/_resources/config-safety.md +61 -61
  103. package/templates/skills/_resources/context-digest-template.md +53 -53
  104. package/templates/skills/_resources/doc-context-cache.md +60 -60
  105. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  106. package/templates/skills/_resources/formatting-guide.md +124 -124
  107. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  108. package/templates/skills/_shared.md +228 -228
  109. package/templates/skills/admin/SKILL.md +48 -48
  110. package/templates/skills/ai-prompt/SKILL.md +107 -107
  111. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  112. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  113. package/templates/skills/apex/SKILL.md +168 -168
  114. package/templates/skills/apex/_shared.md +141 -141
  115. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  116. package/templates/skills/apex/references/analysis-methods.md +141 -141
  117. package/templates/skills/apex/references/challenge-questions.md +145 -145
  118. package/templates/skills/apex/references/code-generation.md +412 -412
  119. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  120. package/templates/skills/apex/references/error-classification.md +144 -144
  121. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  122. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  123. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  124. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  125. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  126. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  127. package/templates/skills/apex/references/post-checks.md +1584 -1584
  128. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  129. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  130. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  131. package/templates/skills/apex/steps/step-00-init.md +307 -307
  132. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  133. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  134. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  135. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  136. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  137. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  138. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  139. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  140. package/templates/skills/application/SKILL.md +4 -4
  141. package/templates/skills/application/references/application-roles-template.md +227 -227
  142. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  143. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  144. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  145. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  146. package/templates/skills/application/references/backend-verification.md +88 -88
  147. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  148. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  149. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  150. package/templates/skills/application/references/frontend-verification.md +156 -156
  151. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  152. package/templates/skills/application/references/provider-template.md +177 -177
  153. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  154. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  155. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  156. package/templates/skills/application/references/test-frontend.md +73 -73
  157. package/templates/skills/application/references/test-prerequisites.md +72 -72
  158. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  159. package/templates/skills/application/steps/step-06-migration.md +193 -193
  160. package/templates/skills/application/steps/step-07-tests.md +356 -356
  161. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  162. package/templates/skills/application/templates-backend.md +463 -463
  163. package/templates/skills/application/templates-frontend.md +685 -685
  164. package/templates/skills/application/templates-i18n.md +520 -520
  165. package/templates/skills/application/templates-seed.md +1096 -1096
  166. package/templates/skills/business-analyse/SKILL.md +327 -327
  167. package/templates/skills/business-analyse/_architecture.md +123 -123
  168. package/templates/skills/business-analyse/_elicitation.md +206 -206
  169. package/templates/skills/business-analyse/_module-loop.md +115 -115
  170. package/templates/skills/business-analyse/_shared.md +383 -383
  171. package/templates/skills/business-analyse/_suggestions.md +34 -34
  172. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  173. package/templates/skills/business-analyse/html/build-html.js +77 -77
  174. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  175. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  176. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  177. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  178. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  179. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  180. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  181. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  182. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  183. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  184. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  185. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  186. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  187. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  188. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  189. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  190. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  191. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  192. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  193. package/templates/skills/business-analyse/html/src/template.html +516 -516
  194. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  195. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  196. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  197. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  198. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  199. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  200. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  201. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  202. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  203. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  204. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  205. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  206. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  207. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  208. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  209. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  210. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  211. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  212. package/templates/skills/business-analyse/questionnaire.md +337 -337
  213. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  214. package/templates/skills/business-analyse/react/components.md +551 -551
  215. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  216. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  217. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  218. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  219. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  220. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  221. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  222. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  223. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  224. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  225. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  226. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  227. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  228. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  229. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  230. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  231. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  232. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  233. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  234. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  235. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  236. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  237. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  238. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  239. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  240. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  241. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  242. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  243. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  244. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  245. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  246. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  247. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  248. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  249. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  250. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  251. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  252. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  253. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  254. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  255. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  256. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  257. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  258. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  259. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  260. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  261. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  262. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  263. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  264. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  265. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  266. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  267. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  268. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  269. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  270. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  271. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  272. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  273. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  274. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  275. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  276. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  277. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  278. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  279. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  280. package/templates/skills/business-analyse/templates-frd.md +476 -476
  281. package/templates/skills/business-analyse/templates-react.md +574 -574
  282. package/templates/skills/cc-agent/SKILL.md +129 -129
  283. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  284. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  285. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  286. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  287. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  288. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  289. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  290. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  291. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  292. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  293. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  294. package/templates/skills/cc-audit/SKILL.md +108 -108
  295. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  296. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  297. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  298. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  299. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  300. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  301. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  302. package/templates/skills/cc-skill/SKILL.md +134 -134
  303. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  304. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  305. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  306. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  307. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  308. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  309. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  310. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  311. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  312. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  313. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  314. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  315. package/templates/skills/check-version/SKILL.md +196 -196
  316. package/templates/skills/controller/SKILL.md +162 -162
  317. package/templates/skills/controller/postman-templates.md +614 -614
  318. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  319. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  320. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  321. package/templates/skills/controller/steps/step-00-init.md +193 -191
  322. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  323. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  324. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  325. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  326. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  327. package/templates/skills/controller/templates.md +1555 -1555
  328. package/templates/skills/debug/SKILL.md +70 -70
  329. package/templates/skills/debug/references/team-protocol.md +232 -232
  330. package/templates/skills/debug/steps/step-00-init.md +57 -57
  331. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  332. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  333. package/templates/skills/documentation/SKILL.md +132 -132
  334. package/templates/skills/documentation/data-schema.md +227 -227
  335. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  336. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  337. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  338. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  339. package/templates/skills/documentation/templates.md +662 -663
  340. package/templates/skills/efcore/SKILL.md +168 -167
  341. package/templates/skills/efcore/references/both-contexts.md +32 -32
  342. package/templates/skills/efcore/references/database-operations.md +67 -67
  343. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  344. package/templates/skills/efcore/references/reset-operations.md +81 -81
  345. package/templates/skills/efcore/references/seed-methods.md +86 -86
  346. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  347. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  348. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  349. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  350. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  351. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  352. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  353. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  354. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  355. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  356. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  357. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  362. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  363. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  364. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  365. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  366. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  367. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  368. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  369. package/templates/skills/explore/SKILL.md +98 -98
  370. package/templates/skills/feature-full/SKILL.md +111 -111
  371. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  372. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  373. package/templates/skills/gitflow/SKILL.md +377 -377
  374. package/templates/skills/gitflow/_shared.md +620 -620
  375. package/templates/skills/gitflow/phases/abort.md +189 -189
  376. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  377. package/templates/skills/gitflow/phases/status.md +192 -192
  378. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  379. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  380. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  381. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  382. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  383. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  384. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  385. package/templates/skills/gitflow/references/init-questions.md +185 -185
  386. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  387. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  388. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  389. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  390. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  391. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  392. package/templates/skills/gitflow/references/plan-template.md +69 -69
  393. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  394. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  395. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  396. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  397. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  398. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  399. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  400. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  401. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  402. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  403. package/templates/skills/gitflow/steps/step-init.md +190 -190
  404. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  405. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  406. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  407. package/templates/skills/gitflow/steps/step-start.md +195 -195
  408. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  409. package/templates/skills/gitflow/templates/config.json +72 -72
  410. package/templates/skills/mcp/SKILL.md +62 -62
  411. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  412. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  413. package/templates/skills/notification/SKILL.md +173 -173
  414. package/templates/skills/quick-search/SKILL.md +99 -99
  415. package/templates/skills/ralph-loop/SKILL.md +234 -234
  416. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  417. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  418. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  419. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  420. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  421. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  422. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  423. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  424. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  425. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  426. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  427. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  428. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  429. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  430. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  431. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  432. package/templates/skills/refactor/SKILL.md +56 -56
  433. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  434. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  435. package/templates/skills/review-code/SKILL.md +95 -94
  436. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  437. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  438. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  439. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  440. package/templates/skills/review-code/references/security-checklist.md +212 -212
  441. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  442. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  443. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  444. package/templates/skills/ui-components/SKILL.md +137 -137
  445. package/templates/skills/ui-components/accessibility.md +170 -170
  446. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  447. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  448. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  449. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  450. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  451. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  452. package/templates/skills/ui-components/style-guide.md +113 -113
  453. package/templates/skills/utils/SKILL.md +44 -44
  454. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  455. package/templates/skills/utils/subcommands/test-web.md +123 -123
  456. package/templates/skills/validate/SKILL.md +181 -181
  457. package/templates/skills/validate-feature/SKILL.md +101 -101
  458. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  459. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  460. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  461. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  462. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  463. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  464. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  465. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  466. package/templates/skills/workflow/SKILL.md +127 -127
  467. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  468. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  469. package/templates/test-web/api-health.json +38 -38
  470. package/templates/test-web/minimal.json +19 -19
  471. package/templates/test-web/npm-package.json +46 -46
  472. package/templates/test-web/seo-check.json +54 -54
@@ -1,1555 +1,1555 @@
1
- # SmartStack Controller Templates
2
-
3
- > **⚠️ OBSOLETE - DO NOT USE THESE TEMPLATES MANUALLY**
4
- >
5
- > **The `/controller` skill now uses the MCP `scaffold_extension` tool to generate controllers.**
6
- > These templates are kept for reference only. All controller generation MUST go through the MCP to ensure:
7
- > - ✅ `[NavRoute]` attribute is included for frontend/backend sync
8
- > - ✅ Permissions are correctly generated
9
- > - ✅ Consistency with SmartStack conventions
10
- >
11
- > **To generate a controller, use:** `/controller` skill which calls MCP `scaffold_extension` automatically.
12
- >
13
- > **If you modify these templates, they will NOT be used.** The MCP templates in `templates/mcp-scaffolding/controller.cs.hbs` are the source of truth.
14
-
15
- ---
16
-
17
- ## Template CRUD Controller (Standard)
18
-
19
- ```csharp
20
- // src/SmartStack.Api/Controllers/{Area}/{Module}Controller.cs
21
-
22
- using Microsoft.AspNetCore.Authorization;
23
- using Microsoft.AspNetCore.Mvc;
24
- using Microsoft.EntityFrameworkCore;
25
- using SmartStack.Application.Common.Authorization;
26
- using SmartStack.Application.Common.Interfaces;
27
- using SmartStack.Api.Authorization;
28
- using SmartStack.Domain.{DomainNamespace};
29
-
30
- namespace SmartStack.Api.Controllers.{Area};
31
-
32
- [ApiController]
33
- [Route("api/{area-kebab}/{module-kebab}")]
34
- [Authorize]
35
- [Tags("{Module}")]
36
- public class {Module}Controller : ControllerBase
37
- {
38
- private readonly IApplicationDbContext _context;
39
- private readonly ICurrentUserService _currentUser;
40
- private readonly ILogger<{Module}Controller> _logger;
41
-
42
- public {Module}Controller(
43
- IApplicationDbContext context,
44
- ICurrentUserService currentUser,
45
- ILogger<{Module}Controller> logger)
46
- {
47
- _context = context;
48
- _currentUser = currentUser;
49
- _logger = logger;
50
- }
51
-
52
- #region GET - List with Pagination
53
-
54
- [HttpGet]
55
- [RequirePermission(Permissions.{PermissionClass}.View)]
56
- [ProducesResponseType(typeof(PaginatedResult<{Entity}ListDto>), StatusCodes.Status200OK)]
57
- public async Task<ActionResult<PaginatedResult<{Entity}ListDto>>> Get{Module}(
58
- [FromQuery] int page = 1,
59
- [FromQuery] int pageSize = 20,
60
- [FromQuery] string? search = null,
61
- CancellationToken cancellationToken = default)
62
- {
63
- var query = _context.{DbSet}.AsQueryable();
64
-
65
- // Search filter
66
- if (!string.IsNullOrWhiteSpace(search))
67
- {
68
- var searchLower = search.ToLower();
69
- query = query.Where(x =>
70
- x.Name.ToLower().Contains(searchLower) ||
71
- x.Description != null && x.Description.ToLower().Contains(searchLower));
72
- }
73
-
74
- var totalCount = await query.CountAsync(cancellationToken);
75
-
76
- var items = await query
77
- .OrderBy(x => x.Name)
78
- .Skip((page - 1) * pageSize)
79
- .Take(pageSize)
80
- .Select(x => new {Entity}ListDto(
81
- x.Id,
82
- x.Name,
83
- x.Description,
84
- x.IsActive,
85
- x.CreatedAt
86
- ))
87
- .ToListAsync(cancellationToken);
88
-
89
- _logger.LogInformation("User {User} retrieved {Count} {Module}",
90
- _currentUser.Email, items.Count, "{Module}");
91
-
92
- return Ok(new PaginatedResult<{Entity}ListDto>(items, totalCount, page, pageSize));
93
- }
94
-
95
- #endregion
96
-
97
- #region GET - Single by ID
98
-
99
- [HttpGet("{id:guid}")]
100
- [RequirePermission(Permissions.{PermissionClass}.View)]
101
- [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
102
- [ProducesResponseType(StatusCodes.Status404NotFound)]
103
- public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
104
- Guid id,
105
- CancellationToken cancellationToken)
106
- {
107
- var entity = await _context.{DbSet}
108
- .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
109
-
110
- if (entity == null)
111
- return NotFound(new { message = "{Entity} not found" });
112
-
113
- return Ok(new {Entity}DetailDto(
114
- entity.Id,
115
- entity.Name,
116
- entity.Description,
117
- entity.IsActive,
118
- entity.CreatedAt,
119
- entity.UpdatedAt
120
- ));
121
- }
122
-
123
- #endregion
124
-
125
- #region POST - Create
126
-
127
- [HttpPost]
128
- [RequirePermission(Permissions.{PermissionClass}.Create)]
129
- [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status201Created)]
130
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
131
- [ProducesResponseType(StatusCodes.Status409Conflict)]
132
- public async Task<ActionResult<{Entity}DetailDto>> Create{Entity}(
133
- [FromBody] Create{Entity}Request request,
134
- CancellationToken cancellationToken)
135
- {
136
- // Check for duplicates
137
- var exists = await _context.{DbSet}
138
- .AnyAsync(x => x.Name == request.Name, cancellationToken);
139
-
140
- if (exists)
141
- return Conflict(new { message = "{Entity} with this name already exists" });
142
-
143
- var entity = {Entity}.Create(
144
- request.Name,
145
- request.Description
146
- );
147
-
148
- _context.{DbSet}.Add(entity);
149
- await _context.SaveChangesAsync(cancellationToken);
150
-
151
- _logger.LogInformation("User {User} created {Entity} {EntityId} ({Name})",
152
- _currentUser.Email, entity.Id, entity.Name);
153
-
154
- return CreatedAtAction(
155
- nameof(Get{Entity}),
156
- new { id = entity.Id },
157
- new {Entity}DetailDto(
158
- entity.Id,
159
- entity.Name,
160
- entity.Description,
161
- entity.IsActive,
162
- entity.CreatedAt,
163
- entity.UpdatedAt
164
- ));
165
- }
166
-
167
- #endregion
168
-
169
- #region PUT - Update
170
-
171
- [HttpPut("{id:guid}")]
172
- [RequirePermission(Permissions.{PermissionClass}.Update)]
173
- [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
174
- [ProducesResponseType(StatusCodes.Status404NotFound)]
175
- [ProducesResponseType(StatusCodes.Status409Conflict)]
176
- public async Task<ActionResult<{Entity}DetailDto>> Update{Entity}(
177
- Guid id,
178
- [FromBody] Update{Entity}Request request,
179
- CancellationToken cancellationToken)
180
- {
181
- var entity = await _context.{DbSet}
182
- .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
183
-
184
- if (entity == null)
185
- return NotFound(new { message = "{Entity} not found" });
186
-
187
- // Check for duplicate name (excluding current)
188
- if (!string.IsNullOrEmpty(request.Name))
189
- {
190
- var duplicate = await _context.{DbSet}
191
- .AnyAsync(x => x.Name == request.Name && x.Id != id, cancellationToken);
192
-
193
- if (duplicate)
194
- return Conflict(new { message = "{Entity} with this name already exists" });
195
- }
196
-
197
- entity.Update(
198
- request.Name ?? entity.Name,
199
- request.Description ?? entity.Description
200
- );
201
-
202
- await _context.SaveChangesAsync(cancellationToken);
203
-
204
- _logger.LogInformation("User {User} updated {Entity} {EntityId}",
205
- _currentUser.Email, entity.Id);
206
-
207
- return Ok(new {Entity}DetailDto(
208
- entity.Id,
209
- entity.Name,
210
- entity.Description,
211
- entity.IsActive,
212
- entity.CreatedAt,
213
- entity.UpdatedAt
214
- ));
215
- }
216
-
217
- #endregion
218
-
219
- #region PATCH - Activate/Deactivate
220
-
221
- [HttpPatch("{id:guid}/activate")]
222
- [RequirePermission(Permissions.{PermissionClass}.Update)]
223
- [ProducesResponseType(StatusCodes.Status204NoContent)]
224
- [ProducesResponseType(StatusCodes.Status404NotFound)]
225
- public async Task<IActionResult> Activate{Entity}(
226
- Guid id,
227
- CancellationToken cancellationToken)
228
- {
229
- var entity = await _context.{DbSet}
230
- .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
231
-
232
- if (entity == null)
233
- return NotFound(new { message = "{Entity} not found" });
234
-
235
- entity.Activate();
236
- await _context.SaveChangesAsync(cancellationToken);
237
-
238
- _logger.LogInformation("User {User} activated {Entity} {EntityId}",
239
- _currentUser.Email, entity.Id);
240
-
241
- return NoContent();
242
- }
243
-
244
- [HttpPatch("{id:guid}/deactivate")]
245
- [RequirePermission(Permissions.{PermissionClass}.Update)]
246
- [ProducesResponseType(StatusCodes.Status204NoContent)]
247
- [ProducesResponseType(StatusCodes.Status404NotFound)]
248
- public async Task<IActionResult> Deactivate{Entity}(
249
- Guid id,
250
- CancellationToken cancellationToken)
251
- {
252
- var entity = await _context.{DbSet}
253
- .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
254
-
255
- if (entity == null)
256
- return NotFound(new { message = "{Entity} not found" });
257
-
258
- entity.Deactivate();
259
- await _context.SaveChangesAsync(cancellationToken);
260
-
261
- _logger.LogWarning("User {User} deactivated {Entity} {EntityId}",
262
- _currentUser.Email, entity.Id);
263
-
264
- return NoContent();
265
- }
266
-
267
- #endregion
268
-
269
- #region DELETE
270
-
271
- [HttpDelete("{id:guid}")]
272
- [RequirePermission(Permissions.{PermissionClass}.Delete)]
273
- [ProducesResponseType(StatusCodes.Status204NoContent)]
274
- [ProducesResponseType(StatusCodes.Status404NotFound)]
275
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
276
- public async Task<IActionResult> Delete{Entity}(
277
- Guid id,
278
- CancellationToken cancellationToken)
279
- {
280
- var entity = await _context.{DbSet}
281
- .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
282
-
283
- if (entity == null)
284
- return NotFound(new { message = "{Entity} not found" });
285
-
286
- // Check for dependencies before deletion
287
- // var hasReferences = await _context.ChildEntities.AnyAsync(x => x.{Entity}Id == id, ct);
288
- // if (hasReferences)
289
- // return BadRequest(new { message = "Cannot delete: has dependent records" });
290
-
291
- _context.{DbSet}.Remove(entity);
292
- await _context.SaveChangesAsync(cancellationToken);
293
-
294
- _logger.LogWarning("User {User} deleted {Entity} {EntityId} ({Name})",
295
- _currentUser.Email, id, entity.Name);
296
-
297
- return NoContent();
298
- }
299
-
300
- #endregion
301
- }
302
-
303
- #region DTOs
304
-
305
- public record {Entity}ListDto(
306
- Guid Id,
307
- string Name,
308
- string? Description,
309
- bool IsActive,
310
- DateTime CreatedAt
311
- );
312
-
313
- public record {Entity}DetailDto(
314
- Guid Id,
315
- string Name,
316
- string? Description,
317
- bool IsActive,
318
- DateTime CreatedAt,
319
- DateTime? UpdatedAt
320
- );
321
-
322
- public record Create{Entity}Request(
323
- string Name,
324
- string? Description
325
- );
326
-
327
- public record Update{Entity}Request(
328
- string? Name,
329
- string? Description
330
- );
331
-
332
- public record PaginatedResult<T>(
333
- List<T> Items,
334
- int TotalCount,
335
- int Page,
336
- int PageSize
337
- )
338
- {
339
- public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
340
- public bool HasPreviousPage => Page > 1;
341
- public bool HasNextPage => Page < TotalPages;
342
- }
343
-
344
- #endregion
345
- ```
346
-
347
- ---
348
-
349
- ## Template Auth Controller (Login/Logout)
350
-
351
- ```csharp
352
- // src/SmartStack.Api/Controllers/AuthController.cs
353
- // NOTE: This controller already exists - use as reference for auth patterns
354
-
355
- using Microsoft.AspNetCore.Authorization;
356
- using Microsoft.AspNetCore.Mvc;
357
- using Microsoft.EntityFrameworkCore;
358
- using Microsoft.Extensions.Options;
359
- using SmartStack.Application.Common.Interfaces;
360
- using SmartStack.Application.Common.Settings;
361
- using SmartStack.Domain.Platform.Administration.Users;
362
-
363
- namespace SmartStack.Api.Controllers;
364
-
365
- [ApiController]
366
- [Route("api/[controller]")]
367
- public class AuthController : ControllerBase
368
- {
369
- private readonly IApplicationDbContext _context;
370
- private readonly IPasswordService _passwordService;
371
- private readonly IJwtService _jwtService;
372
- private readonly IUserSessionService _sessionService;
373
- private readonly ISessionValidationService _sessionValidationService;
374
- private readonly SessionSettings _sessionSettings;
375
- private readonly ILogger<AuthController> _logger;
376
-
377
- // ... Constructor avec tous les services auth
378
-
379
- #region Login - LOGS CRITIQUES OBLIGATOIRES
380
-
381
- [HttpPost("login")]
382
- [AllowAnonymous]
383
- [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
384
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
385
- public async Task<ActionResult<LoginResponse>> Login(
386
- [FromBody] LoginRequest request,
387
- CancellationToken cancellationToken)
388
- {
389
- var ipAddress = GetClientIpAddress();
390
- var userAgent = Request.Headers.UserAgent.ToString();
391
-
392
- var user = await _context.Users
393
- .Include(u => u.UserRoles)
394
- .ThenInclude(ur => ur.Role)
395
- .ThenInclude(r => r!.RolePermissions)
396
- .ThenInclude(rp => rp.Permission)
397
- .FirstOrDefaultAsync(u => u.Email == request.Email, cancellationToken);
398
-
399
- // ============================================
400
- // LOGS CRITIQUES - NE JAMAIS OMETTRE
401
- // ============================================
402
-
403
- if (user == null)
404
- {
405
- // WARNING: User not found (potential enumeration)
406
- _logger.LogWarning(
407
- "Login failed: User not found - {Email} from {IpAddress}",
408
- request.Email, ipAddress);
409
- return Unauthorized(new ErrorResponse("Identifiants invalides", "INVALID_CREDENTIALS"));
410
- }
411
-
412
- if (!user.IsActive)
413
- {
414
- // WARNING: Disabled account
415
- _logger.LogWarning(
416
- "Login failed: Account disabled - {Email} from {IpAddress}",
417
- request.Email, ipAddress);
418
- return Unauthorized(new ErrorResponse("Account disabled", "ACCOUNT_DISABLED"));
419
- }
420
-
421
- if (user.IsLocked)
422
- {
423
- // CRITICAL: Locked account attempt
424
- _logger.LogCritical(
425
- "SECURITY: Login attempt on locked account - User: {Email} (ID: {UserId}) from {IpAddress}",
426
- request.Email, user.Id, ipAddress);
427
- return Unauthorized(new ErrorResponse("Account locked", "ACCOUNT_LOCKED_BY_ADMIN"));
428
- }
429
-
430
- // Check brute force protection
431
- var recentFailedAttempts = await _context.UserSessions
432
- .Where(s => s.UserId == user.Id && !s.IsSuccessful && s.LoginAt > DateTime.UtcNow.AddMinutes(-15))
433
- .CountAsync(cancellationToken);
434
-
435
- if (recentFailedAttempts >= 5)
436
- {
437
- // CRITICAL: Too many failed attempts
438
- _logger.LogCritical(
439
- "SECURITY: Account temporarily locked due to brute force - User: {Email} (ID: {UserId}) from {IpAddress}",
440
- request.Email, user.Id, ipAddress);
441
- return Unauthorized(new ErrorResponse("Account temporarily locked", "ACCOUNT_LOCKED"));
442
- }
443
-
444
- if (!_passwordService.VerifyPassword(request.Password, user.PasswordHash))
445
- {
446
- // WARNING: Invalid password with remaining attempts
447
- var remainingAttempts = 5 - recentFailedAttempts - 1;
448
- _logger.LogWarning(
449
- "Login failed: Invalid password - {Email} from {IpAddress}, remaining attempts: {Remaining}",
450
- request.Email, ipAddress, remainingAttempts);
451
-
452
- // Log failed attempt to session
453
- await _sessionService.LogFailedLoginAsync(user.Id, ipAddress, userAgent, cancellationToken);
454
-
455
- return Unauthorized(new ErrorResponse("Mot de passe incorrect", "INVALID_PASSWORD"));
456
- }
457
-
458
- // ============================================
459
- // SUCCESS - Generate tokens
460
- // ============================================
461
-
462
- var roles = user.UserRoles.Select(ur => ur.Role!.Name).ToList();
463
- var permissions = user.UserRoles
464
- .SelectMany(ur => ur.Role!.RolePermissions)
465
- .Select(rp => rp.Permission!.Path)
466
- .Distinct()
467
- .ToList();
468
-
469
- var accessToken = _jwtService.GenerateAccessToken(user, roles, permissions);
470
- var refreshToken = _jwtService.GenerateRefreshToken();
471
-
472
- await _sessionService.LogLoginAsync(user.Id, accessToken, ipAddress, userAgent, cancellationToken: cancellationToken);
473
-
474
- // INFO: Successful login
475
- _logger.LogInformation(
476
- "User logged in successfully: {Email} from {IpAddress}",
477
- user.Email, ipAddress);
478
-
479
- return Ok(new LoginResponse(accessToken, refreshToken, /* UserInfo */));
480
- }
481
-
482
- #endregion
483
-
484
- #region Logout
485
-
486
- [HttpPost("logout")]
487
- [Authorize]
488
- [ProducesResponseType(StatusCodes.Status204NoContent)]
489
- public async Task<IActionResult> Logout(CancellationToken cancellationToken)
490
- {
491
- var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
492
- await _sessionService.LogLogoutAsync(token, cancellationToken);
493
-
494
- _logger.LogInformation("User logged out: {UserId}", User.FindFirst("sub")?.Value);
495
-
496
- return NoContent();
497
- }
498
-
499
- #endregion
500
-
501
- #region Change Password - LOG WARNING
502
-
503
- [HttpPost("change-password")]
504
- [Authorize]
505
- [ProducesResponseType(typeof(ChangePasswordResponse), StatusCodes.Status200OK)]
506
- [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
507
- public async Task<ActionResult<ChangePasswordResponse>> ChangePassword(
508
- [FromBody] ChangePasswordRequest request,
509
- CancellationToken cancellationToken)
510
- {
511
- // ... validation logic
512
-
513
- user.UpdatePassword(newPasswordHash);
514
- await _context.SaveChangesAsync(cancellationToken);
515
-
516
- // Invalidate ALL sessions after password change
517
- await _sessionService.InvalidateAllUserSessionsAsync(userId, "Password changed", cancellationToken);
518
-
519
- // WARNING: Sensitive operation
520
- _logger.LogWarning(
521
- "Password changed for user {Email} - All sessions invalidated",
522
- user.Email);
523
-
524
- return Ok(new ChangePasswordResponse("Password changed", true));
525
- }
526
-
527
- #endregion
528
-
529
- private string GetClientIpAddress()
530
- {
531
- var forwardedFor = Request.Headers["X-Forwarded-For"].FirstOrDefault();
532
- if (!string.IsNullOrEmpty(forwardedFor))
533
- return forwardedFor.Split(',')[0].Trim();
534
- return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
535
- }
536
- }
537
- ```
538
-
539
- ---
540
-
541
- ## Template Permissions Constants
542
-
543
- ```csharp
544
- // src/SmartStack.Application/Common/Authorization/Permissions.cs
545
- // ADD to existing class
546
-
547
- public static class Permissions
548
- {
549
- // ... existing permissions ...
550
-
551
- public static class {PermissionClass}
552
- {
553
- public const string Access = "{permission.path}";
554
- public const string View = "{permission.path}.read";
555
- public const string Create = "{permission.path}.create";
556
- public const string Update = "{permission.path}.update";
557
- public const string Delete = "{permission.path}.delete";
558
- // Optional depending on module
559
- public const string Assign = "{permission.path}.assign";
560
- public const string Execute = "{permission.path}.execute";
561
- public const string Export = "{permission.path}.export";
562
- }
563
- }
564
- ```
565
-
566
- ---
567
-
568
- ## Template PermissionConfiguration Seed
569
-
570
- > **CRITICAL:** This template is MANDATORY. Without these entries, all API calls will return 403 Forbidden.
571
-
572
- ```csharp
573
- // src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
574
- // ADD in Configure() method, HasData section
575
-
576
- // ============================================
577
- // STEP 1: Declare ModuleId
578
- // ============================================
579
- // Check in ModuleConfiguration.cs if module already exists
580
- // Otherwise, create the module first via /application skill
581
-
582
- var {module}ModuleId = Guid.Parse("{MODULE-GUID}"); // Get from ModuleConfiguration.cs
583
-
584
- // ============================================
585
- // STEP 2: Add permissions (HasData)
586
- // ============================================
587
-
588
- // Pattern: {application}.{module}.{action}
589
- // Example: administration.users.read
590
-
591
- var seedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
592
-
593
- builder.HasData(
594
- // Wildcard permission (full module access)
595
- new
596
- {
597
- Id = Guid.Parse("{NOUVEAU-GUID-1}"),
598
- Path = "{application}.{module}.*",
599
- Level = PermissionLevel.Module,
600
- Action = (PermissionAction?)null,
601
- IsWildcard = true,
602
- ModuleId = {module}ModuleId,
603
- Description = "Full {module} management",
604
- CreatedAt = seedDate
605
- },
606
-
607
- // Read permission
608
- new
609
- {
610
- Id = Guid.Parse("{NOUVEAU-GUID-2}"),
611
- Path = "{application}.{module}.read",
612
- Level = PermissionLevel.Module,
613
- Action = PermissionAction.Read,
614
- IsWildcard = false,
615
- ModuleId = {module}ModuleId,
616
- Description = "View {module}",
617
- CreatedAt = seedDate
618
- },
619
-
620
- // Create permission
621
- new
622
- {
623
- Id = Guid.Parse("{NOUVEAU-GUID-3}"),
624
- Path = "{application}.{module}.create",
625
- Level = PermissionLevel.Module,
626
- Action = PermissionAction.Create,
627
- IsWildcard = false,
628
- ModuleId = {module}ModuleId,
629
- Description = "Create {module}",
630
- CreatedAt = seedDate
631
- },
632
-
633
- // Update permission
634
- new
635
- {
636
- Id = Guid.Parse("{NOUVEAU-GUID-4}"),
637
- Path = "{application}.{module}.update",
638
- Level = PermissionLevel.Module,
639
- Action = PermissionAction.Update,
640
- IsWildcard = false,
641
- ModuleId = {module}ModuleId,
642
- Description = "Update {module}",
643
- CreatedAt = seedDate
644
- },
645
-
646
- // Delete permission
647
- new
648
- {
649
- Id = Guid.Parse("{NOUVEAU-GUID-5}"),
650
- Path = "{application}.{module}.delete",
651
- Level = PermissionLevel.Module,
652
- Action = PermissionAction.Delete,
653
- IsWildcard = false,
654
- ModuleId = {module}ModuleId,
655
- Description = "Delete {module}",
656
- CreatedAt = seedDate
657
- }
658
-
659
- // Optional actions depending on module:
660
- // - PermissionAction.Assign → To assign resources/roles
661
- // - PermissionAction.Execute → To execute actions (export, etc.)
662
- );
663
- ```
664
-
665
- ### GUID Generation
666
-
667
- ```bash
668
- # PowerShell (Windows)
669
- [guid]::NewGuid().ToString()
670
-
671
- # Bash (Linux/Mac)
672
- uuidgen | tr '[:upper:]' '[:lower:]'
673
- ```
674
-
675
- ### Validation Consistency Permissions.cs ↔ PermissionConfiguration.cs
676
-
677
- > **RULE:** Each constant in `Permissions.cs` MUST have a corresponding entry in `PermissionConfiguration.cs`
678
-
679
- ```
680
- ┌─────────────────────────────────────────────────────────────────────────────┐
681
- │ CONSISTENCY VALIDATION │
682
- ├─────────────────────────────────────────────────────────────────────────────┤
683
- │ │
684
- │ Permissions.cs PermissionConfiguration.cs │
685
- │ ────────────────────────────── ────────────────────────────────────── │
686
- │ Permissions.Support.Tickets.View → Path = "support.tickets.read"│
687
- │ Permissions.Support.Tickets.Create→ Path = "support.tickets.create"│
688
- │ Permissions.Support.Tickets.Update→ Path = "support.tickets.update"│
689
- │ Permissions.Support.Tickets.Delete→ Path = "support.tickets.delete"│
690
- │ │
691
- │ WARNING: COMMON ERROR: │
692
- │ - Permissions.cs: "support.tickets.read" │
693
- │ - PermissionConfiguration.cs: MISSING │
694
- │ → Result: 403 Forbidden for ALL users │
695
- │ │
696
- └─────────────────────────────────────────────────────────────────────────────┘
697
- ```
698
-
699
- ### Post-generation commands
700
-
701
- After adding entries in both files:
702
-
703
- ```bash
704
- # 1. Create migration
705
- /efcore:migration Add{Module}Permissions
706
-
707
- # 2. Apply migration
708
- /efcore:db-deploy
709
-
710
- # 3. Verify (optional)
711
- /efcore:db-status
712
- ```
713
-
714
- ---
715
-
716
- ## Template Controller avec Relations
717
-
718
- ```csharp
719
- // For controllers with related entities (ex: Tickets with Comments)
720
-
721
- #region GET with Includes
722
-
723
- [HttpGet("{id:guid}")]
724
- [RequirePermission(Permissions.{PermissionClass}.View)]
725
- [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
726
- [ProducesResponseType(StatusCodes.Status404NotFound)]
727
- public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
728
- Guid id,
729
- CancellationToken cancellationToken)
730
- {
731
- var entity = await _context.{DbSet}
732
- .Include(x => x.CreatedByUser)
733
- .Include(x => x.AssignedToUser)
734
- .Include(x => x.Comments)
735
- .ThenInclude(c => c.Author)
736
- .Include(x => x.Attachments)
737
- .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
738
-
739
- if (entity == null)
740
- return NotFound(new { message = "{Entity} not found" });
741
-
742
- return Ok(MapToDetailDto(entity));
743
- }
744
-
745
- #endregion
746
-
747
- #region Nested Resources
748
-
749
- [HttpGet("{parentId:guid}/children")]
750
- [RequirePermission(Permissions.{PermissionClass}.View)]
751
- [ProducesResponseType(typeof(List<ChildDto>), StatusCodes.Status200OK)]
752
- public async Task<ActionResult<List<ChildDto>>> GetChildren(
753
- Guid parentId,
754
- CancellationToken cancellationToken)
755
- {
756
- var children = await _context.Children
757
- .Where(x => x.ParentId == parentId)
758
- .OrderByDescending(x => x.CreatedAt)
759
- .Select(x => new ChildDto(x.Id, x.Name, x.CreatedAt))
760
- .ToListAsync(cancellationToken);
761
-
762
- return Ok(children);
763
- }
764
-
765
- [HttpPost("{parentId:guid}/children")]
766
- [RequirePermission(Permissions.{PermissionClass}.Create)]
767
- [ProducesResponseType(typeof(ChildDto), StatusCodes.Status201Created)]
768
- public async Task<ActionResult<ChildDto>> AddChild(
769
- Guid parentId,
770
- [FromBody] CreateChildRequest request,
771
- CancellationToken cancellationToken)
772
- {
773
- var parent = await _context.{DbSet}
774
- .FirstOrDefaultAsync(x => x.Id == parentId, cancellationToken);
775
-
776
- if (parent == null)
777
- return NotFound(new { message = "Parent not found" });
778
-
779
- var child = Child.Create(parentId, request.Name, _currentUser.UserId!.Value);
780
-
781
- _context.Children.Add(child);
782
- await _context.SaveChangesAsync(cancellationToken);
783
-
784
- _logger.LogInformation("User {User} added child to {Entity} {ParentId}",
785
- _currentUser.Email, parentId);
786
-
787
- return CreatedAtAction(
788
- nameof(GetChildren),
789
- new { parentId },
790
- new ChildDto(child.Id, child.Name, child.CreatedAt));
791
- }
792
-
793
- #endregion
794
- ```
795
-
796
- ---
797
-
798
- ## Reusable Patterns
799
-
800
- ### Error Response Standard
801
-
802
- ```csharp
803
- public record ErrorResponse(string Message, string? Code = null);
804
-
805
- // Usage:
806
- return BadRequest(new ErrorResponse("Validation failed", "VALIDATION_ERROR"));
807
- return Conflict(new ErrorResponse("Already exists", "DUPLICATE"));
808
- return NotFound(new { message = "Resource not found" });
809
- ```
810
-
811
- ### Pagination Query Extension
812
-
813
- ```csharp
814
- public static class QueryableExtensions
815
- {
816
- public static async Task<PaginatedResult<T>> ToPaginatedResultAsync<T>(
817
- this IQueryable<T> query,
818
- int page,
819
- int pageSize,
820
- CancellationToken ct = default)
821
- {
822
- var totalCount = await query.CountAsync(ct);
823
- var items = await query
824
- .Skip((page - 1) * pageSize)
825
- .Take(pageSize)
826
- .ToListAsync(ct);
827
-
828
- return new PaginatedResult<T>(items, totalCount, page, pageSize);
829
- }
830
- }
831
- ```
832
-
833
- ### Log Context Pattern
834
-
835
- ```csharp
836
- // Always include user context in logs
837
- _logger.LogInformation(
838
- "User {User} ({UserId}) performed {Action} on {Entity} {EntityId}",
839
- _currentUser.Email,
840
- _currentUser.UserId,
841
- "Create",
842
- "{Entity}",
843
- entity.Id);
844
- ```
845
-
846
- ---
847
-
848
- ## Template Section-Level Permissions (Level 3)
849
-
850
- > **Usage:** When a Module has multiple sub-pages/tabs with different permissions (ex: AI → Dashboard, Settings, Prompts)
851
-
852
- ### Permissions.cs - Section
853
-
854
- ```csharp
855
- // src/SmartStack.Application/Common/Authorization/Permissions.cs
856
-
857
- public static class Admin
858
- {
859
- public static class {Module}
860
- {
861
- // Section permissions (Level 3)
862
- public static class {Section}
863
- {
864
- public const string View = "{application}.{module}.{section}.read";
865
- public const string Create = "{application}.{module}.{section}.create";
866
- public const string Update = "{application}.{module}.{section}.update";
867
- public const string Delete = "{application}.{module}.{section}.delete";
868
- public const string Execute = "{application}.{module}.{section}.execute";
869
- }
870
- }
871
- }
872
- ```
873
-
874
- ### PermissionConfiguration.cs - Section Seed
875
-
876
- ```csharp
877
- // src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
878
- // ADD in Configure() method, HasData section
879
-
880
- // ============================================
881
- // STEP 1: Declare SectionId
882
- // ============================================
883
- // Get from NavigationSectionConfiguration.cs
884
-
885
- var {section}SectionId = Guid.Parse("{SECTION-GUID}");
886
-
887
- // ============================================
888
- // STEP 2: Add Section permissions (Level 3)
889
- // ============================================
890
-
891
- // Pattern: {application}.{module}.{section}.{action}
892
- // Example: administration.ai.settings.read
893
-
894
- builder.HasData(
895
- // Wildcard permission (full section access)
896
- new
897
- {
898
- Id = Guid.Parse("{NOUVEAU-GUID-1}"),
899
- Path = "{application}.{module}.{section}.*",
900
- Level = PermissionLevel.Section,
901
- Action = (PermissionAction?)null,
902
- IsWildcard = true,
903
- SectionId = {section}SectionId,
904
- Description = "Full {section} access",
905
- CreatedAt = seedDate
906
- },
907
-
908
- // Read permission
909
- new
910
- {
911
- Id = Guid.Parse("{NOUVEAU-GUID-2}"),
912
- Path = "{application}.{module}.{section}.read",
913
- Level = PermissionLevel.Section,
914
- Action = PermissionAction.Read,
915
- IsWildcard = false,
916
- SectionId = {section}SectionId,
917
- Description = "View {section}",
918
- CreatedAt = seedDate
919
- },
920
-
921
- // Create permission
922
- new
923
- {
924
- Id = Guid.Parse("{NOUVEAU-GUID-3}"),
925
- Path = "{application}.{module}.{section}.create",
926
- Level = PermissionLevel.Section,
927
- Action = PermissionAction.Create,
928
- IsWildcard = false,
929
- SectionId = {section}SectionId,
930
- Description = "Create in {section}",
931
- CreatedAt = seedDate
932
- },
933
-
934
- // Update permission
935
- new
936
- {
937
- Id = Guid.Parse("{NOUVEAU-GUID-4}"),
938
- Path = "{application}.{module}.{section}.update",
939
- Level = PermissionLevel.Section,
940
- Action = PermissionAction.Update,
941
- IsWildcard = false,
942
- SectionId = {section}SectionId,
943
- Description = "Update in {section}",
944
- CreatedAt = seedDate
945
- },
946
-
947
- // Delete permission
948
- new
949
- {
950
- Id = Guid.Parse("{NOUVEAU-GUID-5}"),
951
- Path = "{application}.{module}.{section}.delete",
952
- Level = PermissionLevel.Section,
953
- Action = PermissionAction.Delete,
954
- IsWildcard = false,
955
- SectionId = {section}SectionId,
956
- Description = "Delete in {section}",
957
- CreatedAt = seedDate
958
- },
959
-
960
- // Execute permission (optional)
961
- new
962
- {
963
- Id = Guid.Parse("{NOUVEAU-GUID-6}"),
964
- Path = "{application}.{module}.{section}.execute",
965
- Level = PermissionLevel.Section,
966
- Action = PermissionAction.Execute,
967
- IsWildcard = false,
968
- SectionId = {section}SectionId,
969
- Description = "Execute actions in {section}",
970
- CreatedAt = seedDate
971
- }
972
- );
973
- ```
974
-
975
- ---
976
-
977
- ## Template Resource-Level Permissions (Level 4)
978
-
979
- > **Usage:** For the finest granularity level (ex: Prompts → Blocks, Users → Profiles)
980
- > **CRITICAL:** Used when a Section contains sub-resources with distinct permissions
981
-
982
- ### Permissions.cs - Resource
983
-
984
- ```csharp
985
- // src/SmartStack.Application/Common/Authorization/Permissions.cs
986
-
987
- public static class Admin
988
- {
989
- public static class {Module}
990
- {
991
- public static class {Section}
992
- {
993
- // Section-level permissions...
994
-
995
- // Resource permissions (Level 4 - finest granularity)
996
- public static class {Resource}
997
- {
998
- public const string View = "{application}.{module}.{section}.{resource}.read";
999
- public const string Create = "{application}.{module}.{section}.{resource}.create";
1000
- public const string Update = "{application}.{module}.{section}.{resource}.update";
1001
- public const string Delete = "{application}.{module}.{section}.{resource}.delete";
1002
- }
1003
- }
1004
- }
1005
- }
1006
- ```
1007
-
1008
- ### PermissionConfiguration.cs - Resource Seed
1009
-
1010
- ```csharp
1011
- // src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
1012
- // ADD in Configure() method, HasData section
1013
-
1014
- // ============================================
1015
- // STEP 1: Declare ResourceId
1016
- // ============================================
1017
- // Get from NavigationResourceConfiguration.cs
1018
-
1019
- var {resource}ResourceId = Guid.Parse("{RESOURCE-GUID}");
1020
-
1021
- // ============================================
1022
- // STEP 2: Add Resource permissions (Level 4)
1023
- // ============================================
1024
-
1025
- // Pattern: {application}.{module}.{section}.{resource}.{action}
1026
- // Example: administration.ai.prompts.blocks.read
1027
-
1028
- builder.HasData(
1029
- // Wildcard permission (full resource access)
1030
- new
1031
- {
1032
- Id = Guid.Parse("{NOUVEAU-GUID-1}"),
1033
- Path = "{application}.{module}.{section}.{resource}.*",
1034
- Level = PermissionLevel.Resource,
1035
- Action = (PermissionAction?)null,
1036
- IsWildcard = true,
1037
- ResourceId = {resource}ResourceId,
1038
- Description = "Full {resource} access",
1039
- CreatedAt = seedDate
1040
- },
1041
-
1042
- // Read permission
1043
- new
1044
- {
1045
- Id = Guid.Parse("{NOUVEAU-GUID-2}"),
1046
- Path = "{application}.{module}.{section}.{resource}.read",
1047
- Level = PermissionLevel.Resource,
1048
- Action = PermissionAction.Read,
1049
- IsWildcard = false,
1050
- ResourceId = {resource}ResourceId,
1051
- Description = "View {resource}",
1052
- CreatedAt = seedDate
1053
- },
1054
-
1055
- // Create permission
1056
- new
1057
- {
1058
- Id = Guid.Parse("{NOUVEAU-GUID-3}"),
1059
- Path = "{application}.{module}.{section}.{resource}.create",
1060
- Level = PermissionLevel.Resource,
1061
- Action = PermissionAction.Create,
1062
- IsWildcard = false,
1063
- ResourceId = {resource}ResourceId,
1064
- Description = "Create {resource}",
1065
- CreatedAt = seedDate
1066
- },
1067
-
1068
- // Update permission
1069
- new
1070
- {
1071
- Id = Guid.Parse("{NOUVEAU-GUID-4}"),
1072
- Path = "{application}.{module}.{section}.{resource}.update",
1073
- Level = PermissionLevel.Resource,
1074
- Action = PermissionAction.Update,
1075
- IsWildcard = false,
1076
- ResourceId = {resource}ResourceId,
1077
- Description = "Update {resource}",
1078
- CreatedAt = seedDate
1079
- },
1080
-
1081
- // Delete permission
1082
- new
1083
- {
1084
- Id = Guid.Parse("{NOUVEAU-GUID-5}"),
1085
- Path = "{application}.{module}.{section}.{resource}.delete",
1086
- Level = PermissionLevel.Resource,
1087
- Action = PermissionAction.Delete,
1088
- IsWildcard = false,
1089
- ResourceId = {resource}ResourceId,
1090
- Description = "Delete {resource}",
1091
- CreatedAt = seedDate
1092
- }
1093
- );
1094
- ```
1095
-
1096
- ---
1097
-
1098
- ## Template Bulk Operations (Batch Insertion)
1099
-
1100
- > **MANDATORY:** Always provide bulk endpoints when creating a CRUD controller
1101
-
1102
- ### Permissions.cs - Bulk Operations
1103
-
1104
- ```csharp
1105
- // src/SmartStack.Application/Common/Authorization/Permissions.cs
1106
-
1107
- public static class {Module}
1108
- {
1109
- // CRUD standard
1110
- public const string View = "{path}.read";
1111
- public const string Create = "{path}.create";
1112
- public const string Update = "{path}.update";
1113
- public const string Delete = "{path}.delete";
1114
-
1115
- // Bulk operations (MANDATORY for all CRUD modules)
1116
- public const string BulkCreate = "{path}.bulk-create";
1117
- public const string BulkUpdate = "{path}.bulk-update";
1118
- public const string BulkDelete = "{path}.bulk-delete";
1119
- public const string Export = "{path}.export";
1120
- public const string Import = "{path}.import";
1121
- }
1122
- ```
1123
-
1124
- ### PermissionConfiguration.cs - Bulk Permissions Seed
1125
-
1126
- ```csharp
1127
- // Add after standard CRUD permissions
1128
-
1129
- // Bulk Create permission
1130
- new
1131
- {
1132
- Id = Guid.Parse("{NOUVEAU-GUID-BULK-1}"),
1133
- Path = "{application}.{module}.bulk-create",
1134
- Level = PermissionLevel.Module,
1135
- Action = PermissionAction.Create,
1136
- IsWildcard = false,
1137
- ModuleId = {module}ModuleId,
1138
- Description = "Bulk create {module}",
1139
- CreatedAt = seedDate
1140
- },
1141
-
1142
- // Bulk Update permission
1143
- new
1144
- {
1145
- Id = Guid.Parse("{NOUVEAU-GUID-BULK-2}"),
1146
- Path = "{application}.{module}.bulk-update",
1147
- Level = PermissionLevel.Module,
1148
- Action = PermissionAction.Update,
1149
- IsWildcard = false,
1150
- ModuleId = {module}ModuleId,
1151
- Description = "Bulk update {module}",
1152
- CreatedAt = seedDate
1153
- },
1154
-
1155
- // Bulk Delete permission
1156
- new
1157
- {
1158
- Id = Guid.Parse("{NOUVEAU-GUID-BULK-3}"),
1159
- Path = "{application}.{module}.bulk-delete",
1160
- Level = PermissionLevel.Module,
1161
- Action = PermissionAction.Delete,
1162
- IsWildcard = false,
1163
- ModuleId = {module}ModuleId,
1164
- Description = "Bulk delete {module}",
1165
- CreatedAt = seedDate
1166
- },
1167
-
1168
- // Export permission
1169
- new
1170
- {
1171
- Id = Guid.Parse("{NOUVEAU-GUID-EXPORT}"),
1172
- Path = "{application}.{module}.export",
1173
- Level = PermissionLevel.Module,
1174
- Action = PermissionAction.Execute,
1175
- IsWildcard = false,
1176
- ModuleId = {module}ModuleId,
1177
- Description = "Export {module} data",
1178
- CreatedAt = seedDate
1179
- },
1180
-
1181
- // Import permission
1182
- new
1183
- {
1184
- Id = Guid.Parse("{NOUVEAU-GUID-IMPORT}"),
1185
- Path = "{application}.{module}.import",
1186
- Level = PermissionLevel.Module,
1187
- Action = PermissionAction.Create,
1188
- IsWildcard = false,
1189
- ModuleId = {module}ModuleId,
1190
- Description = "Import {module} data",
1191
- CreatedAt = seedDate
1192
- }
1193
- ```
1194
-
1195
- ### Controller Endpoints - Bulk Operations
1196
-
1197
- ```csharp
1198
- // src/SmartStack.Api/Controllers/{Area}/{Module}Controller.cs
1199
- // ADD after standard CRUD endpoints
1200
-
1201
- #region BULK OPERATIONS
1202
-
1203
- /// <summary>
1204
- /// Bulk create multiple entities
1205
- /// </summary>
1206
- [HttpPost("bulk")]
1207
- [RequirePermission(Permissions.{PermissionClass}.BulkCreate)]
1208
- [ProducesResponseType(typeof(BulkOperationResult<{Entity}Dto>), StatusCodes.Status201Created)]
1209
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
1210
- public async Task<ActionResult<BulkOperationResult<{Entity}Dto>>> BulkCreate{Entity}(
1211
- [FromBody] List<Create{Entity}Request> requests,
1212
- CancellationToken cancellationToken)
1213
- {
1214
- if (requests == null || requests.Count == 0)
1215
- return BadRequest(new { message = "No items provided" });
1216
-
1217
- if (requests.Count > 100)
1218
- return BadRequest(new { message = "Maximum 100 items per bulk operation" });
1219
-
1220
- var results = new List<{Entity}Dto>();
1221
- var errors = new List<BulkOperationError>();
1222
-
1223
- for (int i = 0; i < requests.Count; i++)
1224
- {
1225
- try
1226
- {
1227
- var entity = {Entity}.Create(
1228
- requests[i].Name,
1229
- requests[i].Description
1230
- );
1231
-
1232
- _context.{DbSet}.Add(entity);
1233
- results.Add(new {Entity}Dto(entity.Id, entity.Name));
1234
- }
1235
- catch (Exception ex)
1236
- {
1237
- errors.Add(new BulkOperationError(i, requests[i].Name, ex.Message));
1238
- }
1239
- }
1240
-
1241
- await _context.SaveChangesAsync(cancellationToken);
1242
-
1243
- _logger.LogInformation("User {User} bulk created {Count} {Entity}(s), {Errors} error(s)",
1244
- _currentUser.Email, results.Count, errors.Count);
1245
-
1246
- return CreatedAtAction(
1247
- nameof(Get{Module}),
1248
- new BulkOperationResult<{Entity}Dto>(results, errors, results.Count, errors.Count));
1249
- }
1250
-
1251
- /// <summary>
1252
- /// Bulk update multiple entities
1253
- /// </summary>
1254
- [HttpPut("bulk")]
1255
- [RequirePermission(Permissions.{PermissionClass}.BulkUpdate)]
1256
- [ProducesResponseType(typeof(BulkOperationResult), StatusCodes.Status200OK)]
1257
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
1258
- public async Task<ActionResult<BulkOperationResult>> BulkUpdate{Entity}(
1259
- [FromBody] List<BulkUpdate{Entity}Request> requests,
1260
- CancellationToken cancellationToken)
1261
- {
1262
- if (requests == null || requests.Count == 0)
1263
- return BadRequest(new { message = "No items provided" });
1264
-
1265
- if (requests.Count > 100)
1266
- return BadRequest(new { message = "Maximum 100 items per bulk operation" });
1267
-
1268
- var ids = requests.Select(r => r.Id).ToList();
1269
- var entities = await _context.{DbSet}
1270
- .Where(x => ids.Contains(x.Id))
1271
- .ToDictionaryAsync(x => x.Id, cancellationToken);
1272
-
1273
- var updated = 0;
1274
- var errors = new List<BulkOperationError>();
1275
-
1276
- for (int i = 0; i < requests.Count; i++)
1277
- {
1278
- if (!entities.TryGetValue(requests[i].Id, out var entity))
1279
- {
1280
- errors.Add(new BulkOperationError(i, requests[i].Id.ToString(), "Entity not found"));
1281
- continue;
1282
- }
1283
-
1284
- try
1285
- {
1286
- entity.Update(
1287
- requests[i].Name ?? entity.Name,
1288
- requests[i].Description ?? entity.Description
1289
- );
1290
- updated++;
1291
- }
1292
- catch (Exception ex)
1293
- {
1294
- errors.Add(new BulkOperationError(i, requests[i].Id.ToString(), ex.Message));
1295
- }
1296
- }
1297
-
1298
- await _context.SaveChangesAsync(cancellationToken);
1299
-
1300
- _logger.LogInformation("User {User} bulk updated {Count} {Entity}(s), {Errors} error(s)",
1301
- _currentUser.Email, updated, errors.Count);
1302
-
1303
- return Ok(new BulkOperationResult(updated, errors.Count, errors));
1304
- }
1305
-
1306
- /// <summary>
1307
- /// Bulk delete multiple entities by IDs
1308
- /// </summary>
1309
- [HttpDelete("bulk")]
1310
- [RequirePermission(Permissions.{PermissionClass}.BulkDelete)]
1311
- [ProducesResponseType(typeof(BulkOperationResult), StatusCodes.Status200OK)]
1312
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
1313
- public async Task<ActionResult<BulkOperationResult>> BulkDelete{Entity}(
1314
- [FromBody] List<Guid> ids,
1315
- CancellationToken cancellationToken)
1316
- {
1317
- if (ids == null || ids.Count == 0)
1318
- return BadRequest(new { message = "No IDs provided" });
1319
-
1320
- if (ids.Count > 100)
1321
- return BadRequest(new { message = "Maximum 100 items per bulk operation" });
1322
-
1323
- var entities = await _context.{DbSet}
1324
- .Where(x => ids.Contains(x.Id))
1325
- .ToListAsync(cancellationToken);
1326
-
1327
- var deleted = entities.Count;
1328
- var notFound = ids.Count - deleted;
1329
-
1330
- _context.{DbSet}.RemoveRange(entities);
1331
- await _context.SaveChangesAsync(cancellationToken);
1332
-
1333
- _logger.LogWarning("User {User} bulk deleted {Count} {Entity}(s), {NotFound} not found",
1334
- _currentUser.Email, deleted, notFound);
1335
-
1336
- var errors = notFound > 0
1337
- ? new List<BulkOperationError> { new(-1, "N/A", $"{notFound} entities not found") }
1338
- : new List<BulkOperationError>();
1339
-
1340
- return Ok(new BulkOperationResult(deleted, errors.Count, errors));
1341
- }
1342
-
1343
- /// <summary>
1344
- /// Export entities to CSV/Excel
1345
- /// </summary>
1346
- [HttpGet("export")]
1347
- [RequirePermission(Permissions.{PermissionClass}.Export)]
1348
- [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
1349
- public async Task<IActionResult> Export{Module}(
1350
- [FromQuery] string format = "csv",
1351
- [FromQuery] string? search = null,
1352
- CancellationToken cancellationToken = default)
1353
- {
1354
- var query = _context.{DbSet}.AsQueryable();
1355
-
1356
- if (!string.IsNullOrWhiteSpace(search))
1357
- {
1358
- var searchLower = search.ToLower();
1359
- query = query.Where(x => x.Name.ToLower().Contains(searchLower));
1360
- }
1361
-
1362
- var entities = await query.ToListAsync(cancellationToken);
1363
-
1364
- _logger.LogInformation("User {User} exported {Count} {Entity}(s) to {Format}",
1365
- _currentUser.Email, entities.Count, format);
1366
-
1367
- // Implement CSV/Excel export logic here
1368
- // Using libraries like CsvHelper or ClosedXML
1369
-
1370
- var content = format.ToLower() switch
1371
- {
1372
- "xlsx" => GenerateExcel(entities),
1373
- _ => GenerateCsv(entities)
1374
- };
1375
-
1376
- var contentType = format.ToLower() == "xlsx"
1377
- ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1378
- : "text/csv";
1379
-
1380
- var fileName = $"{module}-export-{DateTime.UtcNow:yyyyMMdd-HHmmss}.{format}";
1381
-
1382
- return File(content, contentType, fileName);
1383
- }
1384
-
1385
- #endregion
1386
-
1387
- #region Bulk DTOs
1388
-
1389
- public record BulkOperationResult(
1390
- int SuccessCount,
1391
- int ErrorCount,
1392
- List<BulkOperationError> Errors);
1393
-
1394
- public record BulkOperationResult<T>(
1395
- List<T> Created,
1396
- List<BulkOperationError> Errors,
1397
- int SuccessCount,
1398
- int ErrorCount);
1399
-
1400
- public record BulkOperationError(
1401
- int Index,
1402
- string Identifier,
1403
- string Message);
1404
-
1405
- public record BulkUpdate{Entity}Request(
1406
- Guid Id,
1407
- string? Name,
1408
- string? Description);
1409
-
1410
- #endregion
1411
- ```
1412
-
1413
- ---
1414
-
1415
- ## Complete Permissions Hierarchy
1416
-
1417
- ```
1418
- ┌─────────────────────────────────────────────────────────────────────────────────┐
1419
- │ COMPLETE PERMISSIONS HIERARCHY │
1420
- ├─────────────────────────────────────────────────────────────────────────────────┤
1421
- │ │
1422
- │ Level 1: APPLICATION │
1423
- │ └─ Path: {application}.* │
1424
- │ └─ Ex: administration.* → Full administration access │
1425
- │ │
1426
- │ Level 2: MODULE │
1427
- │ └─ Path: {application}.{module}.{action} │
1428
- │ └─ Ex: administration.users.read → Read users │
1429
- │ └─ BULK: administration.users.bulk-create → Batch create │
1430
- │ │
1431
- │ Level 3: SECTION │
1432
- │ └─ Path: {application}.{module}.{section}.{action} │
1433
- │ └─ Ex: administration.ai.settings.update → Update AI settings │
1434
- │ │
1435
- │ Level 4: RESOURCE (finest granularity) │
1436
- │ └─ Path: {application}.{module}.{section}.{resource}.{action} │
1437
- │ └─ Ex: administration.ai.prompts.blocks.delete → Delete blocks │
1438
- │ │
1439
- └─────────────────────────────────────────────────────────────────────────────────┘
1440
- ```
1441
-
1442
- ---
1443
-
1444
- ## Controller Checklist with Complete Permissions
1445
-
1446
- ```
1447
- □ CRUD Standard
1448
- □ GET /api/.../ → {path}.read
1449
- □ GET /api/.../{id} → {path}.read
1450
- □ POST /api/.../ → {path}.create
1451
- □ PUT /api/.../{id} → {path}.update
1452
- □ DELETE /api/.../{id} → {path}.delete
1453
-
1454
- □ Bulk Operations
1455
- □ POST /api/.../bulk → {path}.bulk-create
1456
- □ PUT /api/.../bulk → {path}.bulk-update
1457
- □ DELETE /api/.../bulk → {path}.bulk-delete
1458
-
1459
- □ Export/Import
1460
- □ GET /api/.../export → {path}.export
1461
- □ POST /api/.../import → {path}.import
1462
-
1463
- □ Permissions Configured
1464
- □ Permissions.cs - Constants defined
1465
- □ PermissionConfiguration.cs - Seed HasData
1466
- □ EF Core Migration created
1467
- □ Migration applied
1468
-
1469
- □ API Versioning (if applicable)
1470
- □ See API Versioning section below
1471
-
1472
- □ Correct Permission Level
1473
- □ Module (Level 2) - For main CRUD
1474
- □ Section (Level 3) - If sub-pages with different permissions
1475
- □ Resource (Level 4) - If sub-resources with distinct permissions
1476
- ```
1477
-
1478
- ---
1479
-
1480
- ## API Versioning
1481
-
1482
- **SmartStack convention:** Header-based versioning using `Asp.Versioning.Mvc`.
1483
-
1484
- ### Setup
1485
-
1486
- ```csharp
1487
- // Program.cs
1488
- builder.Services.AddApiVersioning(options =>
1489
- {
1490
- options.DefaultApiVersion = new ApiVersion(1, 0);
1491
- options.AssumeDefaultVersionWhenUnspecified = true;
1492
- options.ReportApiVersions = true; // Adds api-supported-versions header
1493
- options.ApiVersionReader = new HeaderApiVersionReader("api-version");
1494
- })
1495
- .AddApiExplorer(options =>
1496
- {
1497
- options.GroupNameFormat = "'v'VVV";
1498
- options.SubstituteApiVersionInUrl = true;
1499
- });
1500
- ```
1501
-
1502
- ### Controller Usage
1503
-
1504
- ```csharp
1505
- // v1 - Default (no attribute needed if only one version exists)
1506
- [ApiController]
1507
- [ApiVersion("1.0")]
1508
- [NavRoute("crm.contacts")]
1509
- public class ContactsController : ControllerBase
1510
- {
1511
- [HttpGet]
1512
- [RequirePermission(Permissions.Crm.Contacts.Read)]
1513
- public async Task<ActionResult<PaginatedResult<ContactDto>>> GetAll(...) { ... }
1514
- }
1515
-
1516
- // v2 - Breaking changes only
1517
- [ApiController]
1518
- [ApiVersion("2.0")]
1519
- [NavRoute("crm.contacts")]
1520
- public class ContactsV2Controller : ControllerBase
1521
- {
1522
- [HttpGet]
1523
- [RequirePermission(Permissions.Crm.Contacts.Read)]
1524
- public async Task<ActionResult<PaginatedResult<ContactV2Dto>>> GetAll(...) { ... }
1525
- }
1526
- ```
1527
-
1528
- ### Deprecation
1529
-
1530
- ```csharp
1531
- [ApiVersion("1.0", Deprecated = true)] // Adds api-deprecated-versions header
1532
- [ApiVersion("2.0")]
1533
- public class ContactsController : ControllerBase
1534
- {
1535
- [HttpGet]
1536
- [MapToApiVersion("1.0")]
1537
- public async Task<ActionResult<PaginatedResult<ContactDto>>> GetAllV1(...) { ... }
1538
-
1539
- [HttpGet]
1540
- [MapToApiVersion("2.0")]
1541
- public async Task<ActionResult<PaginatedResult<ContactV2Dto>>> GetAllV2(...) { ... }
1542
- }
1543
- ```
1544
-
1545
- ### SmartStack Versioning Rules
1546
-
1547
- | Rule | Detail |
1548
- |------|--------|
1549
- | Default version | `1.0` (assumed when no header sent) |
1550
- | Version header | `api-version: 2.0` |
1551
- | When to version | Breaking changes only (field removal, type change, behavior change) |
1552
- | Non-breaking changes | Add to existing version (new fields, new endpoints) |
1553
- | Naming | `V2Controller` suffix or `[MapToApiVersion]` in same controller |
1554
- | Deprecation | Mark old version deprecated, maintain for 2 releases minimum |
1555
- | Documentation | Both versions documented via `[ProducesResponseType]` |
1
+ # SmartStack Controller Templates
2
+
3
+ > **⚠️ OBSOLETE - DO NOT USE THESE TEMPLATES MANUALLY**
4
+ >
5
+ > **The `/controller` skill now uses the MCP `scaffold_extension` tool to generate controllers.**
6
+ > These templates are kept for reference only. All controller generation MUST go through the MCP to ensure:
7
+ > - ✅ `[NavRoute]` attribute is included for frontend/backend sync
8
+ > - ✅ Permissions are correctly generated
9
+ > - ✅ Consistency with SmartStack conventions
10
+ >
11
+ > **To generate a controller, use:** `/controller` skill which calls MCP `scaffold_extension` automatically.
12
+ >
13
+ > **If you modify these templates, they will NOT be used.** The MCP templates in `templates/mcp-scaffolding/controller.cs.hbs` are the source of truth.
14
+
15
+ ---
16
+
17
+ ## Template CRUD Controller (Standard)
18
+
19
+ ```csharp
20
+ // src/SmartStack.Api/Controllers/{Area}/{Module}Controller.cs
21
+
22
+ using Microsoft.AspNetCore.Authorization;
23
+ using Microsoft.AspNetCore.Mvc;
24
+ using Microsoft.EntityFrameworkCore;
25
+ using SmartStack.Application.Common.Authorization;
26
+ using SmartStack.Application.Common.Interfaces;
27
+ using SmartStack.Api.Authorization;
28
+ using SmartStack.Domain.{DomainNamespace};
29
+
30
+ namespace SmartStack.Api.Controllers.{Area};
31
+
32
+ [ApiController]
33
+ [Route("api/{area-kebab}/{module-kebab}")]
34
+ [Authorize]
35
+ [Tags("{Module}")]
36
+ public class {Module}Controller : ControllerBase
37
+ {
38
+ private readonly IApplicationDbContext _context;
39
+ private readonly ICurrentUserService _currentUser;
40
+ private readonly ILogger<{Module}Controller> _logger;
41
+
42
+ public {Module}Controller(
43
+ IApplicationDbContext context,
44
+ ICurrentUserService currentUser,
45
+ ILogger<{Module}Controller> logger)
46
+ {
47
+ _context = context;
48
+ _currentUser = currentUser;
49
+ _logger = logger;
50
+ }
51
+
52
+ #region GET - List with Pagination
53
+
54
+ [HttpGet]
55
+ [RequirePermission(Permissions.{PermissionClass}.View)]
56
+ [ProducesResponseType(typeof(PaginatedResult<{Entity}ListDto>), StatusCodes.Status200OK)]
57
+ public async Task<ActionResult<PaginatedResult<{Entity}ListDto>>> Get{Module}(
58
+ [FromQuery] int page = 1,
59
+ [FromQuery] int pageSize = 20,
60
+ [FromQuery] string? search = null,
61
+ CancellationToken cancellationToken = default)
62
+ {
63
+ var query = _context.{DbSet}.AsQueryable();
64
+
65
+ // Search filter
66
+ if (!string.IsNullOrWhiteSpace(search))
67
+ {
68
+ var searchLower = search.ToLower();
69
+ query = query.Where(x =>
70
+ x.Name.ToLower().Contains(searchLower) ||
71
+ x.Description != null && x.Description.ToLower().Contains(searchLower));
72
+ }
73
+
74
+ var totalCount = await query.CountAsync(cancellationToken);
75
+
76
+ var items = await query
77
+ .OrderBy(x => x.Name)
78
+ .Skip((page - 1) * pageSize)
79
+ .Take(pageSize)
80
+ .Select(x => new {Entity}ListDto(
81
+ x.Id,
82
+ x.Name,
83
+ x.Description,
84
+ x.IsActive,
85
+ x.CreatedAt
86
+ ))
87
+ .ToListAsync(cancellationToken);
88
+
89
+ _logger.LogInformation("User {User} retrieved {Count} {Module}",
90
+ _currentUser.Email, items.Count, "{Module}");
91
+
92
+ return Ok(new PaginatedResult<{Entity}ListDto>(items, totalCount, page, pageSize));
93
+ }
94
+
95
+ #endregion
96
+
97
+ #region GET - Single by ID
98
+
99
+ [HttpGet("{id:guid}")]
100
+ [RequirePermission(Permissions.{PermissionClass}.View)]
101
+ [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
102
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
103
+ public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
104
+ Guid id,
105
+ CancellationToken cancellationToken)
106
+ {
107
+ var entity = await _context.{DbSet}
108
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
109
+
110
+ if (entity == null)
111
+ return NotFound(new { message = "{Entity} not found" });
112
+
113
+ return Ok(new {Entity}DetailDto(
114
+ entity.Id,
115
+ entity.Name,
116
+ entity.Description,
117
+ entity.IsActive,
118
+ entity.CreatedAt,
119
+ entity.UpdatedAt
120
+ ));
121
+ }
122
+
123
+ #endregion
124
+
125
+ #region POST - Create
126
+
127
+ [HttpPost]
128
+ [RequirePermission(Permissions.{PermissionClass}.Create)]
129
+ [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status201Created)]
130
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
131
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
132
+ public async Task<ActionResult<{Entity}DetailDto>> Create{Entity}(
133
+ [FromBody] Create{Entity}Request request,
134
+ CancellationToken cancellationToken)
135
+ {
136
+ // Check for duplicates
137
+ var exists = await _context.{DbSet}
138
+ .AnyAsync(x => x.Name == request.Name, cancellationToken);
139
+
140
+ if (exists)
141
+ return Conflict(new { message = "{Entity} with this name already exists" });
142
+
143
+ var entity = {Entity}.Create(
144
+ request.Name,
145
+ request.Description
146
+ );
147
+
148
+ _context.{DbSet}.Add(entity);
149
+ await _context.SaveChangesAsync(cancellationToken);
150
+
151
+ _logger.LogInformation("User {User} created {Entity} {EntityId} ({Name})",
152
+ _currentUser.Email, entity.Id, entity.Name);
153
+
154
+ return CreatedAtAction(
155
+ nameof(Get{Entity}),
156
+ new { id = entity.Id },
157
+ new {Entity}DetailDto(
158
+ entity.Id,
159
+ entity.Name,
160
+ entity.Description,
161
+ entity.IsActive,
162
+ entity.CreatedAt,
163
+ entity.UpdatedAt
164
+ ));
165
+ }
166
+
167
+ #endregion
168
+
169
+ #region PUT - Update
170
+
171
+ [HttpPut("{id:guid}")]
172
+ [RequirePermission(Permissions.{PermissionClass}.Update)]
173
+ [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
174
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
175
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
176
+ public async Task<ActionResult<{Entity}DetailDto>> Update{Entity}(
177
+ Guid id,
178
+ [FromBody] Update{Entity}Request request,
179
+ CancellationToken cancellationToken)
180
+ {
181
+ var entity = await _context.{DbSet}
182
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
183
+
184
+ if (entity == null)
185
+ return NotFound(new { message = "{Entity} not found" });
186
+
187
+ // Check for duplicate name (excluding current)
188
+ if (!string.IsNullOrEmpty(request.Name))
189
+ {
190
+ var duplicate = await _context.{DbSet}
191
+ .AnyAsync(x => x.Name == request.Name && x.Id != id, cancellationToken);
192
+
193
+ if (duplicate)
194
+ return Conflict(new { message = "{Entity} with this name already exists" });
195
+ }
196
+
197
+ entity.Update(
198
+ request.Name ?? entity.Name,
199
+ request.Description ?? entity.Description
200
+ );
201
+
202
+ await _context.SaveChangesAsync(cancellationToken);
203
+
204
+ _logger.LogInformation("User {User} updated {Entity} {EntityId}",
205
+ _currentUser.Email, entity.Id);
206
+
207
+ return Ok(new {Entity}DetailDto(
208
+ entity.Id,
209
+ entity.Name,
210
+ entity.Description,
211
+ entity.IsActive,
212
+ entity.CreatedAt,
213
+ entity.UpdatedAt
214
+ ));
215
+ }
216
+
217
+ #endregion
218
+
219
+ #region PATCH - Activate/Deactivate
220
+
221
+ [HttpPatch("{id:guid}/activate")]
222
+ [RequirePermission(Permissions.{PermissionClass}.Update)]
223
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
224
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
225
+ public async Task<IActionResult> Activate{Entity}(
226
+ Guid id,
227
+ CancellationToken cancellationToken)
228
+ {
229
+ var entity = await _context.{DbSet}
230
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
231
+
232
+ if (entity == null)
233
+ return NotFound(new { message = "{Entity} not found" });
234
+
235
+ entity.Activate();
236
+ await _context.SaveChangesAsync(cancellationToken);
237
+
238
+ _logger.LogInformation("User {User} activated {Entity} {EntityId}",
239
+ _currentUser.Email, entity.Id);
240
+
241
+ return NoContent();
242
+ }
243
+
244
+ [HttpPatch("{id:guid}/deactivate")]
245
+ [RequirePermission(Permissions.{PermissionClass}.Update)]
246
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
247
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
248
+ public async Task<IActionResult> Deactivate{Entity}(
249
+ Guid id,
250
+ CancellationToken cancellationToken)
251
+ {
252
+ var entity = await _context.{DbSet}
253
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
254
+
255
+ if (entity == null)
256
+ return NotFound(new { message = "{Entity} not found" });
257
+
258
+ entity.Deactivate();
259
+ await _context.SaveChangesAsync(cancellationToken);
260
+
261
+ _logger.LogWarning("User {User} deactivated {Entity} {EntityId}",
262
+ _currentUser.Email, entity.Id);
263
+
264
+ return NoContent();
265
+ }
266
+
267
+ #endregion
268
+
269
+ #region DELETE
270
+
271
+ [HttpDelete("{id:guid}")]
272
+ [RequirePermission(Permissions.{PermissionClass}.Delete)]
273
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
274
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
275
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
276
+ public async Task<IActionResult> Delete{Entity}(
277
+ Guid id,
278
+ CancellationToken cancellationToken)
279
+ {
280
+ var entity = await _context.{DbSet}
281
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
282
+
283
+ if (entity == null)
284
+ return NotFound(new { message = "{Entity} not found" });
285
+
286
+ // Check for dependencies before deletion
287
+ // var hasReferences = await _context.ChildEntities.AnyAsync(x => x.{Entity}Id == id, ct);
288
+ // if (hasReferences)
289
+ // return BadRequest(new { message = "Cannot delete: has dependent records" });
290
+
291
+ _context.{DbSet}.Remove(entity);
292
+ await _context.SaveChangesAsync(cancellationToken);
293
+
294
+ _logger.LogWarning("User {User} deleted {Entity} {EntityId} ({Name})",
295
+ _currentUser.Email, id, entity.Name);
296
+
297
+ return NoContent();
298
+ }
299
+
300
+ #endregion
301
+ }
302
+
303
+ #region DTOs
304
+
305
+ public record {Entity}ListDto(
306
+ Guid Id,
307
+ string Name,
308
+ string? Description,
309
+ bool IsActive,
310
+ DateTime CreatedAt
311
+ );
312
+
313
+ public record {Entity}DetailDto(
314
+ Guid Id,
315
+ string Name,
316
+ string? Description,
317
+ bool IsActive,
318
+ DateTime CreatedAt,
319
+ DateTime? UpdatedAt
320
+ );
321
+
322
+ public record Create{Entity}Request(
323
+ string Name,
324
+ string? Description
325
+ );
326
+
327
+ public record Update{Entity}Request(
328
+ string? Name,
329
+ string? Description
330
+ );
331
+
332
+ public record PaginatedResult<T>(
333
+ List<T> Items,
334
+ int TotalCount,
335
+ int Page,
336
+ int PageSize
337
+ )
338
+ {
339
+ public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
340
+ public bool HasPreviousPage => Page > 1;
341
+ public bool HasNextPage => Page < TotalPages;
342
+ }
343
+
344
+ #endregion
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Template Auth Controller (Login/Logout)
350
+
351
+ ```csharp
352
+ // src/SmartStack.Api/Controllers/AuthController.cs
353
+ // NOTE: This controller already exists - use as reference for auth patterns
354
+
355
+ using Microsoft.AspNetCore.Authorization;
356
+ using Microsoft.AspNetCore.Mvc;
357
+ using Microsoft.EntityFrameworkCore;
358
+ using Microsoft.Extensions.Options;
359
+ using SmartStack.Application.Common.Interfaces;
360
+ using SmartStack.Application.Common.Settings;
361
+ using SmartStack.Domain.Platform.Administration.Users;
362
+
363
+ namespace SmartStack.Api.Controllers;
364
+
365
+ [ApiController]
366
+ [Route("api/[controller]")]
367
+ public class AuthController : ControllerBase
368
+ {
369
+ private readonly IApplicationDbContext _context;
370
+ private readonly IPasswordService _passwordService;
371
+ private readonly IJwtService _jwtService;
372
+ private readonly IUserSessionService _sessionService;
373
+ private readonly ISessionValidationService _sessionValidationService;
374
+ private readonly SessionSettings _sessionSettings;
375
+ private readonly ILogger<AuthController> _logger;
376
+
377
+ // ... Constructor avec tous les services auth
378
+
379
+ #region Login - LOGS CRITIQUES OBLIGATOIRES
380
+
381
+ [HttpPost("login")]
382
+ [AllowAnonymous]
383
+ [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
384
+ [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
385
+ public async Task<ActionResult<LoginResponse>> Login(
386
+ [FromBody] LoginRequest request,
387
+ CancellationToken cancellationToken)
388
+ {
389
+ var ipAddress = GetClientIpAddress();
390
+ var userAgent = Request.Headers.UserAgent.ToString();
391
+
392
+ var user = await _context.Users
393
+ .Include(u => u.UserRoles)
394
+ .ThenInclude(ur => ur.Role)
395
+ .ThenInclude(r => r!.RolePermissions)
396
+ .ThenInclude(rp => rp.Permission)
397
+ .FirstOrDefaultAsync(u => u.Email == request.Email, cancellationToken);
398
+
399
+ // ============================================
400
+ // LOGS CRITIQUES - NE JAMAIS OMETTRE
401
+ // ============================================
402
+
403
+ if (user == null)
404
+ {
405
+ // WARNING: User not found (potential enumeration)
406
+ _logger.LogWarning(
407
+ "Login failed: User not found - {Email} from {IpAddress}",
408
+ request.Email, ipAddress);
409
+ return Unauthorized(new ErrorResponse("Identifiants invalides", "INVALID_CREDENTIALS"));
410
+ }
411
+
412
+ if (!user.IsActive)
413
+ {
414
+ // WARNING: Disabled account
415
+ _logger.LogWarning(
416
+ "Login failed: Account disabled - {Email} from {IpAddress}",
417
+ request.Email, ipAddress);
418
+ return Unauthorized(new ErrorResponse("Account disabled", "ACCOUNT_DISABLED"));
419
+ }
420
+
421
+ if (user.IsLocked)
422
+ {
423
+ // CRITICAL: Locked account attempt
424
+ _logger.LogCritical(
425
+ "SECURITY: Login attempt on locked account - User: {Email} (ID: {UserId}) from {IpAddress}",
426
+ request.Email, user.Id, ipAddress);
427
+ return Unauthorized(new ErrorResponse("Account locked", "ACCOUNT_LOCKED_BY_ADMIN"));
428
+ }
429
+
430
+ // Check brute force protection
431
+ var recentFailedAttempts = await _context.UserSessions
432
+ .Where(s => s.UserId == user.Id && !s.IsSuccessful && s.LoginAt > DateTime.UtcNow.AddMinutes(-15))
433
+ .CountAsync(cancellationToken);
434
+
435
+ if (recentFailedAttempts >= 5)
436
+ {
437
+ // CRITICAL: Too many failed attempts
438
+ _logger.LogCritical(
439
+ "SECURITY: Account temporarily locked due to brute force - User: {Email} (ID: {UserId}) from {IpAddress}",
440
+ request.Email, user.Id, ipAddress);
441
+ return Unauthorized(new ErrorResponse("Account temporarily locked", "ACCOUNT_LOCKED"));
442
+ }
443
+
444
+ if (!_passwordService.VerifyPassword(request.Password, user.PasswordHash))
445
+ {
446
+ // WARNING: Invalid password with remaining attempts
447
+ var remainingAttempts = 5 - recentFailedAttempts - 1;
448
+ _logger.LogWarning(
449
+ "Login failed: Invalid password - {Email} from {IpAddress}, remaining attempts: {Remaining}",
450
+ request.Email, ipAddress, remainingAttempts);
451
+
452
+ // Log failed attempt to session
453
+ await _sessionService.LogFailedLoginAsync(user.Id, ipAddress, userAgent, cancellationToken);
454
+
455
+ return Unauthorized(new ErrorResponse("Mot de passe incorrect", "INVALID_PASSWORD"));
456
+ }
457
+
458
+ // ============================================
459
+ // SUCCESS - Generate tokens
460
+ // ============================================
461
+
462
+ var roles = user.UserRoles.Select(ur => ur.Role!.Name).ToList();
463
+ var permissions = user.UserRoles
464
+ .SelectMany(ur => ur.Role!.RolePermissions)
465
+ .Select(rp => rp.Permission!.Path)
466
+ .Distinct()
467
+ .ToList();
468
+
469
+ var accessToken = _jwtService.GenerateAccessToken(user, roles, permissions);
470
+ var refreshToken = _jwtService.GenerateRefreshToken();
471
+
472
+ await _sessionService.LogLoginAsync(user.Id, accessToken, ipAddress, userAgent, cancellationToken: cancellationToken);
473
+
474
+ // INFO: Successful login
475
+ _logger.LogInformation(
476
+ "User logged in successfully: {Email} from {IpAddress}",
477
+ user.Email, ipAddress);
478
+
479
+ return Ok(new LoginResponse(accessToken, refreshToken, /* UserInfo */));
480
+ }
481
+
482
+ #endregion
483
+
484
+ #region Logout
485
+
486
+ [HttpPost("logout")]
487
+ [Authorize]
488
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
489
+ public async Task<IActionResult> Logout(CancellationToken cancellationToken)
490
+ {
491
+ var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
492
+ await _sessionService.LogLogoutAsync(token, cancellationToken);
493
+
494
+ _logger.LogInformation("User logged out: {UserId}", User.FindFirst("sub")?.Value);
495
+
496
+ return NoContent();
497
+ }
498
+
499
+ #endregion
500
+
501
+ #region Change Password - LOG WARNING
502
+
503
+ [HttpPost("change-password")]
504
+ [Authorize]
505
+ [ProducesResponseType(typeof(ChangePasswordResponse), StatusCodes.Status200OK)]
506
+ [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
507
+ public async Task<ActionResult<ChangePasswordResponse>> ChangePassword(
508
+ [FromBody] ChangePasswordRequest request,
509
+ CancellationToken cancellationToken)
510
+ {
511
+ // ... validation logic
512
+
513
+ user.UpdatePassword(newPasswordHash);
514
+ await _context.SaveChangesAsync(cancellationToken);
515
+
516
+ // Invalidate ALL sessions after password change
517
+ await _sessionService.InvalidateAllUserSessionsAsync(userId, "Password changed", cancellationToken);
518
+
519
+ // WARNING: Sensitive operation
520
+ _logger.LogWarning(
521
+ "Password changed for user {Email} - All sessions invalidated",
522
+ user.Email);
523
+
524
+ return Ok(new ChangePasswordResponse("Password changed", true));
525
+ }
526
+
527
+ #endregion
528
+
529
+ private string GetClientIpAddress()
530
+ {
531
+ var forwardedFor = Request.Headers["X-Forwarded-For"].FirstOrDefault();
532
+ if (!string.IsNullOrEmpty(forwardedFor))
533
+ return forwardedFor.Split(',')[0].Trim();
534
+ return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
535
+ }
536
+ }
537
+ ```
538
+
539
+ ---
540
+
541
+ ## Template Permissions Constants
542
+
543
+ ```csharp
544
+ // src/SmartStack.Application/Common/Authorization/Permissions.cs
545
+ // ADD to existing class
546
+
547
+ public static class Permissions
548
+ {
549
+ // ... existing permissions ...
550
+
551
+ public static class {PermissionClass}
552
+ {
553
+ public const string Access = "{permission.path}";
554
+ public const string View = "{permission.path}.read";
555
+ public const string Create = "{permission.path}.create";
556
+ public const string Update = "{permission.path}.update";
557
+ public const string Delete = "{permission.path}.delete";
558
+ // Optional depending on module
559
+ public const string Assign = "{permission.path}.assign";
560
+ public const string Execute = "{permission.path}.execute";
561
+ public const string Export = "{permission.path}.export";
562
+ }
563
+ }
564
+ ```
565
+
566
+ ---
567
+
568
+ ## Template PermissionConfiguration Seed
569
+
570
+ > **CRITICAL:** This template is MANDATORY. Without these entries, all API calls will return 403 Forbidden.
571
+
572
+ ```csharp
573
+ // src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
574
+ // ADD in Configure() method, HasData section
575
+
576
+ // ============================================
577
+ // STEP 1: Declare ModuleId
578
+ // ============================================
579
+ // Check in ModuleConfiguration.cs if module already exists
580
+ // Otherwise, create the module first via /application skill
581
+
582
+ var {module}ModuleId = Guid.Parse("{MODULE-GUID}"); // Get from ModuleConfiguration.cs
583
+
584
+ // ============================================
585
+ // STEP 2: Add permissions (HasData)
586
+ // ============================================
587
+
588
+ // Pattern: {application}.{module}.{action}
589
+ // Example: administration.users.read
590
+
591
+ var seedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
592
+
593
+ builder.HasData(
594
+ // Wildcard permission (full module access)
595
+ new
596
+ {
597
+ Id = Guid.Parse("{NOUVEAU-GUID-1}"),
598
+ Path = "{application}.{module}.*",
599
+ Level = PermissionLevel.Module,
600
+ Action = (PermissionAction?)null,
601
+ IsWildcard = true,
602
+ ModuleId = {module}ModuleId,
603
+ Description = "Full {module} management",
604
+ CreatedAt = seedDate
605
+ },
606
+
607
+ // Read permission
608
+ new
609
+ {
610
+ Id = Guid.Parse("{NOUVEAU-GUID-2}"),
611
+ Path = "{application}.{module}.read",
612
+ Level = PermissionLevel.Module,
613
+ Action = PermissionAction.Read,
614
+ IsWildcard = false,
615
+ ModuleId = {module}ModuleId,
616
+ Description = "View {module}",
617
+ CreatedAt = seedDate
618
+ },
619
+
620
+ // Create permission
621
+ new
622
+ {
623
+ Id = Guid.Parse("{NOUVEAU-GUID-3}"),
624
+ Path = "{application}.{module}.create",
625
+ Level = PermissionLevel.Module,
626
+ Action = PermissionAction.Create,
627
+ IsWildcard = false,
628
+ ModuleId = {module}ModuleId,
629
+ Description = "Create {module}",
630
+ CreatedAt = seedDate
631
+ },
632
+
633
+ // Update permission
634
+ new
635
+ {
636
+ Id = Guid.Parse("{NOUVEAU-GUID-4}"),
637
+ Path = "{application}.{module}.update",
638
+ Level = PermissionLevel.Module,
639
+ Action = PermissionAction.Update,
640
+ IsWildcard = false,
641
+ ModuleId = {module}ModuleId,
642
+ Description = "Update {module}",
643
+ CreatedAt = seedDate
644
+ },
645
+
646
+ // Delete permission
647
+ new
648
+ {
649
+ Id = Guid.Parse("{NOUVEAU-GUID-5}"),
650
+ Path = "{application}.{module}.delete",
651
+ Level = PermissionLevel.Module,
652
+ Action = PermissionAction.Delete,
653
+ IsWildcard = false,
654
+ ModuleId = {module}ModuleId,
655
+ Description = "Delete {module}",
656
+ CreatedAt = seedDate
657
+ }
658
+
659
+ // Optional actions depending on module:
660
+ // - PermissionAction.Assign → To assign resources/roles
661
+ // - PermissionAction.Execute → To execute actions (export, etc.)
662
+ );
663
+ ```
664
+
665
+ ### GUID Generation
666
+
667
+ ```bash
668
+ # PowerShell (Windows)
669
+ [guid]::NewGuid().ToString()
670
+
671
+ # Bash (Linux/Mac)
672
+ uuidgen | tr '[:upper:]' '[:lower:]'
673
+ ```
674
+
675
+ ### Validation Consistency Permissions.cs ↔ PermissionConfiguration.cs
676
+
677
+ > **RULE:** Each constant in `Permissions.cs` MUST have a corresponding entry in `PermissionConfiguration.cs`
678
+
679
+ ```
680
+ ┌─────────────────────────────────────────────────────────────────────────────┐
681
+ │ CONSISTENCY VALIDATION │
682
+ ├─────────────────────────────────────────────────────────────────────────────┤
683
+ │ │
684
+ │ Permissions.cs PermissionConfiguration.cs │
685
+ │ ────────────────────────────── ────────────────────────────────────── │
686
+ │ Permissions.Support.Tickets.View → Path = "support.tickets.read"│
687
+ │ Permissions.Support.Tickets.Create→ Path = "support.tickets.create"│
688
+ │ Permissions.Support.Tickets.Update→ Path = "support.tickets.update"│
689
+ │ Permissions.Support.Tickets.Delete→ Path = "support.tickets.delete"│
690
+ │ │
691
+ │ WARNING: COMMON ERROR: │
692
+ │ - Permissions.cs: "support.tickets.read" │
693
+ │ - PermissionConfiguration.cs: MISSING │
694
+ │ → Result: 403 Forbidden for ALL users │
695
+ │ │
696
+ └─────────────────────────────────────────────────────────────────────────────┘
697
+ ```
698
+
699
+ ### Post-generation commands
700
+
701
+ After adding entries in both files:
702
+
703
+ ```bash
704
+ # 1. Create migration
705
+ /efcore migration Add{Module}Permissions
706
+
707
+ # 2. Apply migration
708
+ /efcore db-deploy
709
+
710
+ # 3. Verify (optional)
711
+ /efcore db-status
712
+ ```
713
+
714
+ ---
715
+
716
+ ## Template Controller avec Relations
717
+
718
+ ```csharp
719
+ // For controllers with related entities (ex: Tickets with Comments)
720
+
721
+ #region GET with Includes
722
+
723
+ [HttpGet("{id:guid}")]
724
+ [RequirePermission(Permissions.{PermissionClass}.View)]
725
+ [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
726
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
727
+ public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
728
+ Guid id,
729
+ CancellationToken cancellationToken)
730
+ {
731
+ var entity = await _context.{DbSet}
732
+ .Include(x => x.CreatedByUser)
733
+ .Include(x => x.AssignedToUser)
734
+ .Include(x => x.Comments)
735
+ .ThenInclude(c => c.Author)
736
+ .Include(x => x.Attachments)
737
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
738
+
739
+ if (entity == null)
740
+ return NotFound(new { message = "{Entity} not found" });
741
+
742
+ return Ok(MapToDetailDto(entity));
743
+ }
744
+
745
+ #endregion
746
+
747
+ #region Nested Resources
748
+
749
+ [HttpGet("{parentId:guid}/children")]
750
+ [RequirePermission(Permissions.{PermissionClass}.View)]
751
+ [ProducesResponseType(typeof(List<ChildDto>), StatusCodes.Status200OK)]
752
+ public async Task<ActionResult<List<ChildDto>>> GetChildren(
753
+ Guid parentId,
754
+ CancellationToken cancellationToken)
755
+ {
756
+ var children = await _context.Children
757
+ .Where(x => x.ParentId == parentId)
758
+ .OrderByDescending(x => x.CreatedAt)
759
+ .Select(x => new ChildDto(x.Id, x.Name, x.CreatedAt))
760
+ .ToListAsync(cancellationToken);
761
+
762
+ return Ok(children);
763
+ }
764
+
765
+ [HttpPost("{parentId:guid}/children")]
766
+ [RequirePermission(Permissions.{PermissionClass}.Create)]
767
+ [ProducesResponseType(typeof(ChildDto), StatusCodes.Status201Created)]
768
+ public async Task<ActionResult<ChildDto>> AddChild(
769
+ Guid parentId,
770
+ [FromBody] CreateChildRequest request,
771
+ CancellationToken cancellationToken)
772
+ {
773
+ var parent = await _context.{DbSet}
774
+ .FirstOrDefaultAsync(x => x.Id == parentId, cancellationToken);
775
+
776
+ if (parent == null)
777
+ return NotFound(new { message = "Parent not found" });
778
+
779
+ var child = Child.Create(parentId, request.Name, _currentUser.UserId!.Value);
780
+
781
+ _context.Children.Add(child);
782
+ await _context.SaveChangesAsync(cancellationToken);
783
+
784
+ _logger.LogInformation("User {User} added child to {Entity} {ParentId}",
785
+ _currentUser.Email, parentId);
786
+
787
+ return CreatedAtAction(
788
+ nameof(GetChildren),
789
+ new { parentId },
790
+ new ChildDto(child.Id, child.Name, child.CreatedAt));
791
+ }
792
+
793
+ #endregion
794
+ ```
795
+
796
+ ---
797
+
798
+ ## Reusable Patterns
799
+
800
+ ### Error Response Standard
801
+
802
+ ```csharp
803
+ public record ErrorResponse(string Message, string? Code = null);
804
+
805
+ // Usage:
806
+ return BadRequest(new ErrorResponse("Validation failed", "VALIDATION_ERROR"));
807
+ return Conflict(new ErrorResponse("Already exists", "DUPLICATE"));
808
+ return NotFound(new { message = "Resource not found" });
809
+ ```
810
+
811
+ ### Pagination Query Extension
812
+
813
+ ```csharp
814
+ public static class QueryableExtensions
815
+ {
816
+ public static async Task<PaginatedResult<T>> ToPaginatedResultAsync<T>(
817
+ this IQueryable<T> query,
818
+ int page,
819
+ int pageSize,
820
+ CancellationToken ct = default)
821
+ {
822
+ var totalCount = await query.CountAsync(ct);
823
+ var items = await query
824
+ .Skip((page - 1) * pageSize)
825
+ .Take(pageSize)
826
+ .ToListAsync(ct);
827
+
828
+ return new PaginatedResult<T>(items, totalCount, page, pageSize);
829
+ }
830
+ }
831
+ ```
832
+
833
+ ### Log Context Pattern
834
+
835
+ ```csharp
836
+ // Always include user context in logs
837
+ _logger.LogInformation(
838
+ "User {User} ({UserId}) performed {Action} on {Entity} {EntityId}",
839
+ _currentUser.Email,
840
+ _currentUser.UserId,
841
+ "Create",
842
+ "{Entity}",
843
+ entity.Id);
844
+ ```
845
+
846
+ ---
847
+
848
+ ## Template Section-Level Permissions (Level 3)
849
+
850
+ > **Usage:** When a Module has multiple sub-pages/tabs with different permissions (ex: AI → Dashboard, Settings, Prompts)
851
+
852
+ ### Permissions.cs - Section
853
+
854
+ ```csharp
855
+ // src/SmartStack.Application/Common/Authorization/Permissions.cs
856
+
857
+ public static class Admin
858
+ {
859
+ public static class {Module}
860
+ {
861
+ // Section permissions (Level 3)
862
+ public static class {Section}
863
+ {
864
+ public const string View = "{application}.{module}.{section}.read";
865
+ public const string Create = "{application}.{module}.{section}.create";
866
+ public const string Update = "{application}.{module}.{section}.update";
867
+ public const string Delete = "{application}.{module}.{section}.delete";
868
+ public const string Execute = "{application}.{module}.{section}.execute";
869
+ }
870
+ }
871
+ }
872
+ ```
873
+
874
+ ### PermissionConfiguration.cs - Section Seed
875
+
876
+ ```csharp
877
+ // src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
878
+ // ADD in Configure() method, HasData section
879
+
880
+ // ============================================
881
+ // STEP 1: Declare SectionId
882
+ // ============================================
883
+ // Get from NavigationSectionConfiguration.cs
884
+
885
+ var {section}SectionId = Guid.Parse("{SECTION-GUID}");
886
+
887
+ // ============================================
888
+ // STEP 2: Add Section permissions (Level 3)
889
+ // ============================================
890
+
891
+ // Pattern: {application}.{module}.{section}.{action}
892
+ // Example: administration.ai.settings.read
893
+
894
+ builder.HasData(
895
+ // Wildcard permission (full section access)
896
+ new
897
+ {
898
+ Id = Guid.Parse("{NOUVEAU-GUID-1}"),
899
+ Path = "{application}.{module}.{section}.*",
900
+ Level = PermissionLevel.Section,
901
+ Action = (PermissionAction?)null,
902
+ IsWildcard = true,
903
+ SectionId = {section}SectionId,
904
+ Description = "Full {section} access",
905
+ CreatedAt = seedDate
906
+ },
907
+
908
+ // Read permission
909
+ new
910
+ {
911
+ Id = Guid.Parse("{NOUVEAU-GUID-2}"),
912
+ Path = "{application}.{module}.{section}.read",
913
+ Level = PermissionLevel.Section,
914
+ Action = PermissionAction.Read,
915
+ IsWildcard = false,
916
+ SectionId = {section}SectionId,
917
+ Description = "View {section}",
918
+ CreatedAt = seedDate
919
+ },
920
+
921
+ // Create permission
922
+ new
923
+ {
924
+ Id = Guid.Parse("{NOUVEAU-GUID-3}"),
925
+ Path = "{application}.{module}.{section}.create",
926
+ Level = PermissionLevel.Section,
927
+ Action = PermissionAction.Create,
928
+ IsWildcard = false,
929
+ SectionId = {section}SectionId,
930
+ Description = "Create in {section}",
931
+ CreatedAt = seedDate
932
+ },
933
+
934
+ // Update permission
935
+ new
936
+ {
937
+ Id = Guid.Parse("{NOUVEAU-GUID-4}"),
938
+ Path = "{application}.{module}.{section}.update",
939
+ Level = PermissionLevel.Section,
940
+ Action = PermissionAction.Update,
941
+ IsWildcard = false,
942
+ SectionId = {section}SectionId,
943
+ Description = "Update in {section}",
944
+ CreatedAt = seedDate
945
+ },
946
+
947
+ // Delete permission
948
+ new
949
+ {
950
+ Id = Guid.Parse("{NOUVEAU-GUID-5}"),
951
+ Path = "{application}.{module}.{section}.delete",
952
+ Level = PermissionLevel.Section,
953
+ Action = PermissionAction.Delete,
954
+ IsWildcard = false,
955
+ SectionId = {section}SectionId,
956
+ Description = "Delete in {section}",
957
+ CreatedAt = seedDate
958
+ },
959
+
960
+ // Execute permission (optional)
961
+ new
962
+ {
963
+ Id = Guid.Parse("{NOUVEAU-GUID-6}"),
964
+ Path = "{application}.{module}.{section}.execute",
965
+ Level = PermissionLevel.Section,
966
+ Action = PermissionAction.Execute,
967
+ IsWildcard = false,
968
+ SectionId = {section}SectionId,
969
+ Description = "Execute actions in {section}",
970
+ CreatedAt = seedDate
971
+ }
972
+ );
973
+ ```
974
+
975
+ ---
976
+
977
+ ## Template Resource-Level Permissions (Level 4)
978
+
979
+ > **Usage:** For the finest granularity level (ex: Prompts → Blocks, Users → Profiles)
980
+ > **CRITICAL:** Used when a Section contains sub-resources with distinct permissions
981
+
982
+ ### Permissions.cs - Resource
983
+
984
+ ```csharp
985
+ // src/SmartStack.Application/Common/Authorization/Permissions.cs
986
+
987
+ public static class Admin
988
+ {
989
+ public static class {Module}
990
+ {
991
+ public static class {Section}
992
+ {
993
+ // Section-level permissions...
994
+
995
+ // Resource permissions (Level 4 - finest granularity)
996
+ public static class {Resource}
997
+ {
998
+ public const string View = "{application}.{module}.{section}.{resource}.read";
999
+ public const string Create = "{application}.{module}.{section}.{resource}.create";
1000
+ public const string Update = "{application}.{module}.{section}.{resource}.update";
1001
+ public const string Delete = "{application}.{module}.{section}.{resource}.delete";
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ ```
1007
+
1008
+ ### PermissionConfiguration.cs - Resource Seed
1009
+
1010
+ ```csharp
1011
+ // src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
1012
+ // ADD in Configure() method, HasData section
1013
+
1014
+ // ============================================
1015
+ // STEP 1: Declare ResourceId
1016
+ // ============================================
1017
+ // Get from NavigationResourceConfiguration.cs
1018
+
1019
+ var {resource}ResourceId = Guid.Parse("{RESOURCE-GUID}");
1020
+
1021
+ // ============================================
1022
+ // STEP 2: Add Resource permissions (Level 4)
1023
+ // ============================================
1024
+
1025
+ // Pattern: {application}.{module}.{section}.{resource}.{action}
1026
+ // Example: administration.ai.prompts.blocks.read
1027
+
1028
+ builder.HasData(
1029
+ // Wildcard permission (full resource access)
1030
+ new
1031
+ {
1032
+ Id = Guid.Parse("{NOUVEAU-GUID-1}"),
1033
+ Path = "{application}.{module}.{section}.{resource}.*",
1034
+ Level = PermissionLevel.Resource,
1035
+ Action = (PermissionAction?)null,
1036
+ IsWildcard = true,
1037
+ ResourceId = {resource}ResourceId,
1038
+ Description = "Full {resource} access",
1039
+ CreatedAt = seedDate
1040
+ },
1041
+
1042
+ // Read permission
1043
+ new
1044
+ {
1045
+ Id = Guid.Parse("{NOUVEAU-GUID-2}"),
1046
+ Path = "{application}.{module}.{section}.{resource}.read",
1047
+ Level = PermissionLevel.Resource,
1048
+ Action = PermissionAction.Read,
1049
+ IsWildcard = false,
1050
+ ResourceId = {resource}ResourceId,
1051
+ Description = "View {resource}",
1052
+ CreatedAt = seedDate
1053
+ },
1054
+
1055
+ // Create permission
1056
+ new
1057
+ {
1058
+ Id = Guid.Parse("{NOUVEAU-GUID-3}"),
1059
+ Path = "{application}.{module}.{section}.{resource}.create",
1060
+ Level = PermissionLevel.Resource,
1061
+ Action = PermissionAction.Create,
1062
+ IsWildcard = false,
1063
+ ResourceId = {resource}ResourceId,
1064
+ Description = "Create {resource}",
1065
+ CreatedAt = seedDate
1066
+ },
1067
+
1068
+ // Update permission
1069
+ new
1070
+ {
1071
+ Id = Guid.Parse("{NOUVEAU-GUID-4}"),
1072
+ Path = "{application}.{module}.{section}.{resource}.update",
1073
+ Level = PermissionLevel.Resource,
1074
+ Action = PermissionAction.Update,
1075
+ IsWildcard = false,
1076
+ ResourceId = {resource}ResourceId,
1077
+ Description = "Update {resource}",
1078
+ CreatedAt = seedDate
1079
+ },
1080
+
1081
+ // Delete permission
1082
+ new
1083
+ {
1084
+ Id = Guid.Parse("{NOUVEAU-GUID-5}"),
1085
+ Path = "{application}.{module}.{section}.{resource}.delete",
1086
+ Level = PermissionLevel.Resource,
1087
+ Action = PermissionAction.Delete,
1088
+ IsWildcard = false,
1089
+ ResourceId = {resource}ResourceId,
1090
+ Description = "Delete {resource}",
1091
+ CreatedAt = seedDate
1092
+ }
1093
+ );
1094
+ ```
1095
+
1096
+ ---
1097
+
1098
+ ## Template Bulk Operations (Batch Insertion)
1099
+
1100
+ > **MANDATORY:** Always provide bulk endpoints when creating a CRUD controller
1101
+
1102
+ ### Permissions.cs - Bulk Operations
1103
+
1104
+ ```csharp
1105
+ // src/SmartStack.Application/Common/Authorization/Permissions.cs
1106
+
1107
+ public static class {Module}
1108
+ {
1109
+ // CRUD standard
1110
+ public const string View = "{path}.read";
1111
+ public const string Create = "{path}.create";
1112
+ public const string Update = "{path}.update";
1113
+ public const string Delete = "{path}.delete";
1114
+
1115
+ // Bulk operations (MANDATORY for all CRUD modules)
1116
+ public const string BulkCreate = "{path}.bulk-create";
1117
+ public const string BulkUpdate = "{path}.bulk-update";
1118
+ public const string BulkDelete = "{path}.bulk-delete";
1119
+ public const string Export = "{path}.export";
1120
+ public const string Import = "{path}.import";
1121
+ }
1122
+ ```
1123
+
1124
+ ### PermissionConfiguration.cs - Bulk Permissions Seed
1125
+
1126
+ ```csharp
1127
+ // Add after standard CRUD permissions
1128
+
1129
+ // Bulk Create permission
1130
+ new
1131
+ {
1132
+ Id = Guid.Parse("{NOUVEAU-GUID-BULK-1}"),
1133
+ Path = "{application}.{module}.bulk-create",
1134
+ Level = PermissionLevel.Module,
1135
+ Action = PermissionAction.Create,
1136
+ IsWildcard = false,
1137
+ ModuleId = {module}ModuleId,
1138
+ Description = "Bulk create {module}",
1139
+ CreatedAt = seedDate
1140
+ },
1141
+
1142
+ // Bulk Update permission
1143
+ new
1144
+ {
1145
+ Id = Guid.Parse("{NOUVEAU-GUID-BULK-2}"),
1146
+ Path = "{application}.{module}.bulk-update",
1147
+ Level = PermissionLevel.Module,
1148
+ Action = PermissionAction.Update,
1149
+ IsWildcard = false,
1150
+ ModuleId = {module}ModuleId,
1151
+ Description = "Bulk update {module}",
1152
+ CreatedAt = seedDate
1153
+ },
1154
+
1155
+ // Bulk Delete permission
1156
+ new
1157
+ {
1158
+ Id = Guid.Parse("{NOUVEAU-GUID-BULK-3}"),
1159
+ Path = "{application}.{module}.bulk-delete",
1160
+ Level = PermissionLevel.Module,
1161
+ Action = PermissionAction.Delete,
1162
+ IsWildcard = false,
1163
+ ModuleId = {module}ModuleId,
1164
+ Description = "Bulk delete {module}",
1165
+ CreatedAt = seedDate
1166
+ },
1167
+
1168
+ // Export permission
1169
+ new
1170
+ {
1171
+ Id = Guid.Parse("{NOUVEAU-GUID-EXPORT}"),
1172
+ Path = "{application}.{module}.export",
1173
+ Level = PermissionLevel.Module,
1174
+ Action = PermissionAction.Execute,
1175
+ IsWildcard = false,
1176
+ ModuleId = {module}ModuleId,
1177
+ Description = "Export {module} data",
1178
+ CreatedAt = seedDate
1179
+ },
1180
+
1181
+ // Import permission
1182
+ new
1183
+ {
1184
+ Id = Guid.Parse("{NOUVEAU-GUID-IMPORT}"),
1185
+ Path = "{application}.{module}.import",
1186
+ Level = PermissionLevel.Module,
1187
+ Action = PermissionAction.Create,
1188
+ IsWildcard = false,
1189
+ ModuleId = {module}ModuleId,
1190
+ Description = "Import {module} data",
1191
+ CreatedAt = seedDate
1192
+ }
1193
+ ```
1194
+
1195
+ ### Controller Endpoints - Bulk Operations
1196
+
1197
+ ```csharp
1198
+ // src/SmartStack.Api/Controllers/{Area}/{Module}Controller.cs
1199
+ // ADD after standard CRUD endpoints
1200
+
1201
+ #region BULK OPERATIONS
1202
+
1203
+ /// <summary>
1204
+ /// Bulk create multiple entities
1205
+ /// </summary>
1206
+ [HttpPost("bulk")]
1207
+ [RequirePermission(Permissions.{PermissionClass}.BulkCreate)]
1208
+ [ProducesResponseType(typeof(BulkOperationResult<{Entity}Dto>), StatusCodes.Status201Created)]
1209
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
1210
+ public async Task<ActionResult<BulkOperationResult<{Entity}Dto>>> BulkCreate{Entity}(
1211
+ [FromBody] List<Create{Entity}Request> requests,
1212
+ CancellationToken cancellationToken)
1213
+ {
1214
+ if (requests == null || requests.Count == 0)
1215
+ return BadRequest(new { message = "No items provided" });
1216
+
1217
+ if (requests.Count > 100)
1218
+ return BadRequest(new { message = "Maximum 100 items per bulk operation" });
1219
+
1220
+ var results = new List<{Entity}Dto>();
1221
+ var errors = new List<BulkOperationError>();
1222
+
1223
+ for (int i = 0; i < requests.Count; i++)
1224
+ {
1225
+ try
1226
+ {
1227
+ var entity = {Entity}.Create(
1228
+ requests[i].Name,
1229
+ requests[i].Description
1230
+ );
1231
+
1232
+ _context.{DbSet}.Add(entity);
1233
+ results.Add(new {Entity}Dto(entity.Id, entity.Name));
1234
+ }
1235
+ catch (Exception ex)
1236
+ {
1237
+ errors.Add(new BulkOperationError(i, requests[i].Name, ex.Message));
1238
+ }
1239
+ }
1240
+
1241
+ await _context.SaveChangesAsync(cancellationToken);
1242
+
1243
+ _logger.LogInformation("User {User} bulk created {Count} {Entity}(s), {Errors} error(s)",
1244
+ _currentUser.Email, results.Count, errors.Count);
1245
+
1246
+ return CreatedAtAction(
1247
+ nameof(Get{Module}),
1248
+ new BulkOperationResult<{Entity}Dto>(results, errors, results.Count, errors.Count));
1249
+ }
1250
+
1251
+ /// <summary>
1252
+ /// Bulk update multiple entities
1253
+ /// </summary>
1254
+ [HttpPut("bulk")]
1255
+ [RequirePermission(Permissions.{PermissionClass}.BulkUpdate)]
1256
+ [ProducesResponseType(typeof(BulkOperationResult), StatusCodes.Status200OK)]
1257
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
1258
+ public async Task<ActionResult<BulkOperationResult>> BulkUpdate{Entity}(
1259
+ [FromBody] List<BulkUpdate{Entity}Request> requests,
1260
+ CancellationToken cancellationToken)
1261
+ {
1262
+ if (requests == null || requests.Count == 0)
1263
+ return BadRequest(new { message = "No items provided" });
1264
+
1265
+ if (requests.Count > 100)
1266
+ return BadRequest(new { message = "Maximum 100 items per bulk operation" });
1267
+
1268
+ var ids = requests.Select(r => r.Id).ToList();
1269
+ var entities = await _context.{DbSet}
1270
+ .Where(x => ids.Contains(x.Id))
1271
+ .ToDictionaryAsync(x => x.Id, cancellationToken);
1272
+
1273
+ var updated = 0;
1274
+ var errors = new List<BulkOperationError>();
1275
+
1276
+ for (int i = 0; i < requests.Count; i++)
1277
+ {
1278
+ if (!entities.TryGetValue(requests[i].Id, out var entity))
1279
+ {
1280
+ errors.Add(new BulkOperationError(i, requests[i].Id.ToString(), "Entity not found"));
1281
+ continue;
1282
+ }
1283
+
1284
+ try
1285
+ {
1286
+ entity.Update(
1287
+ requests[i].Name ?? entity.Name,
1288
+ requests[i].Description ?? entity.Description
1289
+ );
1290
+ updated++;
1291
+ }
1292
+ catch (Exception ex)
1293
+ {
1294
+ errors.Add(new BulkOperationError(i, requests[i].Id.ToString(), ex.Message));
1295
+ }
1296
+ }
1297
+
1298
+ await _context.SaveChangesAsync(cancellationToken);
1299
+
1300
+ _logger.LogInformation("User {User} bulk updated {Count} {Entity}(s), {Errors} error(s)",
1301
+ _currentUser.Email, updated, errors.Count);
1302
+
1303
+ return Ok(new BulkOperationResult(updated, errors.Count, errors));
1304
+ }
1305
+
1306
+ /// <summary>
1307
+ /// Bulk delete multiple entities by IDs
1308
+ /// </summary>
1309
+ [HttpDelete("bulk")]
1310
+ [RequirePermission(Permissions.{PermissionClass}.BulkDelete)]
1311
+ [ProducesResponseType(typeof(BulkOperationResult), StatusCodes.Status200OK)]
1312
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
1313
+ public async Task<ActionResult<BulkOperationResult>> BulkDelete{Entity}(
1314
+ [FromBody] List<Guid> ids,
1315
+ CancellationToken cancellationToken)
1316
+ {
1317
+ if (ids == null || ids.Count == 0)
1318
+ return BadRequest(new { message = "No IDs provided" });
1319
+
1320
+ if (ids.Count > 100)
1321
+ return BadRequest(new { message = "Maximum 100 items per bulk operation" });
1322
+
1323
+ var entities = await _context.{DbSet}
1324
+ .Where(x => ids.Contains(x.Id))
1325
+ .ToListAsync(cancellationToken);
1326
+
1327
+ var deleted = entities.Count;
1328
+ var notFound = ids.Count - deleted;
1329
+
1330
+ _context.{DbSet}.RemoveRange(entities);
1331
+ await _context.SaveChangesAsync(cancellationToken);
1332
+
1333
+ _logger.LogWarning("User {User} bulk deleted {Count} {Entity}(s), {NotFound} not found",
1334
+ _currentUser.Email, deleted, notFound);
1335
+
1336
+ var errors = notFound > 0
1337
+ ? new List<BulkOperationError> { new(-1, "N/A", $"{notFound} entities not found") }
1338
+ : new List<BulkOperationError>();
1339
+
1340
+ return Ok(new BulkOperationResult(deleted, errors.Count, errors));
1341
+ }
1342
+
1343
+ /// <summary>
1344
+ /// Export entities to CSV/Excel
1345
+ /// </summary>
1346
+ [HttpGet("export")]
1347
+ [RequirePermission(Permissions.{PermissionClass}.Export)]
1348
+ [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
1349
+ public async Task<IActionResult> Export{Module}(
1350
+ [FromQuery] string format = "csv",
1351
+ [FromQuery] string? search = null,
1352
+ CancellationToken cancellationToken = default)
1353
+ {
1354
+ var query = _context.{DbSet}.AsQueryable();
1355
+
1356
+ if (!string.IsNullOrWhiteSpace(search))
1357
+ {
1358
+ var searchLower = search.ToLower();
1359
+ query = query.Where(x => x.Name.ToLower().Contains(searchLower));
1360
+ }
1361
+
1362
+ var entities = await query.ToListAsync(cancellationToken);
1363
+
1364
+ _logger.LogInformation("User {User} exported {Count} {Entity}(s) to {Format}",
1365
+ _currentUser.Email, entities.Count, format);
1366
+
1367
+ // Implement CSV/Excel export logic here
1368
+ // Using libraries like CsvHelper or ClosedXML
1369
+
1370
+ var content = format.ToLower() switch
1371
+ {
1372
+ "xlsx" => GenerateExcel(entities),
1373
+ _ => GenerateCsv(entities)
1374
+ };
1375
+
1376
+ var contentType = format.ToLower() == "xlsx"
1377
+ ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1378
+ : "text/csv";
1379
+
1380
+ var fileName = $"{module}-export-{DateTime.UtcNow:yyyyMMdd-HHmmss}.{format}";
1381
+
1382
+ return File(content, contentType, fileName);
1383
+ }
1384
+
1385
+ #endregion
1386
+
1387
+ #region Bulk DTOs
1388
+
1389
+ public record BulkOperationResult(
1390
+ int SuccessCount,
1391
+ int ErrorCount,
1392
+ List<BulkOperationError> Errors);
1393
+
1394
+ public record BulkOperationResult<T>(
1395
+ List<T> Created,
1396
+ List<BulkOperationError> Errors,
1397
+ int SuccessCount,
1398
+ int ErrorCount);
1399
+
1400
+ public record BulkOperationError(
1401
+ int Index,
1402
+ string Identifier,
1403
+ string Message);
1404
+
1405
+ public record BulkUpdate{Entity}Request(
1406
+ Guid Id,
1407
+ string? Name,
1408
+ string? Description);
1409
+
1410
+ #endregion
1411
+ ```
1412
+
1413
+ ---
1414
+
1415
+ ## Complete Permissions Hierarchy
1416
+
1417
+ ```
1418
+ ┌─────────────────────────────────────────────────────────────────────────────────┐
1419
+ │ COMPLETE PERMISSIONS HIERARCHY │
1420
+ ├─────────────────────────────────────────────────────────────────────────────────┤
1421
+ │ │
1422
+ │ Level 1: APPLICATION │
1423
+ │ └─ Path: {application}.* │
1424
+ │ └─ Ex: administration.* → Full administration access │
1425
+ │ │
1426
+ │ Level 2: MODULE │
1427
+ │ └─ Path: {application}.{module}.{action} │
1428
+ │ └─ Ex: administration.users.read → Read users │
1429
+ │ └─ BULK: administration.users.bulk-create → Batch create │
1430
+ │ │
1431
+ │ Level 3: SECTION │
1432
+ │ └─ Path: {application}.{module}.{section}.{action} │
1433
+ │ └─ Ex: administration.ai.settings.update → Update AI settings │
1434
+ │ │
1435
+ │ Level 4: RESOURCE (finest granularity) │
1436
+ │ └─ Path: {application}.{module}.{section}.{resource}.{action} │
1437
+ │ └─ Ex: administration.ai.prompts.blocks.delete → Delete blocks │
1438
+ │ │
1439
+ └─────────────────────────────────────────────────────────────────────────────────┘
1440
+ ```
1441
+
1442
+ ---
1443
+
1444
+ ## Controller Checklist with Complete Permissions
1445
+
1446
+ ```
1447
+ □ CRUD Standard
1448
+ □ GET /api/.../ → {path}.read
1449
+ □ GET /api/.../{id} → {path}.read
1450
+ □ POST /api/.../ → {path}.create
1451
+ □ PUT /api/.../{id} → {path}.update
1452
+ □ DELETE /api/.../{id} → {path}.delete
1453
+
1454
+ □ Bulk Operations
1455
+ □ POST /api/.../bulk → {path}.bulk-create
1456
+ □ PUT /api/.../bulk → {path}.bulk-update
1457
+ □ DELETE /api/.../bulk → {path}.bulk-delete
1458
+
1459
+ □ Export/Import
1460
+ □ GET /api/.../export → {path}.export
1461
+ □ POST /api/.../import → {path}.import
1462
+
1463
+ □ Permissions Configured
1464
+ □ Permissions.cs - Constants defined
1465
+ □ PermissionConfiguration.cs - Seed HasData
1466
+ □ EF Core Migration created
1467
+ □ Migration applied
1468
+
1469
+ □ API Versioning (if applicable)
1470
+ □ See API Versioning section below
1471
+
1472
+ □ Correct Permission Level
1473
+ □ Module (Level 2) - For main CRUD
1474
+ □ Section (Level 3) - If sub-pages with different permissions
1475
+ □ Resource (Level 4) - If sub-resources with distinct permissions
1476
+ ```
1477
+
1478
+ ---
1479
+
1480
+ ## API Versioning
1481
+
1482
+ **SmartStack convention:** Header-based versioning using `Asp.Versioning.Mvc`.
1483
+
1484
+ ### Setup
1485
+
1486
+ ```csharp
1487
+ // Program.cs
1488
+ builder.Services.AddApiVersioning(options =>
1489
+ {
1490
+ options.DefaultApiVersion = new ApiVersion(1, 0);
1491
+ options.AssumeDefaultVersionWhenUnspecified = true;
1492
+ options.ReportApiVersions = true; // Adds api-supported-versions header
1493
+ options.ApiVersionReader = new HeaderApiVersionReader("api-version");
1494
+ })
1495
+ .AddApiExplorer(options =>
1496
+ {
1497
+ options.GroupNameFormat = "'v'VVV";
1498
+ options.SubstituteApiVersionInUrl = true;
1499
+ });
1500
+ ```
1501
+
1502
+ ### Controller Usage
1503
+
1504
+ ```csharp
1505
+ // v1 - Default (no attribute needed if only one version exists)
1506
+ [ApiController]
1507
+ [ApiVersion("1.0")]
1508
+ [NavRoute("crm.contacts")]
1509
+ public class ContactsController : ControllerBase
1510
+ {
1511
+ [HttpGet]
1512
+ [RequirePermission(Permissions.Crm.Contacts.Read)]
1513
+ public async Task<ActionResult<PaginatedResult<ContactDto>>> GetAll(...) { ... }
1514
+ }
1515
+
1516
+ // v2 - Breaking changes only
1517
+ [ApiController]
1518
+ [ApiVersion("2.0")]
1519
+ [NavRoute("crm.contacts")]
1520
+ public class ContactsV2Controller : ControllerBase
1521
+ {
1522
+ [HttpGet]
1523
+ [RequirePermission(Permissions.Crm.Contacts.Read)]
1524
+ public async Task<ActionResult<PaginatedResult<ContactV2Dto>>> GetAll(...) { ... }
1525
+ }
1526
+ ```
1527
+
1528
+ ### Deprecation
1529
+
1530
+ ```csharp
1531
+ [ApiVersion("1.0", Deprecated = true)] // Adds api-deprecated-versions header
1532
+ [ApiVersion("2.0")]
1533
+ public class ContactsController : ControllerBase
1534
+ {
1535
+ [HttpGet]
1536
+ [MapToApiVersion("1.0")]
1537
+ public async Task<ActionResult<PaginatedResult<ContactDto>>> GetAllV1(...) { ... }
1538
+
1539
+ [HttpGet]
1540
+ [MapToApiVersion("2.0")]
1541
+ public async Task<ActionResult<PaginatedResult<ContactV2Dto>>> GetAllV2(...) { ... }
1542
+ }
1543
+ ```
1544
+
1545
+ ### SmartStack Versioning Rules
1546
+
1547
+ | Rule | Detail |
1548
+ |------|--------|
1549
+ | Default version | `1.0` (assumed when no header sent) |
1550
+ | Version header | `api-version: 2.0` |
1551
+ | When to version | Breaking changes only (field removal, type change, behavior change) |
1552
+ | Non-breaking changes | Add to existing version (new fields, new endpoints) |
1553
+ | Naming | `V2Controller` suffix or `[MapToApiVersion]` in same controller |
1554
+ | Deprecation | Mark old version deprecated, maintain for 2 releases minimum |
1555
+ | Documentation | Both versions documented via `[ProducesResponseType]` |