@nakedev/go-scaffold 0.4.3 → 0.5.1

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 (164) hide show
  1. package/README.md +567 -513
  2. package/dist/commands/auth.js +12 -1
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/create.js +2 -1
  5. package/dist/commands/generate.js +5 -2
  6. package/dist/commands/method.js +66 -70
  7. package/dist/commands/observability.js +4 -53
  8. package/dist/commands/rbac.js +19 -8
  9. package/dist/commands/undo.js +6 -3
  10. package/dist/commands/worker.js +14 -4
  11. package/dist/index.js +13 -1
  12. package/dist/templates/auth-manifest.js +46 -45
  13. package/dist/templates/create-manifest.js +4 -0
  14. package/dist/templates/module-manifest.js +82 -60
  15. package/dist/templates/rbac-manifest.js +15 -11
  16. package/dist/templates/worker-manifest.js +4 -1
  17. package/dist/types.js +2 -0
  18. package/dist/utils/auth-patcher.js +22 -22
  19. package/dist/utils/config.js +24 -5
  20. package/dist/utils/docs-patcher.js +68 -0
  21. package/dist/utils/hexagonal-method-patcher.js +334 -0
  22. package/dist/utils/main-patcher.js +3 -3
  23. package/dist/utils/module-location.js +17 -11
  24. package/dist/utils/platform-patcher.js +27 -0
  25. package/dist/utils/rbac-patcher.js +73 -216
  26. package/package.json +1 -1
  27. package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
  28. package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
  29. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  30. package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
  31. package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
  32. package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
  33. package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
  34. package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
  35. package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
  36. package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
  37. package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
  38. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  39. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  40. package/templates/add/auth/internal/app/user/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
  41. package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
  42. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  43. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  44. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  45. package/templates/add/auth/internal/app/user/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
  46. package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
  47. package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
  48. package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
  49. package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
  50. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  51. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  52. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  53. package/templates/add/auth/internal/app/user/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
  54. package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
  55. package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
  56. package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
  57. package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
  58. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
  59. package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
  60. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
  61. package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
  62. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  63. package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
  64. package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
  65. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  66. package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
  67. package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
  68. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  69. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  70. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  71. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  72. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +1 -1
  73. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  74. package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
  75. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  76. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  77. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  78. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  79. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  80. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  81. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  82. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  83. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  84. package/templates/add/rbac/internal/app/role/composition.go.hbs +23 -10
  85. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  86. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  87. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  88. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  89. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  90. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  91. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +54 -36
  92. package/templates/create/base/.golangci.yml.hbs +2 -2
  93. package/templates/create/base/AGENTS.md.hbs +55 -31
  94. package/templates/create/base/README.md.hbs +77 -22
  95. package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
  96. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  97. package/templates/create/features/docs/architecture.md.hbs +64 -26
  98. package/templates/create/features/docs/patterns.md.hbs +117 -90
  99. package/templates/create/features/docs/techstack.md.hbs +17 -2
  100. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  101. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  102. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  103. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  104. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  105. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  106. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  107. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  108. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  109. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  110. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  111. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  112. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  113. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  114. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  115. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  116. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  117. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  118. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  119. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  120. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  121. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  122. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  123. package/templates/generate/module/migration.up.sql.hbs +1 -1
  124. package/dist/utils/method-patcher.js +0 -537
  125. package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
  126. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
  127. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
  128. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
  129. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
  130. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  131. package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
  132. package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
  133. package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
  134. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
  135. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
  136. package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
  137. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
  138. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  139. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  140. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  141. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  142. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  143. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  144. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  145. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  146. package/templates/generate/module/commands.go.hbs +0 -95
  147. package/templates/generate/module/composition.go.hbs +0 -23
  148. package/templates/generate/module/cqrs_test.go.hbs +0 -7
  149. package/templates/generate/module/dto.go.hbs +0 -36
  150. package/templates/generate/module/errors.go.hbs +0 -33
  151. package/templates/generate/module/handler.go.hbs +0 -179
  152. package/templates/generate/module/handler_test.go.hbs +0 -174
  153. package/templates/generate/module/minimal/commands.go.hbs +0 -34
  154. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  155. package/templates/generate/module/minimal/handler.go.hbs +0 -82
  156. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  157. package/templates/generate/module/minimal/queries.go.hbs +0 -45
  158. package/templates/generate/module/minimal/service.go.hbs +0 -71
  159. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  160. package/templates/generate/module/model/model.go.hbs +0 -36
  161. package/templates/generate/module/queries.go.hbs +0 -62
  162. package/templates/generate/module/repository.go.hbs +0 -103
  163. package/templates/generate/module/service.go.hbs +0 -164
  164. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -13,7 +13,6 @@ exports.patchUserServiceTestForRbac = patchUserServiceTestForRbac;
13
13
  exports.patchUserDTOForRbac = patchUserDTOForRbac;
14
14
  exports.patchUserHandlerForRbac = patchUserHandlerForRbac;
15
15
  exports.patchUserErrorsForRbac = patchUserErrorsForRbac;
16
- exports.userSvcLineFor = userSvcLineFor;
17
16
  exports.assertRbacPatchable = assertRbacPatchable;
18
17
  exports.patchMainGoForRbac = patchMainGoForRbac;
19
18
  exports.patchCmdSeedForRbac = patchCmdSeedForRbac;
@@ -23,13 +22,12 @@ const marker_patch_1 = require("./marker-patch");
23
22
  const IMPORT_MARKER = "// go-scaffold:imports";
24
23
  const SCHEMA_MARKER = "// go-scaffold:schemas";
25
24
  const MODEL_MARKER = "// go-scaffold:models";
26
- const ROUTE_MARKER = "// go-scaffold:routes";
27
25
  const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
28
26
  const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
29
27
  // patchAuthDocsForRbac adds `role` to the hand-written MeResponse schema in
30
28
  // docs/auth/schemas.yaml — same marker convention as the Go dto.go patch
31
29
  // (patchUserDTOForRbac), since GET/PATCH /users(/me) all serialize the same
32
- // model.User whether or not RBAC is installed.
30
+ // user response whether or not RBAC is installed.
33
31
  function patchAuthDocsForRbac(authSchemasYamlPath) {
34
32
  if (!fs_extra_1.default.existsSync(authSchemasYamlPath))
35
33
  return; // openapi docs feature disabled
@@ -46,15 +44,16 @@ function patchConfigForRbac(configGoPath) {
46
44
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, 'AuthzCacheTTL: time.Duration(envInt("AUTHZ_CACHE_TTL_MIN", 1)) * time.Minute,', "AuthzCacheTTL: time.Duration");
47
45
  fs_extra_1.default.writeFileSync(configGoPath, content);
48
46
  }
49
- // patchUserModelForRbac adds the Role column to model.User default 'staff'
50
- // at the DB level (via the gorm tag), so every existing user-creation path
51
- // (Register, Google, cmd/seed) gets a sane role without each one needing to
52
- // set it explicitly.
47
+ // patchUserModelForRbac verifies the auth template exposes the optional role
48
+ // field in its outbound persistence model. Auth owns the stable user shape;
49
+ // RBAC adds the role catalog and authorization policy on top of it. Keeping
50
+ // this check idempotent lets the command work across scaffold versions
51
+ // without recreating a second model package.
53
52
  function patchUserModelForRbac(userModelPath) {
54
53
  let content = fs_extra_1.default.readFileSync(userModelPath, "utf8");
55
- const field = `Role string \`json:"role" gorm:"type:varchar(20);not null;default:'staff'"\``;
56
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-fields", field, "Role string");
57
- fs_extra_1.default.writeFileSync(userModelPath, content);
54
+ if (/\bRole\s+string\b/.test(content))
55
+ return;
56
+ throw new Error(`${userModelPath} does not expose User.Role — the auth module must be regenerated or migrated before adding RBAC`);
58
57
  }
59
58
  // patchMiddlewareAuthForRbac adds RoleKey + the Role claim to the shared
60
59
  // middleware's own accessClaims copy (see auth.go's own comment on why it's
@@ -78,201 +77,78 @@ function patchUserJWTForRbac(jwtGoPath) {
78
77
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:jwt-claims-values", "Role: role,", "Role: role,");
79
78
  fs_extra_1.default.writeFileSync(jwtGoPath, content);
80
79
  }
81
- // patchUserServiceForRbac adds the RBAC behavior to user.Service. The base
82
- // auth constructor already exposes Dependencies.Roles, so enabling RBAC does
83
- // not mutate a positional constructor signature anymore.
84
- function patchUserServiceForRbac(serviceGoPath, goModule, sessionsGoPath) {
85
- let content = fs_extra_1.default.readFileSync(serviceGoPath, "utf8");
86
- const roleCheckerInterface = [
87
- "// RoleChecker is what the service needs from the role domain to validate a",
88
- "// role assignment declared consumer-side, same convention as repository/mailer.",
89
- "type RoleChecker interface {",
90
- "\tCodeExists(ctx context.Context, code string) (bool, error)",
91
- "}",
92
- ].join("\n");
93
- if (!content.includes("type RoleChecker interface") && !content.includes("type roleChecker interface")) {
94
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-interfaces", roleCheckerInterface, "type RoleChecker interface");
95
- }
96
- // issueTokens moved out of service.go with auth's use-case split. Keep the
97
- // patch compatible with both shapes: new projects patch sessions.go, while
98
- // older generated projects still have the marker in service.go.
99
- const tokenIssuePath = sessionsGoPath && fs_extra_1.default.existsSync(sessionsGoPath) ? sessionsGoPath : serviceGoPath;
100
- let tokenIssueContent = tokenIssuePath === serviceGoPath ? content : fs_extra_1.default.readFileSync(tokenIssuePath, "utf8");
101
- tokenIssueContent = (0, marker_patch_1.insertBeforeMarkerOnce)(tokenIssueContent, "// go-scaffold:issue-access-token-args", "u.Role,", "u.Role,");
102
- if (tokenIssuePath === serviceGoPath) {
103
- content = tokenIssueContent;
80
+ // patchUserServiceForRbac verifies the auth application already exposes the
81
+ // optional role capability. The canonical auth template owns this shared user
82
+ // behavior; `add rbac` only supplies the role catalog and authorizer.
83
+ function patchUserServiceForRbac(serviceGoPath, sessionsGoPath) {
84
+ const content = fs_extra_1.default.readFileSync(serviceGoPath, "utf8");
85
+ for (const required of ["roles ports.RoleChecker", "func (s *Service) SetRole(", "// go-scaffold:service-interface"]) {
86
+ if (!content.includes(required)) {
87
+ throw new Error(`${serviceGoPath} is missing the canonical auth role capability (${required}); regenerate auth before adding RBAC`);
88
+ }
104
89
  }
105
- // Auth keeps its base service focused and therefore does not need RBAC's
106
- // error/ORM imports until this optional method is patched in. Add them here
107
- // instead of carrying unused imports in every auth-only project. The module
108
- // argument is optional for callers that patch legacy projects whose service
109
- // already imported apperror; new CLI calls always provide it.
110
- if (goModule) {
111
- content = (0, marker_patch_1.ensureImport)(content, "errors");
112
- content = (0, marker_patch_1.ensureImport)(content, `${goModule}/internal/shared/apperror`);
113
- content = (0, marker_patch_1.ensureImport)(content, "gorm.io/gorm");
90
+ if (sessionsGoPath && fs_extra_1.default.existsSync(sessionsGoPath)) {
91
+ const sessions = fs_extra_1.default.readFileSync(sessionsGoPath, "utf8");
92
+ if (!sessions.includes("u.Role,")) {
93
+ throw new Error(`${sessionsGoPath} does not include the user's role in access-token issuance; regenerate auth before adding RBAC`);
94
+ }
114
95
  }
115
- const setRoleMethod = [
116
- "// SetRole assigns userID a new role — validated against the role catalog",
117
- "// (not just the DB's FK) so a typo'd code fails with a clear 422 here",
118
- "// instead of surfacing as an opaque FK-violation, and so AutoMigrate-only",
119
- "// test schemas (which never create the FK) still reject it correctly.",
120
- "func (s *Service) SetRole(ctx context.Context, userID uuid.UUID, roleCode string) (*model.User, error) {",
121
- "\tu, err := s.repo.FindByID(ctx, userID)",
122
- "\tif err != nil {",
123
- "\t\tif errors.Is(err, gorm.ErrRecordNotFound) {",
124
- "\t\t\treturn nil, errNotFound()",
125
- "\t\t}",
126
- "\t\treturn nil, apperror.NewInternal(err)",
127
- "\t}",
128
- "\texists, err := s.roles.CodeExists(ctx, roleCode)",
129
- "\tif err != nil {",
130
- "\t\treturn nil, apperror.NewInternal(err)",
131
- "\t}",
132
- "\tif !exists {",
133
- "\t\treturn nil, errUnknownRole()",
134
- "\t}",
135
- "\tu.Role = roleCode",
136
- "\tif err := s.repo.UpdateUser(ctx, u); err != nil {",
137
- "\t\treturn nil, apperror.NewInternal(err)",
138
- "\t}",
139
- "\treturn u, nil",
140
- "}",
141
- ].join("\n");
142
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-methods", setRoleMethod, "func (s *Service) SetRole(");
143
- fs_extra_1.default.writeFileSync(serviceGoPath, content);
144
- if (tokenIssuePath !== serviceGoPath)
145
- fs_extra_1.default.writeFileSync(tokenIssuePath, tokenIssueContent);
146
96
  }
147
- // patchUserServiceTestForRbac supplies a fake role capability through the
148
- // explicit Dependencies literal. The fake itself remains opt-in so auth-only
149
- // projects do not carry an unused RBAC test double.
97
+ // patchUserServiceTestForRbac verifies the canonical auth test seam already
98
+ // supplies the optional role capability. It is part of the auth contract so
99
+ // adding RBAC does not rewrite every test call site.
150
100
  function patchUserServiceTestForRbac(serviceTestGoPath) {
151
- let content = fs_extra_1.default.readFileSync(serviceTestGoPath, "utf8");
152
- const fakeRoles = [
153
- "// fakeRoles satisfies role's roleChecker interface structurally.",
154
- "type fakeRoles struct{}",
155
- "",
156
- "func (fakeRoles) CodeExists(context.Context, string) (bool, error) { return true, nil }",
157
- ].join("\n");
158
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-test-types", fakeRoles, "type fakeRoles struct{}");
159
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-test-deps", "Roles: fakeRoles{},", "Roles: fakeRoles{},");
160
- fs_extra_1.default.writeFileSync(serviceTestGoPath, content);
101
+ const content = fs_extra_1.default.readFileSync(serviceTestGoPath, "utf8");
102
+ for (const required of ["type fakeRoles struct{}", "Roles: fakeRoles{},", "// go-scaffold:user-service-test-deps"]) {
103
+ if (!content.includes(required)) {
104
+ throw new Error(`${serviceTestGoPath} is missing the canonical auth role test seam (${required}); regenerate auth before adding RBAC`);
105
+ }
106
+ }
161
107
  }
162
- // patchUserDTOForRbac adds the request DTO for PATCH /users/:id/set-role and
163
- // surfaces Role on the /me response both gated behind RBAC since
164
- // model.User has no Role field at all without it.
108
+ // patchUserDTOForRbac verifies the role DTOs owned by the canonical auth
109
+ // inbound adapter. The routes remain inactive until an Authz is composed.
165
110
  function patchUserDTOForRbac(dtoGoPath) {
166
- let content = fs_extra_1.default.readFileSync(dtoGoPath, "utf8");
167
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-me-fields", 'Role string `json:"role"`', "Role string");
168
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-me-values", "Role: u.Role,", "Role: u.Role,");
169
- const setRoleInput = ["type setRoleInput struct {", '\tRole string `json:"role" binding:"required"`', "}"].join("\n");
170
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-dto", setRoleInput, "type setRoleInput struct");
171
- fs_extra_1.default.writeFileSync(dtoGoPath, content);
172
- }
173
- // patchUserHandlerForRbac wires an *middleware.Authz into Handler and adds
174
- // the admin routes this PR ships: listing/viewing other users (PermUserRead)
175
- // and changing a user's role (PermUserManageRole). Suspending a user is a
176
- // separate concern, not RBAC's — see the role domain's own /roles,
177
- // /permissions for the actual role/permission management API.
178
- function patchUserHandlerForRbac(handlerGoPath, goModule) {
179
- let content = fs_extra_1.default.readFileSync(handlerGoPath, "utf8");
180
- // The base auth handler is intentionally only the route/composition surface;
181
- // its endpoint implementations now live in sibling files. RBAC still
182
- // patches its admin handlers into handler.go for marker compatibility, so
183
- // make the imports required by those injected handlers explicit here rather
184
- // than relying on imports that used to come from the old monolithic file.
185
- content = (0, marker_patch_1.ensureImport)(content, "net/http");
186
- content = (0, marker_patch_1.ensureImport)(content, `${goModule}/internal/shared/httpx`);
187
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-consts", 'const PermUserManageRole = "user:manage-role"', "PermUserManageRole");
188
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-consts", 'const PermUserRead = "user:read"', "PermUserRead");
189
- const paginationImport = `"${goModule}/internal/shared/pagination"`;
190
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-imports", paginationImport, "internal/shared/pagination");
191
- if (!/\bauthz\s+(?:\*middleware\.Authz|authorizer)\b/.test(content)) {
192
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-fields", "authz *middleware.Authz", "authz *middleware.Authz");
111
+ const content = fs_extra_1.default.readFileSync(dtoGoPath, "utf8");
112
+ if (!/\bRole\s+string\s+`json:"role"`/.test(content)) {
113
+ throw new Error(`${dtoGoPath} is missing the canonical auth role DTO; regenerate auth before adding RBAC`);
193
114
  }
194
- if (!/\bauthz\s+\.\.\.(?:\*middleware\.Authz|authorizer)\s*,/.test(content) && !/\bauthz\s+\*middleware\.Authz\s*,/.test(content)) {
195
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-params", "authz *middleware.Authz,", "authz *middleware.Authz,");
115
+ if (!/Role:\s+user\.Role,/.test(content)) {
116
+ throw new Error(`${dtoGoPath} is missing the canonical auth role DTO mapping; regenerate auth before adding RBAC`);
196
117
  }
197
- if (!/authz:\s+authzMiddleware,/.test(content) && !/authz:\s+authz,/.test(content)) {
198
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-init", "authz: authz,", "authz: authz,");
118
+ if (!content.includes("type setRoleInput struct")) {
119
+ throw new Error(`${dtoGoPath} is missing the canonical auth role DTO (type setRoleInput struct); regenerate auth before adding RBAC`);
199
120
  }
200
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-routes", 'usersGroup.GET("", h.authz.Require(PermUserRead), h.adminListUsers)', 'usersGroup.GET("", h.authz.Require(PermUserRead)');
201
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-routes", 'usersGroup.GET("/:id", h.authz.Require(PermUserRead), h.adminGetUser)', 'usersGroup.GET("/:id", h.authz.Require(PermUserRead)');
202
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-routes", 'usersGroup.PATCH("/:id/set-role", h.authz.Require(PermUserManageRole), h.setRole)', 'usersGroup.PATCH("/:id/set-role"');
203
- const adminListHandler = [
204
- "func (h *Handler) adminListUsers(c *gin.Context) {",
205
- "\tp := pagination.Parse(c)",
206
- "\titems, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)",
207
- "\tif err != nil {",
208
- "\t\tc.Error(err)",
209
- "\t\treturn",
210
- "\t}",
211
- "\tout := make([]meResponse, len(items))",
212
- "\tfor i := range items {",
213
- "\t\tout[i] = toMeResponse(&items[i])",
214
- "\t}",
215
- "\tc.JSON(http.StatusOK, p.Response(out))",
216
- "}",
217
- ].join("\n");
218
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", adminListHandler, "func (h *Handler) adminListUsers(");
219
- const adminGetHandler = [
220
- "func (h *Handler) adminGetUser(c *gin.Context) {",
221
- "\tid, ok := httpx.ParseID(c)",
222
- "\tif !ok {",
223
- "\t\treturn",
224
- "\t}",
225
- "\tu, err := h.svc.Get(c.Request.Context(), id)",
226
- "\tif err != nil {",
227
- "\t\tc.Error(err)",
228
- "\t\treturn",
229
- "\t}",
230
- "\tc.JSON(http.StatusOK, toMeResponse(u))",
231
- "}",
232
- ].join("\n");
233
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", adminGetHandler, "func (h *Handler) adminGetUser(");
234
- const setRoleHandler = [
235
- "func (h *Handler) setRole(c *gin.Context) {",
236
- "\tid, ok := httpx.ParseID(c)",
237
- "\tif !ok {",
238
- "\t\treturn",
239
- "\t}",
240
- "\tvar in setRoleInput",
241
- "\tif err := c.ShouldBindJSON(&in); err != nil {",
242
- "\t\tc.Error(httpx.BindErr(err))",
243
- "\t\treturn",
244
- "\t}",
245
- "\tu, err := h.svc.SetRole(c.Request.Context(), id, in.Role)",
246
- "\tif err != nil {",
247
- "\t\tc.Error(err)",
248
- "\t\treturn",
249
- "\t}",
250
- "\tc.JSON(http.StatusOK, toMeResponse(u))",
251
- "}",
252
- ].join("\n");
253
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", setRoleHandler, "func (h *Handler) setRole(");
254
- fs_extra_1.default.writeFileSync(handlerGoPath, content);
255
121
  }
256
- function patchUserErrorsForRbac(errorsGoPath) {
257
- let content = fs_extra_1.default.readFileSync(errorsGoPath, "utf8");
258
- const errFn = ["func errUnknownRole() *apperror.AppError {", '\treturn apperror.New(http.StatusUnprocessableEntity, "USER_UNKNOWN_ROLE", "unknown role code")', "}"].join("\n");
259
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-errors", errFn, "func errUnknownRole(");
260
- fs_extra_1.default.writeFileSync(errorsGoPath, content);
122
+ // patchUserHandlerForRbac verifies that auth owns the role-aware admin route
123
+ // surface. The routes are guarded by the optional authorizer, so adding RBAC
124
+ // composes the guard; it does not copy or rewrite handler code.
125
+ function patchUserHandlerForRbac(handlerGoPath) {
126
+ const content = fs_extra_1.default.readFileSync(handlerGoPath, "utf8");
127
+ for (const required of [
128
+ "authz authorizer",
129
+ "func (h *Handler) adminListUsers(",
130
+ "func (h *Handler) adminGetUser(",
131
+ "func (h *Handler) setRole(",
132
+ 'usersGroup.GET("", h.authz.Require(PermUserRead), h.adminListUsers)',
133
+ 'usersGroup.GET("/:id", h.authz.Require(PermUserRead), h.adminGetUser)',
134
+ 'usersGroup.PATCH("/:id/set-role", h.authz.Require(PermUserManageRole), h.setRole)',
135
+ ]) {
136
+ if (!content.includes(required)) {
137
+ throw new Error(`${handlerGoPath} is missing the canonical auth role route (${required}); regenerate auth before adding RBAC`);
138
+ }
139
+ }
261
140
  }
262
- // userSvcLineFor rebuilds the legacy line `add auth` wrote before auth got a
263
- // feature-local composition root. It remains here so `add rbac` can upgrade an
264
- // older generated project without pulling the old construction shape into new
265
- // output.
266
- function userSvcLineFor(goModule, store, worker) {
267
- const { tokenStore, mailer } = (0, auth_patcher_1.authWiringLines)({ goModule, queueBackend: "river", store, worker });
268
- return `userSvc := user.NewService(user.NewRepository(db), ${tokenStore}, ${mailer}, cfg, application.NewProviderRegistry())`;
141
+ function patchUserErrorsForRbac(errorsGoPath) {
142
+ const content = fs_extra_1.default.readFileSync(errorsGoPath, "utf8");
143
+ if (!content.includes("func errUnknownRole(")) {
144
+ throw new Error(`${errorsGoPath} is missing the canonical unknown-role error; regenerate auth before adding RBAC`);
145
+ }
269
146
  }
270
147
  function assertRbacPatchable(mainGoPath, goModule, store, worker) {
271
148
  const wiring = { goModule, queueBackend: "river", store, worker };
272
149
  const content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
273
150
  const expected = (0, auth_patcher_1.authHandlerLineFor)(wiring);
274
- const legacyRoute = `user.NewHandler(userSvc, cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, cfg.CookieSameSite, ${(0, auth_patcher_1.authWiringLines)(wiring).limiter}).Register(api)`;
275
- if (content.includes(expected) || content.includes(userSvcLineFor(goModule, store, worker)) || content.includes(legacyRoute))
151
+ if (content.includes(expected))
276
152
  return;
277
153
  throw new Error("cmd/api/wiring.go's auth route doesn't match what `add auth` wrote, so `add rbac` can't extend it.\n" +
278
154
  `Expected to find:\n ${expected}\n\n` +
@@ -282,7 +158,7 @@ function patchMainGoForRbac(mainGoPath, goModule, store, worker) {
282
158
  let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
283
159
  const importLine = `"${goModule}/internal/app/role"`;
284
160
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
285
- const modelImportLine = `rolemodel "${goModule}/internal/app/role/model"`;
161
+ const modelImportLine = `rolepostgres "${goModule}/internal/app/role/adapters/outbound/postgres"`;
286
162
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
287
163
  const schemaBlock = [
288
164
  'if err := db.Exec("CREATE SCHEMA IF NOT EXISTS role_svc").Error; err != nil {',
@@ -290,7 +166,7 @@ function patchMainGoForRbac(mainGoPath, goModule, store, worker) {
290
166
  "}",
291
167
  ].join("\n");
292
168
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaBlock, "CREATE SCHEMA IF NOT EXISTS role_svc");
293
- const migrateLines = ["&rolemodel.Role{},", "&rolemodel.Permission{},", "&rolemodel.RolePermission{},"];
169
+ const migrateLines = ["&rolepostgres.Role{},", "&rolepostgres.Permission{},", "&rolepostgres.RolePermission{},"];
294
170
  for (const line of migrateLines) {
295
171
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, line, line);
296
172
  }
@@ -298,32 +174,13 @@ function patchMainGoForRbac(mainGoPath, goModule, store, worker) {
298
174
  const authRouteLine = (0, auth_patcher_1.authHandlerLineFor)(wiring);
299
175
  const roleCompositionLine = "roleComposition := role.NewCompositionFromDB(db, cfg.JWTSecret, cfg.AuthzCacheTTL)";
300
176
  const roleHandlerLine = "roleComposition.Handler.Register(api)";
301
- if (content.includes(authRouteLine)) {
302
- const authWithRoleLine = (0, auth_patcher_1.authHandlerLineFor)(wiring, ["roleComposition.Service", "roleComposition.Authz"]);
303
- content = content.replace(authRouteLine, [roleCompositionLine, authWithRoleLine, roleHandlerLine].join("\n"));
304
- }
305
- else {
306
- // Upgrade projects generated before auth's local composition root. The
307
- // role feature is still constructed locally; only the legacy user service
308
- // call needs an adapter until that project is regenerated.
309
- const applicationImportLine = `"${goModule}/internal/app/user/application"`;
310
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, applicationImportLine, applicationImportLine);
311
- const userSvcLine = userSvcLineFor(goModule, store, worker);
312
- if (!content.includes(userSvcLine)) {
313
- throw new Error(`cmd/api/wiring.go's auth route doesn't match what \`add auth\` wrote, so \`add rbac\` can't extend it.\n` +
314
- `Expected to find:\n ${authRouteLine}\n\n` +
315
- "It was probably hand-edited. Restore that route and re-run — nothing has been changed yet.");
316
- }
317
- content = content.replace(userSvcLine, [roleCompositionLine, `${userSvcLine.slice(0, -1)}, roleComposition.Service)`].join("\n"));
318
- const limiter = (0, auth_patcher_1.authWiringLines)(wiring).limiter;
319
- const legacyRoute = `user.NewHandler(userSvc, cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, cfg.CookieSameSite, ${limiter}).Register(api)`;
320
- const tail = ").Register(api)";
321
- if (!content.includes(legacyRoute)) {
322
- throw new Error("cmd/api/wiring.go is missing auth's legacy handler route while adding RBAC");
323
- }
324
- content = content.replace(legacyRoute, `${legacyRoute.slice(0, -tail.length)}, roleComposition.Authz${tail}`);
325
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, roleHandlerLine, roleHandlerLine);
177
+ if (!content.includes(authRouteLine)) {
178
+ throw new Error(`cmd/api/wiring.go's auth route doesn't match what \`add auth\` wrote, so \`add rbac\` can't extend it.\n` +
179
+ `Expected to find:\n ${authRouteLine}\n\n` +
180
+ "It was probably hand-edited. Restore that route and re-run — nothing has been changed yet.");
326
181
  }
182
+ const authWithRoleLine = (0, auth_patcher_1.authHandlerLineFor)(wiring, ["roleComposition.Service", "roleComposition.Authz"]);
183
+ content = content.replace(authRouteLine, [roleCompositionLine, authWithRoleLine, roleHandlerLine].join("\n"));
327
184
  fs_extra_1.default.writeFileSync(mainGoPath, content);
328
185
  }
329
186
  // patchCmdSeedForRbac makes the seeded admin actually an admin: wires a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nakedev/go-scaffold",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "Scaffold Gin + GORM + Postgres Go backend projects with a consistent domain-module standard",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,6 +7,7 @@ import (
7
7
  "os"
8
8
 
9
9
  "{{goModule}}/internal/app/user"
10
+ userpassword "{{goModule}}/internal/app/user/adapters/outbound/password"
10
11
  "{{goModule}}/internal/platform/database"
11
12
  "{{goModule}}/internal/shared/config"
12
13
  // go-scaffold:seed-imports
@@ -32,6 +33,7 @@ func main() {
32
33
  // go-scaffold:seed-services
33
34
  svc := user.NewService(user.Dependencies{
34
35
  Repository: user.NewRepository(db),
36
+ Passwords: userpassword.NewBCryptHasher(),
35
37
  // go-scaffold:seed-user-service-args
36
38
  }, user.AuthConfig{
37
39
  JWTSecret: cfg.JWTSecret,
@@ -1,4 +1,4 @@
1
- package user
1
+ package httpadapter
2
2
 
3
3
  import (
4
4
  "fmt"
@@ -61,10 +61,10 @@ func canonicalBrowserOrigin(value string) (string, bool) {
61
61
  return parsed.Scheme + "://" + parsed.Host, true
62
62
  }
63
63
 
64
- // validateBrowserCookiePolicy ties deployment topology to the attributes used
64
+ // ValidateBrowserCookiePolicy ties deployment topology to the attributes used
65
65
  // by /auth/refresh. Same-site is the safe local default; cross-site must opt
66
66
  // into None + Secure, and production always requires Secure.
67
- func validateBrowserCookiePolicy(topology, sameSite string, secure, production bool) error {
67
+ func ValidateBrowserCookiePolicy(topology, sameSite string, secure, production bool) error {
68
68
  if production && !secure {
69
69
  return fmt.Errorf("production browser cookies require COOKIE_SECURE=true")
70
70
  }
@@ -0,0 +1,159 @@
1
+ package httpadapter
2
+
3
+ import (
4
+ "time"
5
+
6
+ userapp "{{goModule}}/internal/app/user/application"
7
+ userdomain "{{goModule}}/internal/app/user/domain"
8
+
9
+ "github.com/google/uuid"
10
+ )
11
+
12
+ // Request DTOs belong to the HTTP adapter. The application receives the
13
+ // transport-neutral inputs from handler mapping code.
14
+ type registerInput struct {
15
+ Email string `json:"email" binding:"required,email"`
16
+ Password string `json:"password" binding:"required,min=8"`
17
+ Name string `json:"name" binding:"required"`
18
+ }
19
+
20
+ type loginInput struct {
21
+ Email string `json:"email" binding:"required,email"`
22
+ Password string `json:"password" binding:"required"`
23
+ }
24
+
25
+ type loginExchangeInput struct {
26
+ Code string `json:"code"`
27
+ State string `json:"state"`
28
+ CodeVerifier string `json:"code_verifier"`
29
+ }
30
+
31
+ type loginStartInput struct {
32
+ State string
33
+ CodeChallenge string
34
+ CodeChallengeMethod string
35
+ }
36
+
37
+ func toRegisterInput(in registerInput) userapp.RegisterInput {
38
+ return userapp.RegisterInput{Email: in.Email, Password: in.Password, Name: in.Name}
39
+ }
40
+
41
+ func toLoginInput(in loginInput) userapp.LoginInput {
42
+ return userapp.LoginInput{Email: in.Email, Password: in.Password}
43
+ }
44
+
45
+ func toLoginStartInput(in loginStartInput) userapp.LoginStartInput {
46
+ return userapp.LoginStartInput{
47
+ State: in.State, CodeChallenge: in.CodeChallenge, CodeChallengeMethod: in.CodeChallengeMethod,
48
+ }
49
+ }
50
+
51
+ func toLoginExchangeInput(in loginExchangeInput) userapp.LoginExchangeInput {
52
+ return userapp.LoginExchangeInput{Code: in.Code, State: in.State, CodeVerifier: in.CodeVerifier}
53
+ }
54
+
55
+ type forgotPasswordInput struct {
56
+ Email string `json:"email" binding:"required,email"`
57
+ }
58
+
59
+ type resetPasswordInput struct {
60
+ Token string `json:"token" binding:"required"`
61
+ NewPassword string `json:"new_password" binding:"required,min=8"`
62
+ }
63
+
64
+ type verifyEmailInput struct {
65
+ Token string `json:"token" binding:"required"`
66
+ }
67
+
68
+ type mfaCodeInput struct {
69
+ Code string `json:"code" binding:"required"`
70
+ }
71
+
72
+ type mfaChallengeInput struct {
73
+ Challenge string `json:"challenge" binding:"required"`
74
+ Code string `json:"code" binding:"required"`
75
+ }
76
+
77
+ type authCookieResponse struct {
78
+ AccessToken string `json:"access_token"`
79
+ TokenType string `json:"token_type"`
80
+ ExpiresIn int `json:"expires_in"`
81
+ }
82
+
83
+ type mfaChallengeResponse struct {
84
+ MFARequired bool `json:"mfa_required"`
85
+ Challenge string `json:"challenge"`
86
+ }
87
+
88
+ type mfaStatusResponse struct {
89
+ Available bool `json:"available"`
90
+ Enabled bool `json:"enabled"`
91
+ }
92
+
93
+ type mfaSetupResponse struct {
94
+ Secret string `json:"secret"`
95
+ OTPAuthURI string `json:"otpauth_uri"`
96
+ }
97
+
98
+ type mfaConfirmResponse struct {
99
+ RecoveryCodes []string `json:"recovery_codes"`
100
+ }
101
+
102
+ func toCookieResponse(auth *userapp.AuthResponse) authCookieResponse {
103
+ return authCookieResponse{AccessToken: auth.AccessToken, TokenType: auth.TokenType, ExpiresIn: auth.ExpiresIn}
104
+ }
105
+
106
+ type meResponse struct {
107
+ ID uuid.UUID `json:"id"`
108
+ Email string `json:"email"`
109
+ Name string `json:"name"`
110
+ AvatarURL string `json:"avatar_url"`
111
+ EmailVerified bool `json:"email_verified"`
112
+ Role string `json:"role"`
113
+ // go-scaffold:user-me-fields
114
+ CreatedAt time.Time `json:"created_at"`
115
+ }
116
+
117
+ func toMeResponse(user *userdomain.User) meResponse {
118
+ return meResponse{
119
+ ID: user.ID,
120
+ Email: user.Email,
121
+ Name: user.Name,
122
+ AvatarURL: user.AvatarURL,
123
+ EmailVerified: user.EmailVerified,
124
+ CreatedAt: user.CreatedAt,
125
+ Role: user.Role,
126
+ // go-scaffold:user-me-values
127
+ }
128
+ }
129
+
130
+ // response is the HTTP representation used by generated methods. It is kept
131
+ // separate from application.Response so JSON naming remains an adapter detail.
132
+ //
133
+ //nolint:unused
134
+ type response struct {
135
+ ID uuid.UUID `json:"id"`
136
+ Email string `json:"email"`
137
+ Name string `json:"name"`
138
+ AvatarURL string `json:"avatar_url"`
139
+ EmailVerified bool `json:"email_verified"`
140
+ Role string `json:"role"`
141
+ CreatedAt time.Time `json:"created_at"`
142
+ UpdatedAt time.Time `json:"updated_at"`
143
+ }
144
+
145
+ //nolint:unused
146
+ func toResponse(out userapp.Response) response {
147
+ return response{
148
+ ID: out.ID, Email: out.Email, Name: out.Name, AvatarURL: out.AvatarURL,
149
+ EmailVerified: out.EmailVerified, Role: out.Role, CreatedAt: out.CreatedAt,
150
+ UpdatedAt: out.UpdatedAt,
151
+ }
152
+ }
153
+
154
+ type setRoleInput struct {
155
+ Role string `json:"role" binding:"required"`
156
+ }
157
+
158
+ // go-scaffold:dto
159
+ // go-scaffold:user-dto