@nakedev/go-scaffold 0.4.0 → 0.5.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 (192) hide show
  1. package/README.md +598 -306
  2. package/dist/commands/auth.js +65 -23
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/config.js +50 -0
  5. package/dist/commands/create.js +33 -2
  6. package/dist/commands/generate.js +29 -3
  7. package/dist/commands/method.js +74 -63
  8. package/dist/commands/migration.js +2 -2
  9. package/dist/commands/observability.js +4 -53
  10. package/dist/commands/rbac.js +21 -10
  11. package/dist/commands/undo.js +11 -3
  12. package/dist/commands/worker.js +15 -5
  13. package/dist/index.js +198 -59
  14. package/dist/prompts/auth-wizard.js +40 -6
  15. package/dist/prompts/create-wizard.js +42 -1
  16. package/dist/prompts/generate-wizard.js +89 -9
  17. package/dist/templates/auth-manifest.js +50 -19
  18. package/dist/templates/create-manifest.js +8 -0
  19. package/dist/templates/module-manifest.js +84 -26
  20. package/dist/templates/rbac-manifest.js +16 -11
  21. package/dist/templates/worker-manifest.js +4 -1
  22. package/dist/types.js +8 -0
  23. package/dist/utils/auth-patcher.js +124 -33
  24. package/dist/utils/config.js +167 -4
  25. package/dist/utils/docs-patcher.js +68 -0
  26. package/dist/utils/hexagonal-method-patcher.js +334 -0
  27. package/dist/utils/main-patcher.js +32 -30
  28. package/dist/utils/marker-patch.js +7 -1
  29. package/dist/utils/module-location.js +17 -11
  30. package/dist/utils/module-profile.js +32 -0
  31. package/dist/utils/platform-patcher.js +56 -7
  32. package/dist/utils/rbac-patcher.js +89 -210
  33. package/package.json +7 -2
  34. package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
  35. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  36. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  37. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  38. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  39. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  40. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  41. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  42. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  43. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  44. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  45. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  46. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  47. package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
  48. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  49. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
  50. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
  51. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
  52. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
  53. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
  54. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
  55. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
  56. package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
  57. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  58. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  59. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
  60. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
  61. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  62. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  63. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  64. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
  65. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
  66. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
  67. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
  68. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
  69. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  70. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  71. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  72. package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
  73. package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
  74. package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
  75. package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
  76. package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
  77. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  78. package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
  79. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
  80. package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
  81. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  82. package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
  83. package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
  84. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  85. package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
  86. package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
  87. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  88. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  89. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  90. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  91. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  92. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  93. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
  94. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  95. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  96. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  97. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  98. package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
  99. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  100. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  101. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  102. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  103. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  104. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  105. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  106. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  107. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  108. package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
  109. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  110. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  111. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  112. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  113. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  114. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  115. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
  116. package/templates/create/base/.env.example.hbs +0 -1
  117. package/templates/create/base/.golangci.yml.hbs +2 -2
  118. package/templates/create/base/AGENTS.md.hbs +279 -67
  119. package/templates/create/base/Makefile.hbs +2 -1
  120. package/templates/create/base/README.md.hbs +115 -32
  121. package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
  122. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  123. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  124. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  125. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  126. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  127. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  128. package/templates/create/features/docs/architecture.md.hbs +92 -32
  129. package/templates/create/features/docs/patterns.md.hbs +137 -91
  130. package/templates/create/features/docs/techstack.md.hbs +18 -3
  131. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  132. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  133. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  134. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  135. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  136. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  137. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  138. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  139. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  140. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  141. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  142. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  143. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  144. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  145. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  146. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  147. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  148. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  149. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  150. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  151. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  152. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  153. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  154. package/templates/generate/module/migration.up.sql.hbs +1 -1
  155. package/dist/utils/method-patcher.js +0 -357
  156. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  157. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
  158. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
  159. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
  160. package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
  161. package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
  162. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
  163. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
  164. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  165. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
  166. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
  167. package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
  168. package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
  169. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
  170. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
  171. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
  172. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  173. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  174. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  175. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  176. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  177. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  178. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  179. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  180. package/templates/generate/module/dto.go.hbs +0 -36
  181. package/templates/generate/module/errors.go.hbs +0 -33
  182. package/templates/generate/module/handler.go.hbs +0 -134
  183. package/templates/generate/module/handler_test.go.hbs +0 -174
  184. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  185. package/templates/generate/module/minimal/handler.go.hbs +0 -48
  186. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  187. package/templates/generate/module/minimal/service.go.hbs +0 -45
  188. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  189. package/templates/generate/module/model/model.go.hbs +0 -36
  190. package/templates/generate/module/repository.go.hbs +0 -103
  191. package/templates/generate/module/service.go.hbs +0 -108
  192. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -0,0 +1,40 @@
1
+ post:
2
+ summary: Exchange a provider authorization code for a local session
3
+ description: The frontend callback route sends the code, the same state, and the PKCE verifier to the API. The API consumes a one-time server-side state transaction bound to the provider, S256 challenge, and OIDC nonce, uses its exact provider-registered redirect URI, validates the provider response, resolves the local identity, sets an HttpOnly refresh cookie, and returns an access token. The request has no redirect_uri, return_to, or frontend destination; credentials are never placed in a URI.
4
+ operationId: providerExchange
5
+ tags: [auth]
6
+ security: []
7
+ parameters:
8
+ - name: provider
9
+ in: path
10
+ required: true
11
+ schema: { type: string, example: google }
12
+ requestBody:
13
+ required: true
14
+ content:
15
+ application/json:
16
+ schema:
17
+ $ref: "./schemas.yaml#/OAuthExchangeInput"
18
+ responses:
19
+ "200":
20
+ description: local session created; refresh token is set only as an HttpOnly cookie
21
+ headers:
22
+ Set-Cookie:
23
+ description: HttpOnly refresh cookie
24
+ schema: { type: string }
25
+ Cache-Control:
26
+ description: token responses are never cacheable
27
+ schema: { type: string, example: no-store }
28
+ Pragma:
29
+ description: legacy cache prevention for token responses
30
+ schema: { type: string, example: no-cache }
31
+ content:
32
+ application/json:
33
+ schema:
34
+ oneOf:
35
+ - { $ref: "./schemas.yaml#/AuthResponse" }
36
+ - { $ref: "./schemas.yaml#/MFAChallengeResponse" }
37
+ "400":
38
+ description: controlled OAuth error (oauth_denied, oauth_state_invalid, or oauth_failed)
39
+ "503":
40
+ description: provider is unavailable or not configured
@@ -0,0 +1,31 @@
1
+ get:
2
+ summary: Start an external OAuth login flow
3
+ description: Redirects the browser to the configured provider. The browser client supplies state and an S256 PKCE challenge; the server creates a one-time transaction bound to that state, provider, challenge, and OIDC nonce, then uses the exact provider-registered redirect URI. It never accepts a client redirect URI.
4
+ operationId: providerLogin
5
+ tags: [auth]
6
+ security: []
7
+ parameters:
8
+ - name: provider
9
+ in: path
10
+ required: true
11
+ schema: { type: string, example: google }
12
+ - name: state
13
+ in: query
14
+ required: true
15
+ schema: { type: string, minLength: 1 }
16
+ - name: code_challenge
17
+ in: query
18
+ required: true
19
+ schema:
20
+ type: string
21
+ minLength: 43
22
+ maxLength: 128
23
+ pattern: '^[A-Za-z0-9._~-]+$'
24
+ - name: code_challenge_method
25
+ in: query
26
+ required: true
27
+ schema: { type: string, enum: [S256] }
28
+ responses:
29
+ "302": { description: redirect to the provider's authorization endpoint }
30
+ "400": { description: missing or invalid state/PKCE parameters; response uses a controlled OAuth error code }
31
+ "503": { description: provider is unavailable or not configured }
@@ -9,6 +9,13 @@ post:
9
9
  responses:
10
10
  "200":
11
11
  description: ok, a new refresh_token cookie is set
12
+ headers:
13
+ Cache-Control:
14
+ description: token responses are never cacheable
15
+ schema: { type: string, example: no-store }
16
+ Pragma:
17
+ description: legacy cache prevention for token responses
18
+ schema: { type: string, example: no-cache }
12
19
  content:
13
20
  application/json:
14
21
  schema: { $ref: './schemas.yaml#/AuthResponse' }
@@ -11,6 +11,13 @@ post:
11
11
  responses:
12
12
  "201":
13
13
  description: created, refresh_token set as an httpOnly cookie
14
+ headers:
15
+ Cache-Control:
16
+ description: token responses are never cacheable
17
+ schema: { type: string, example: no-store }
18
+ Pragma:
19
+ description: legacy cache prevention for token responses
20
+ schema: { type: string, example: no-cache }
14
21
  content:
15
22
  application/json:
16
23
  schema: { $ref: './schemas.yaml#/AuthResponse' }
@@ -1,6 +1,6 @@
1
1
  post:
2
2
  summary: Reset a password using a reset token
3
- description: The token is one-time use (consumed on success) and also revokes every existing refresh token for the account.
3
+ description: The token is consumed only after the password update and refresh-session revocation complete. If the session store is temporarily unavailable, the internal error response restores the token so the caller can retry the complete operation.
4
4
  operationId: resetPassword
5
5
  tags: [auth]
6
6
  security: []
@@ -32,8 +32,66 @@ VerifyEmailInput:
32
32
  properties:
33
33
  token: { type: string }
34
34
 
35
+ MFAChallengeInput:
36
+ type: object
37
+ required: [challenge, code]
38
+ additionalProperties: false
39
+ properties:
40
+ challenge: { type: string, minLength: 1 }
41
+ code: { type: string, minLength: 6, maxLength: 16 }
42
+
43
+ MFACodeInput:
44
+ type: object
45
+ required: [code]
46
+ additionalProperties: false
47
+ properties:
48
+ code: { type: string, minLength: 6, maxLength: 16 }
49
+
50
+ MFAChallengeResponse:
51
+ type: object
52
+ required: [mfa_required, challenge]
53
+ properties:
54
+ mfa_required: { type: boolean, example: true }
55
+ challenge: { type: string }
56
+
57
+ MFAStatus:
58
+ type: object
59
+ required: [available, enabled]
60
+ properties:
61
+ available: { type: boolean }
62
+ enabled: { type: boolean }
63
+
64
+ MFASetupResponse:
65
+ type: object
66
+ required: [secret, otpauth_uri]
67
+ properties:
68
+ secret: { type: string, description: show once and add to an authenticator app }
69
+ otpauth_uri: { type: string, format: uri }
70
+
71
+ MFAConfirmResponse:
72
+ type: object
73
+ required: [recovery_codes]
74
+ properties:
75
+ recovery_codes:
76
+ type: array
77
+ items: { type: string }
78
+
79
+ OAuthExchangeInput:
80
+ type: object
81
+ required: [code, state, code_verifier]
82
+ additionalProperties: false
83
+ properties:
84
+ code: { type: string, minLength: 1 }
85
+ state: { type: string, minLength: 1 }
86
+ code_verifier:
87
+ type: string
88
+ minLength: 43
89
+ maxLength: 128
90
+ pattern: '^[A-Za-z0-9._~-]+$'
91
+
35
92
  # access_token in the body; refresh_token is set as an httpOnly cookie, never
36
- # echoed back — see toCookieResponse in internal/app/user/dto.go.
93
+ # echoed back — see toCookieResponse in internal/app/user/dto.go. All token
94
+ # responses carry Cache-Control: no-store and Pragma: no-cache.
37
95
  AuthResponse:
38
96
  type: object
39
97
  properties:
@@ -0,0 +1,19 @@
1
+ post:
2
+ summary: Confirm MFA enrollment with a current TOTP code
3
+ operationId: confirmMyMFA
4
+ tags: [users]
5
+ security: [{ bearerAuth: [] }]
6
+ requestBody:
7
+ required: true
8
+ content:
9
+ application/json:
10
+ schema: { $ref: './schemas.yaml#/MFACodeInput' }
11
+ responses:
12
+ "200":
13
+ description: MFA enabled; recovery codes are returned once
14
+ content:
15
+ application/json:
16
+ schema: { $ref: './schemas.yaml#/MFAConfirmResponse' }
17
+ "400": { $ref: '../common/responses.yaml#/ValidationError' }
18
+ "401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
19
+ "409": { $ref: '../common/responses.yaml#/ConflictError' }
@@ -0,0 +1,15 @@
1
+ post:
2
+ summary: Disable MFA for the current user
3
+ operationId: disableMyMFA
4
+ tags: [users]
5
+ security: [{ bearerAuth: [] }]
6
+ requestBody:
7
+ required: true
8
+ content:
9
+ application/json:
10
+ schema: { $ref: './schemas.yaml#/MFACodeInput' }
11
+ responses:
12
+ "204": { description: MFA disabled }
13
+ "400": { $ref: '../common/responses.yaml#/ValidationError' }
14
+ "401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
15
+ "409": { $ref: '../common/responses.yaml#/ConflictError' }
@@ -0,0 +1,14 @@
1
+ post:
2
+ summary: Start MFA enrollment for the current user
3
+ operationId: setupMyMFA
4
+ tags: [users]
5
+ security: [{ bearerAuth: [] }]
6
+ responses:
7
+ "200":
8
+ description: secret and otpauth URI; enrollment remains disabled until confirmed
9
+ content:
10
+ application/json:
11
+ schema: { $ref: './schemas.yaml#/MFASetupResponse' }
12
+ "401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
13
+ "409": { $ref: '../common/responses.yaml#/ConflictError' }
14
+ "503": { description: MFA is disabled by the operator }
@@ -0,0 +1,12 @@
1
+ get:
2
+ summary: Get the current user's MFA status
3
+ operationId: getMyMFAStatus
4
+ tags: [users]
5
+ security: [{ bearerAuth: [] }]
6
+ responses:
7
+ "200":
8
+ description: MFA availability and enrollment status
9
+ content:
10
+ application/json:
11
+ schema: { $ref: './schemas.yaml#/MFAStatus' }
12
+ "401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
@@ -0,0 +1,98 @@
1
+ package httpadapter
2
+
3
+ import (
4
+ "fmt"
5
+ "net/http"
6
+ "net/url"
7
+ "strings"
8
+
9
+ "{{goModule}}/internal/shared/apperror"
10
+
11
+ "github.com/gin-gonic/gin"
12
+ )
13
+
14
+ // requireBrowserOrigin is the CSRF guard for the cross-site cookie mode. CORS
15
+ // controls whether a caller can read the response; this exact origin check
16
+ // independently controls whether a state-changing request may use an ambient
17
+ // refresh cookie.
18
+ func (h *Handler) requireBrowserOrigin(c *gin.Context) bool {
19
+ if !strings.EqualFold(strings.TrimSpace(h.cookieSameSite), "none") {
20
+ return true
21
+ }
22
+
23
+ origin := strings.TrimSpace(c.GetHeader("Origin"))
24
+ if origin == "" {
25
+ if referer := strings.TrimSpace(c.GetHeader("Referer")); referer != "" {
26
+ if parsed, err := url.Parse(referer); err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" && parsed.User == nil {
27
+ origin = parsed.Scheme + "://" + parsed.Host
28
+ }
29
+ }
30
+ }
31
+ if !browserOriginAllowed(origin, h.allowedOrigins) {
32
+ c.Error(apperror.New(http.StatusForbidden, "CSRF_ORIGIN_INVALID", "request origin is not allowed"))
33
+ c.Abort()
34
+ return false
35
+ }
36
+ return true
37
+ }
38
+
39
+ func browserOriginAllowed(origin string, allowed []string) bool {
40
+ origin = strings.TrimSpace(origin)
41
+ if _, ok := canonicalBrowserOrigin(origin); !ok {
42
+ return false
43
+ }
44
+ for _, candidate := range allowed {
45
+ candidate = strings.TrimSpace(candidate)
46
+ if _, candidateOK := canonicalBrowserOrigin(candidate); candidateOK && candidate == origin {
47
+ return true
48
+ }
49
+ }
50
+ return false
51
+ }
52
+
53
+ func canonicalBrowserOrigin(value string) (string, bool) {
54
+ parsed, err := url.Parse(strings.TrimSpace(value))
55
+ if err != nil || parsed.User != nil || parsed.Opaque != "" || parsed.Host == "" || parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" {
56
+ return "", false
57
+ }
58
+ if parsed.Scheme != "http" && parsed.Scheme != "https" {
59
+ return "", false
60
+ }
61
+ return parsed.Scheme + "://" + parsed.Host, true
62
+ }
63
+
64
+ // ValidateBrowserCookiePolicy ties deployment topology to the attributes used
65
+ // by /auth/refresh. Same-site is the safe local default; cross-site must opt
66
+ // into None + Secure, and production always requires Secure.
67
+ func ValidateBrowserCookiePolicy(topology, sameSite string, secure, production bool) error {
68
+ if production && !secure {
69
+ return fmt.Errorf("production browser cookies require COOKIE_SECURE=true")
70
+ }
71
+ switch strings.ToLower(strings.TrimSpace(topology)) {
72
+ case "same-origin", "same-site":
73
+ if strings.EqualFold(strings.TrimSpace(sameSite), "none") && !secure {
74
+ return fmt.Errorf("COOKIE_SAMESITE=none requires COOKIE_SECURE=true")
75
+ }
76
+ return nil
77
+ case "cross-site":
78
+ if !strings.EqualFold(strings.TrimSpace(sameSite), "none") || !secure {
79
+ return fmt.Errorf("cross-site browser topology requires COOKIE_SAMESITE=none and COOKIE_SECURE=true")
80
+ }
81
+ return nil
82
+ default:
83
+ return fmt.Errorf("unsupported browser topology %q", topology)
84
+ }
85
+ }
86
+
87
+ // sameSiteFrom maps COOKIE_SAMESITE to the net/http constant and defaults to
88
+ // the strictest option for anything unrecognised.
89
+ func sameSiteFrom(mode string) http.SameSite {
90
+ switch strings.ToLower(mode) {
91
+ case "none":
92
+ return http.SameSiteNoneMode
93
+ case "lax":
94
+ return http.SameSiteLaxMode
95
+ default:
96
+ return http.SameSiteStrictMode
97
+ }
98
+ }
@@ -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