@atlashub/smartstack-cli 3.39.0 → 3.40.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 (471) 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.map +1 -1
  12. package/dist/mcp-entry.mjs +6 -4
  13. package/dist/mcp-entry.mjs.map +1 -1
  14. package/package.json +115 -115
  15. package/scripts/extract-api-endpoints.ts +325 -325
  16. package/scripts/extract-business-rules.ts +440 -440
  17. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  18. package/scripts/health-check.sh +168 -168
  19. package/scripts/postinstall.js +18 -18
  20. package/templates/agents/action.md +37 -37
  21. package/templates/agents/ba-reader.md +378 -378
  22. package/templates/agents/ba-writer.md +861 -861
  23. package/templates/agents/code-reviewer.md +163 -163
  24. package/templates/agents/db-reader.md +149 -149
  25. package/templates/agents/docs-context-reader.md +143 -143
  26. package/templates/agents/docs-sync-checker.md +122 -122
  27. package/templates/agents/efcore/conflicts.md +84 -84
  28. package/templates/agents/efcore/db-deploy.md +74 -74
  29. package/templates/agents/efcore/db-reset.md +85 -85
  30. package/templates/agents/efcore/db-seed.md +61 -61
  31. package/templates/agents/efcore/db-status.md +86 -86
  32. package/templates/agents/efcore/migration.md +186 -186
  33. package/templates/agents/efcore/rebase-snapshot.md +108 -108
  34. package/templates/agents/efcore/scan.md +92 -92
  35. package/templates/agents/efcore/squash.md +161 -161
  36. package/templates/agents/explore-codebase.md +66 -66
  37. package/templates/agents/explore-docs.md +98 -98
  38. package/templates/agents/fix-grammar.md +50 -50
  39. package/templates/agents/gitflow/abort.md +45 -45
  40. package/templates/agents/gitflow/cleanup.md +96 -96
  41. package/templates/agents/gitflow/commit.md +236 -236
  42. package/templates/agents/gitflow/exec.md +48 -48
  43. package/templates/agents/gitflow/finish.md +146 -146
  44. package/templates/agents/gitflow/init-clone.md +199 -199
  45. package/templates/agents/gitflow/init-detect.md +137 -137
  46. package/templates/agents/gitflow/init-validate.md +225 -225
  47. package/templates/agents/gitflow/init.md +340 -340
  48. package/templates/agents/gitflow/merge.md +145 -145
  49. package/templates/agents/gitflow/plan.md +42 -42
  50. package/templates/agents/gitflow/pr.md +191 -191
  51. package/templates/agents/gitflow/review.md +49 -49
  52. package/templates/agents/gitflow/start.md +147 -147
  53. package/templates/agents/gitflow/status.md +95 -95
  54. package/templates/agents/mcp-healthcheck.md +163 -163
  55. package/templates/agents/snipper.md +37 -37
  56. package/templates/agents/websearch.md +46 -46
  57. package/templates/hooks/appsettings-guard.sh +76 -76
  58. package/templates/hooks/docs-drift-check.md +96 -96
  59. package/templates/hooks/ef-migration-check.md +139 -139
  60. package/templates/hooks/hooks.json +58 -58
  61. package/templates/hooks/mcp-check.md +64 -64
  62. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  63. package/templates/hooks/ralph-session-end.sh +69 -69
  64. package/templates/hooks/stop-hook.sh +177 -177
  65. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  66. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  67. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  68. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  69. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  70. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  71. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  72. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  73. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  74. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  75. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  76. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  77. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  78. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  79. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  80. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  81. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  82. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  83. package/templates/project/ExampleEntity.cs.template +116 -116
  84. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  85. package/templates/project/ExampleService.cs.template +146 -146
  86. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  87. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  88. package/templates/project/Program.cs.template +47 -47
  89. package/templates/project/README.md +79 -79
  90. package/templates/project/api.ts.template +12 -12
  91. package/templates/project/appsettings.json.template +170 -170
  92. package/templates/project/claude-settings.json.template +5 -5
  93. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  94. package/templates/project/test-frontend/msw/server.ts +25 -25
  95. package/templates/project/test-frontend/setup.ts +16 -16
  96. package/templates/project/test-frontend/test-utils.tsx +59 -59
  97. package/templates/project/test-frontend/vitest.config.ts +31 -31
  98. package/templates/ralph/README.md +93 -93
  99. package/templates/ralph/ralph.config.yaml +113 -113
  100. package/templates/scripts/setup-ralph-loop.sh +173 -173
  101. package/templates/skills/_resources/config-safety.md +61 -61
  102. package/templates/skills/_resources/context-digest-template.md +53 -53
  103. package/templates/skills/_resources/doc-context-cache.md +60 -60
  104. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  105. package/templates/skills/_resources/formatting-guide.md +124 -124
  106. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  107. package/templates/skills/_shared.md +228 -228
  108. package/templates/skills/admin/SKILL.md +48 -48
  109. package/templates/skills/ai-prompt/SKILL.md +107 -107
  110. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  111. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  112. package/templates/skills/apex/SKILL.md +168 -168
  113. package/templates/skills/apex/_shared.md +141 -141
  114. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  115. package/templates/skills/apex/references/analysis-methods.md +141 -141
  116. package/templates/skills/apex/references/challenge-questions.md +145 -145
  117. package/templates/skills/apex/references/code-generation.md +412 -412
  118. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  119. package/templates/skills/apex/references/error-classification.md +144 -144
  120. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  121. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  122. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  123. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  124. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  125. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  126. package/templates/skills/apex/references/post-checks.md +1584 -1584
  127. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  128. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  129. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  130. package/templates/skills/apex/steps/step-00-init.md +307 -307
  131. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  132. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  133. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  134. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  135. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  136. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  137. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  138. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  139. package/templates/skills/application/SKILL.md +4 -4
  140. package/templates/skills/application/references/application-roles-template.md +227 -227
  141. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  142. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  143. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  144. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  145. package/templates/skills/application/references/backend-verification.md +88 -88
  146. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  147. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  148. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  149. package/templates/skills/application/references/frontend-verification.md +156 -156
  150. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  151. package/templates/skills/application/references/provider-template.md +177 -177
  152. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  153. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  154. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  155. package/templates/skills/application/references/test-frontend.md +73 -73
  156. package/templates/skills/application/references/test-prerequisites.md +72 -72
  157. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  158. package/templates/skills/application/steps/step-06-migration.md +193 -193
  159. package/templates/skills/application/steps/step-07-tests.md +356 -356
  160. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  161. package/templates/skills/application/templates-backend.md +463 -463
  162. package/templates/skills/application/templates-frontend.md +685 -685
  163. package/templates/skills/application/templates-i18n.md +520 -520
  164. package/templates/skills/application/templates-seed.md +1096 -1096
  165. package/templates/skills/business-analyse/SKILL.md +327 -327
  166. package/templates/skills/business-analyse/_architecture.md +123 -123
  167. package/templates/skills/business-analyse/_elicitation.md +206 -206
  168. package/templates/skills/business-analyse/_module-loop.md +115 -115
  169. package/templates/skills/business-analyse/_shared.md +383 -383
  170. package/templates/skills/business-analyse/_suggestions.md +34 -34
  171. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  172. package/templates/skills/business-analyse/html/build-html.js +77 -77
  173. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  174. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  175. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  176. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  177. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  178. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  179. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  180. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  181. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  182. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  183. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  184. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  185. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  186. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  187. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  188. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  189. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  190. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  191. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  192. package/templates/skills/business-analyse/html/src/template.html +516 -516
  193. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  194. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  195. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  196. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  197. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  198. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  199. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  200. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  201. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  202. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  203. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  204. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  205. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  206. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  207. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  208. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  209. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  210. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  211. package/templates/skills/business-analyse/questionnaire.md +337 -337
  212. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  213. package/templates/skills/business-analyse/react/components.md +551 -551
  214. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  215. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  216. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  217. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  218. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  219. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  220. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  221. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  222. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  223. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  224. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  225. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  226. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  227. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  228. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  229. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  230. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  231. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  232. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  233. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  234. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  235. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  236. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  237. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  238. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  239. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  240. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  241. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  242. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  243. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  244. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  245. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  246. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  247. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  248. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  249. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  250. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  251. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  252. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  253. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  254. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  255. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  256. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  257. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  258. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  259. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  260. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  261. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  262. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  263. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  264. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  265. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  266. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  267. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  268. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  269. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  270. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  271. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  272. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  273. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  274. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  275. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  276. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  277. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  278. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  279. package/templates/skills/business-analyse/templates-frd.md +476 -476
  280. package/templates/skills/business-analyse/templates-react.md +574 -574
  281. package/templates/skills/cc-agent/SKILL.md +129 -129
  282. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  283. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  284. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  285. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  286. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  287. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  288. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  289. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  290. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  291. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  292. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  293. package/templates/skills/cc-audit/SKILL.md +108 -108
  294. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  295. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  296. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  297. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  298. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  299. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  300. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  301. package/templates/skills/cc-skill/SKILL.md +134 -134
  302. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  303. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  304. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  305. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  306. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  307. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  308. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  309. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  310. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  311. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  312. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  313. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  314. package/templates/skills/check-version/SKILL.md +196 -196
  315. package/templates/skills/controller/SKILL.md +162 -162
  316. package/templates/skills/controller/postman-templates.md +614 -614
  317. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  318. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  319. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  320. package/templates/skills/controller/steps/step-00-init.md +193 -191
  321. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  322. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  323. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  324. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  325. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  326. package/templates/skills/controller/templates.md +1555 -1555
  327. package/templates/skills/debug/SKILL.md +70 -70
  328. package/templates/skills/debug/references/team-protocol.md +232 -232
  329. package/templates/skills/debug/steps/step-00-init.md +57 -57
  330. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  331. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  332. package/templates/skills/documentation/SKILL.md +132 -132
  333. package/templates/skills/documentation/data-schema.md +227 -227
  334. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  335. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  336. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  337. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  338. package/templates/skills/documentation/templates.md +662 -663
  339. package/templates/skills/efcore/SKILL.md +167 -167
  340. package/templates/skills/efcore/references/both-contexts.md +32 -32
  341. package/templates/skills/efcore/references/database-operations.md +67 -67
  342. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  343. package/templates/skills/efcore/references/reset-operations.md +81 -81
  344. package/templates/skills/efcore/references/seed-methods.md +86 -86
  345. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  346. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  347. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  348. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  349. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  350. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  351. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  352. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  353. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  354. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  355. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  356. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  357. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  362. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  363. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  364. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  365. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  366. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  367. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  368. package/templates/skills/explore/SKILL.md +98 -98
  369. package/templates/skills/feature-full/SKILL.md +111 -111
  370. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  371. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  372. package/templates/skills/gitflow/SKILL.md +377 -377
  373. package/templates/skills/gitflow/_shared.md +620 -620
  374. package/templates/skills/gitflow/phases/abort.md +189 -189
  375. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  376. package/templates/skills/gitflow/phases/status.md +192 -192
  377. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  378. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  379. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  380. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  381. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  382. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  383. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  384. package/templates/skills/gitflow/references/init-questions.md +185 -185
  385. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  386. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  387. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  388. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  389. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  390. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  391. package/templates/skills/gitflow/references/plan-template.md +69 -69
  392. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  393. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  394. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  395. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  396. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  397. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  398. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  399. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  400. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  401. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  402. package/templates/skills/gitflow/steps/step-init.md +190 -190
  403. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  404. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  405. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  406. package/templates/skills/gitflow/steps/step-start.md +195 -195
  407. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  408. package/templates/skills/gitflow/templates/config.json +72 -72
  409. package/templates/skills/mcp/SKILL.md +62 -62
  410. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  411. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  412. package/templates/skills/notification/SKILL.md +173 -173
  413. package/templates/skills/quick-search/SKILL.md +99 -99
  414. package/templates/skills/ralph-loop/SKILL.md +234 -234
  415. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  416. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  417. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  418. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  419. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  420. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  421. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  422. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  423. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  424. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  425. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  426. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  427. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  428. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  429. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  430. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  431. package/templates/skills/refactor/SKILL.md +56 -56
  432. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  433. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  434. package/templates/skills/review-code/SKILL.md +94 -94
  435. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  436. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  437. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  438. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  439. package/templates/skills/review-code/references/security-checklist.md +212 -212
  440. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  441. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  442. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  443. package/templates/skills/ui-components/SKILL.md +137 -137
  444. package/templates/skills/ui-components/accessibility.md +170 -170
  445. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  446. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  447. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  448. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  449. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  450. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  451. package/templates/skills/ui-components/style-guide.md +113 -113
  452. package/templates/skills/utils/SKILL.md +44 -44
  453. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  454. package/templates/skills/utils/subcommands/test-web.md +123 -123
  455. package/templates/skills/validate/SKILL.md +181 -181
  456. package/templates/skills/validate-feature/SKILL.md +101 -101
  457. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  458. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  459. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  460. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  461. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  462. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  463. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  464. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  465. package/templates/skills/workflow/SKILL.md +127 -127
  466. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  467. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  468. package/templates/test-web/api-health.json +38 -38
  469. package/templates/test-web/minimal.json +19 -19
  470. package/templates/test-web/npm-package.json +46 -46
  471. 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]` |