@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
@@ -0,0 +1,62 @@
1
+ package application
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "{{goModule}}/internal/app/user/domain"
7
+ )
8
+
9
+ // Use-case errors carry a stable business code and a domain sentinel. The
10
+ // inbound adapter decides how those errors map to HTTP; application code does
11
+ // not know status codes or response envelopes.
12
+ func errNotFound() error {
13
+ return domain.Rule("USER_NOT_FOUND", "user not found", domain.ErrNotFound)
14
+ }
15
+
16
+ func errEmailTaken() error {
17
+ return domain.Rule("USER_EMAIL_TAKEN", "email already registered", domain.ErrEmailTaken)
18
+ }
19
+
20
+ func errTooManyAttempts() error {
21
+ return domain.Rule("AUTH_TOO_MANY_ATTEMPTS", "too many failed attempts - try again later", domain.ErrTooManyAttempts)
22
+ }
23
+
24
+ func errInvalidCredentials() error {
25
+ return domain.Rule("AUTH_INVALID_CREDENTIALS", "invalid email or password", domain.ErrInvalidCredential)
26
+ }
27
+
28
+ func errInvalidToken() error {
29
+ return domain.Rule("AUTH_INVALID_TOKEN", "invalid or expired token", domain.ErrInvalidToken)
30
+ }
31
+
32
+ func errAlreadyVerified() error {
33
+ return domain.Rule("AUTH_ALREADY_VERIFIED", "email is already verified", domain.ErrAlreadyVerified)
34
+ }
35
+
36
+ func errMFAUnavailable() error {
37
+ return domain.Rule("AUTH_MFA_UNAVAILABLE", "multi-factor authentication is not enabled", domain.ErrMFAUnavailable)
38
+ }
39
+
40
+ func errMFAInvalid() error {
41
+ return domain.Rule("AUTH_MFA_INVALID", "invalid or expired multi-factor authentication code", domain.ErrMFAInvalid)
42
+ }
43
+
44
+ func errMFAAlreadyEnabled() error {
45
+ return domain.Rule("AUTH_MFA_ALREADY_ENABLED", "multi-factor authentication is already enabled", domain.ErrMFAAlreadyEnabled)
46
+ }
47
+
48
+ func errMFANotEnrolled() error {
49
+ return domain.Rule("AUTH_MFA_NOT_ENROLLED", "multi-factor authentication is not enabled for this user", domain.ErrMFANotEnrolled)
50
+ }
51
+
52
+ func errMFASetupRequired() error {
53
+ return domain.Rule("AUTH_MFA_SETUP_REQUIRED", "complete multi-factor authentication setup first", domain.ErrMFASetupRequired)
54
+ }
55
+
56
+ func errMFAConfig() error {
57
+ return fmt.Errorf("multi-factor authentication encryption key is invalid")
58
+ }
59
+
60
+ func errUnknownRole() error {
61
+ return domain.Rule("USER_UNKNOWN_ROLE", "unknown role code", domain.ErrUnknownRole)
62
+ }
@@ -1,4 +1,4 @@
1
- package user
1
+ package application
2
2
 
3
3
  import (
4
4
  "context"
@@ -10,31 +10,27 @@ import (
10
10
  "strings"
11
11
  "time"
12
12
 
13
- "{{goModule}}/internal/app/user/application"
14
- "{{goModule}}/internal/app/user/model"
15
- "{{goModule}}/internal/shared/dberr"
13
+ "{{goModule}}/internal/app/user/domain"
16
14
  "{{goModule}}/internal/shared/id"
17
-
18
- "gorm.io/gorm"
19
15
  )
20
16
 
21
17
  // BeginLogin accepts the browser client's state and S256 PKCE challenge and
22
18
  // asks the registered provider adapter to construct its authorization URL.
23
19
  // The server persists a hashed state transaction binding provider, challenge,
24
20
  // and OIDC nonce before the callback reaches ExchangeLogin.
25
- func (s *Service) BeginLogin(ctx context.Context, providerName string, in application.LoginStartInput) (*application.Authorization, error) {
21
+ func (s *Service) BeginLogin(ctx context.Context, providerName string, in LoginStartInput) (*Authorization, error) {
26
22
  provider, ok := s.providers.Lookup(providerName)
27
23
  if !ok {
28
- return nil, application.NewOAuthError(application.OAuthProviderUnavailable, fmt.Errorf("provider %q is not configured", providerName))
24
+ return nil, NewOAuthError(OAuthProviderUnavailable, fmt.Errorf("provider %q is not configured", providerName))
29
25
  }
30
26
  // OAuth values are opaque: validate their wire-safe shape without trimming
31
27
  // or rewriting what the frontend and provider saw.
32
28
  if !validOAuthValue(in.State) || !validPKCEChallenge(in.CodeChallenge) || in.CodeChallengeMethod != "S256" {
33
- return nil, application.NewOAuthError(application.OAuthStateInvalid, fmt.Errorf("state and S256 code challenge are required"))
29
+ return nil, NewOAuthError(OAuthStateInvalid, fmt.Errorf("state and S256 code challenge are required"))
34
30
  }
35
31
  nonce, err := randomToken()
36
32
  if err != nil {
37
- return nil, application.NewOAuthError(application.OAuthProviderUnavailable, fmt.Errorf("create oauth transaction: %w", err))
33
+ return nil, NewOAuthError(OAuthProviderUnavailable, fmt.Errorf("create oauth transaction: %w", err))
38
34
  }
39
35
  in.Nonce = nonce
40
36
  authorization, err := provider.Begin(ctx, in)
@@ -42,59 +38,53 @@ func (s *Service) BeginLogin(ctx context.Context, providerName string, in applic
42
38
  return nil, mapProviderError(err)
43
39
  }
44
40
  if strings.TrimSpace(authorization.URL) == "" {
45
- return nil, application.NewOAuthError(application.OAuthProviderUnavailable, fmt.Errorf("provider returned an empty authorization URL"))
41
+ return nil, NewOAuthError(OAuthProviderUnavailable, fmt.Errorf("provider returned an empty authorization URL"))
46
42
  }
47
43
  clock := s.now
48
44
  if clock == nil {
49
45
  clock = time.Now
50
46
  }
51
- if err := s.oauthTransactions.SetLoginTransaction(ctx, hashToken(in.State), loginTransaction{
47
+ if err := s.oauthTransactions.SetLoginTransaction(ctx, hashToken(in.State), LoginTransaction{
52
48
  Provider: providerName,
53
49
  CodeChallenge: in.CodeChallenge,
54
50
  Nonce: nonce,
55
51
  ExpiresAt: clock().Add(s.config.OAuthStateTTL),
56
52
  }); err != nil {
57
- return nil, application.NewOAuthError(application.OAuthProviderUnavailable, fmt.Errorf("store oauth transaction: %w", err))
53
+ return nil, NewOAuthError(OAuthProviderUnavailable, fmt.Errorf("store oauth transaction: %w", err))
58
54
  }
59
55
  return &authorization, nil
60
56
  }
61
57
 
62
- type loginExchangeInput struct {
63
- Code string `json:"code"`
64
- State string `json:"state"`
65
- CodeVerifier string `json:"code_verifier"`
66
- }
67
-
68
- func (s *Service) ExchangeLogin(ctx context.Context, providerName string, in loginExchangeInput) (*authResult, error) {
58
+ func (s *Service) ExchangeLogin(ctx context.Context, providerName string, in LoginExchangeInput) (*AuthResult, error) {
69
59
  provider, ok := s.providers.Lookup(providerName)
70
60
  if !ok {
71
- return nil, application.NewOAuthError(application.OAuthProviderUnavailable, fmt.Errorf("provider %q is not configured", providerName))
61
+ return nil, NewOAuthError(OAuthProviderUnavailable, fmt.Errorf("provider %q is not configured", providerName))
72
62
  }
73
63
  if !validOAuthValue(in.State) || !validPKCEValue(in.CodeVerifier) {
74
- return nil, application.NewOAuthError(application.OAuthStateInvalid, fmt.Errorf("state and code verifier are required"))
64
+ return nil, NewOAuthError(OAuthStateInvalid, fmt.Errorf("state and code verifier are required"))
75
65
  }
76
66
  if !validOAuthValue(in.Code) {
77
- return nil, application.NewOAuthError(application.OAuthFailed, fmt.Errorf("authorization code is missing"))
67
+ return nil, NewOAuthError(OAuthFailed, fmt.Errorf("authorization code is missing"))
78
68
  }
79
69
 
80
70
  transaction, ok, err := s.oauthTransactions.ConsumeLoginTransaction(ctx, hashToken(in.State))
81
71
  if err != nil {
82
- return nil, application.NewOAuthError(application.OAuthProviderUnavailable, fmt.Errorf("consume oauth transaction: %w", err))
72
+ return nil, NewOAuthError(OAuthProviderUnavailable, fmt.Errorf("consume oauth transaction: %w", err))
83
73
  }
84
74
  if !ok || transaction.Provider != providerName || !validPKCEVerifier(in.CodeVerifier, transaction.CodeChallenge) {
85
- return nil, application.NewOAuthError(application.OAuthStateInvalid, fmt.Errorf("oauth state or PKCE verifier is invalid"))
75
+ return nil, NewOAuthError(OAuthStateInvalid, fmt.Errorf("oauth state or PKCE verifier is invalid"))
86
76
  }
87
77
 
88
- identity, err := provider.Complete(ctx, application.LoginCompleteInput{Code: in.Code, CodeVerifier: in.CodeVerifier, Nonce: transaction.Nonce})
78
+ identity, err := provider.Complete(ctx, LoginCompleteInput{Code: in.Code, CodeVerifier: in.CodeVerifier, Nonce: transaction.Nonce})
89
79
  if err != nil {
90
80
  return nil, mapProviderError(err)
91
81
  }
92
82
  if identity.Provider != providerName {
93
- return nil, application.NewOAuthError(application.OAuthFailed, fmt.Errorf("provider identity name does not match the requested provider"))
83
+ return nil, NewOAuthError(OAuthFailed, fmt.Errorf("provider identity name does not match the requested provider"))
94
84
  }
95
85
  u, err := s.findOrCreateExternalUser(ctx, identity)
96
86
  if err != nil {
97
- return nil, application.NewOAuthError(application.OAuthFailed, fmt.Errorf("resolve external identity: %w", err))
87
+ return nil, NewOAuthError(OAuthFailed, fmt.Errorf("resolve external identity: %w", err))
98
88
  }
99
89
  return s.completeLogin(ctx, u)
100
90
  }
@@ -150,20 +140,20 @@ func pkceChallenge(verifier string) string {
150
140
  }
151
141
 
152
142
  func mapProviderError(err error) error {
153
- var oauthErr *application.OAuthError
143
+ var oauthErr *OAuthError
154
144
  if errors.As(err, &oauthErr) {
155
145
  return oauthErr
156
146
  }
157
- if application.IsProviderUnavailable(err) {
158
- return application.NewOAuthError(application.OAuthProviderUnavailable, err)
147
+ if IsProviderUnavailable(err) {
148
+ return NewOAuthError(OAuthProviderUnavailable, err)
159
149
  }
160
- return application.NewOAuthError(application.OAuthFailed, err)
150
+ return NewOAuthError(OAuthFailed, err)
161
151
  }
162
152
 
163
153
  // findOrCreateExternalUser is provider-neutral identity resolution. A stable
164
154
  // provider subject wins; only a verified external email may link to an
165
155
  // existing account.
166
- func (s *Service) findOrCreateExternalUser(ctx context.Context, info application.ExternalIdentity) (*model.User, error) {
156
+ func (s *Service) findOrCreateExternalUser(ctx context.Context, info ExternalIdentity) (*domain.User, error) {
167
157
  if strings.TrimSpace(info.Provider) == "" || strings.TrimSpace(info.Subject) == "" || strings.TrimSpace(info.Email) == "" {
168
158
  return nil, fmt.Errorf("external identity is missing provider, subject, or email")
169
159
  }
@@ -171,14 +161,14 @@ func (s *Service) findOrCreateExternalUser(ctx context.Context, info application
171
161
  return nil, fmt.Errorf("external identity provider name is too long")
172
162
  }
173
163
 
174
- provider := model.Provider(info.Provider)
164
+ provider := domain.Provider(info.Provider)
175
165
  if ident, err := s.repo.FindIdentityByProviderUID(ctx, provider, info.Subject); err == nil {
176
166
  user, findErr := s.repo.FindByID(ctx, ident.UserID)
177
167
  if findErr != nil {
178
168
  return nil, fmt.Errorf("find user for existing identity: %w", findErr)
179
169
  }
180
170
  return user, nil
181
- } else if !errors.Is(err, gorm.ErrRecordNotFound) {
171
+ } else if !errors.Is(err, domain.ErrNotFound) {
182
172
  return nil, fmt.Errorf("find external identity: %w", err)
183
173
  }
184
174
 
@@ -186,20 +176,20 @@ func (s *Service) findOrCreateExternalUser(ctx context.Context, info application
186
176
  email := normalizeEmail(info.Email)
187
177
  if info.EmailVerified {
188
178
  if u, err := s.repo.FindByEmail(ctx, email); err == nil {
189
- ident := &model.Identity{ID: id.New(), UserID: u.ID, Provider: provider, ProviderUID: &providerUID}
179
+ ident := &domain.Identity{ID: id.New(), UserID: u.ID, Provider: provider, ProviderUID: &providerUID}
190
180
  if err := s.repo.CreateIdentity(ctx, ident); err != nil {
191
181
  return nil, fmt.Errorf("link external identity: %w", err)
192
182
  }
193
183
  return u, nil
194
- } else if !errors.Is(err, gorm.ErrRecordNotFound) {
184
+ } else if !errors.Is(err, domain.ErrNotFound) {
195
185
  return nil, fmt.Errorf("find user by verified external email: %w", err)
196
186
  }
197
187
  }
198
188
 
199
- u := &model.User{ID: id.New(), Email: email, Name: info.Name, AvatarURL: info.AvatarURL, EmailVerified: info.EmailVerified}
200
- ident := &model.Identity{ID: id.New(), Provider: provider, ProviderUID: &providerUID}
189
+ u := &domain.User{ID: id.New(), Email: email, Name: info.Name, AvatarURL: info.AvatarURL, EmailVerified: info.EmailVerified, Role: domain.DefaultRole}
190
+ ident := &domain.Identity{ID: id.New(), Provider: provider, ProviderUID: &providerUID}
201
191
  if err := s.repo.CreateUserWithIdentity(ctx, u, ident); err != nil {
202
- if dberr.IsDuplicate(err) {
192
+ if errors.Is(err, domain.ErrConflict) {
203
193
  return nil, errEmailTaken()
204
194
  }
205
195
  return nil, fmt.Errorf("create external user: %w", err)
@@ -1,4 +1,4 @@
1
- package user
1
+ package application
2
2
 
3
3
  import (
4
4
  "crypto/rand"
@@ -16,18 +16,21 @@ const (
16
16
  // accessClaims — see internal/shared/middleware/auth.go for why this struct
17
17
  // is duplicated there instead of imported.
18
18
  type accessClaims struct {
19
- Typ string `json:"typ"`
19
+ Typ string `json:"typ"`
20
+ Role string `json:"role,omitempty"`
20
21
  // go-scaffold:jwt-claims
21
22
  jwt.RegisteredClaims
22
23
  }
23
24
 
24
25
  func (s *Service) issueAccessToken(
25
26
  userID uuid.UUID,
27
+ role string,
26
28
  // go-scaffold:issue-access-token-params
27
29
  ) (string, error) {
28
30
  now := s.clock()
29
31
  claims := accessClaims{
30
- Typ: tokenTypeAccess,
32
+ Typ: tokenTypeAccess,
33
+ Role: role,
31
34
  // go-scaffold:jwt-claims-values
32
35
  RegisteredClaims: jwt.RegisteredClaims{
33
36
  Subject: userID.String(),
@@ -1,16 +1,14 @@
1
- package user
1
+ package application
2
2
 
3
3
  import (
4
4
  "context"
5
+ "errors"
6
+ "fmt"
5
7
  "log/slog"
6
8
  "time"
7
9
 
8
- "{{goModule}}/internal/app/user/model"
9
- "{{goModule}}/internal/shared/apperror"
10
- "{{goModule}}/internal/shared/dberr"
10
+ "{{goModule}}/internal/app/user/domain"
11
11
  "{{goModule}}/internal/shared/id"
12
-
13
- "golang.org/x/crypto/bcrypt"
14
12
  )
15
13
 
16
14
  // Failed-attempt policy. These values are security posture rather than
@@ -36,23 +34,23 @@ func (s *Service) throttled(ctx context.Context, key string) bool {
36
34
  slog.Error("read login throttle", "error", err)
37
35
  return false
38
36
  }
39
- return !until.IsZero() && time.Now().Before(until)
37
+ return !until.IsZero() && s.clock().Before(until)
40
38
  }
41
39
 
42
- func (s *Service) Register(ctx context.Context, in registerInput) (*authResult, error) {
43
- hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
40
+ func (s *Service) Register(ctx context.Context, in RegisterInput) (*AuthResult, error) {
41
+ hash, err := s.passwords.Hash(in.Password)
44
42
  if err != nil {
45
- return nil, apperror.NewInternal(err)
43
+ return nil, fmt.Errorf("hash password: %w", err)
46
44
  }
47
45
  hashStr := string(hash)
48
46
 
49
- u := &model.User{ID: id.New(), Email: normalizeEmail(in.Email), Name: in.Name}
50
- i := &model.Identity{ID: id.New(), Provider: model.ProviderLocal, PasswordHash: &hashStr}
47
+ u := &domain.User{ID: id.New(), Email: normalizeEmail(in.Email), Name: in.Name, Role: domain.DefaultRole}
48
+ i := &domain.Identity{ID: id.New(), Provider: domain.ProviderLocal, PasswordHash: &hashStr}
51
49
  if err := s.repo.CreateUserWithIdentity(ctx, u, i); err != nil {
52
- if dberr.IsDuplicate(err) {
50
+ if errors.Is(err, domain.ErrConflict) {
53
51
  return nil, errEmailTaken()
54
52
  }
55
- return nil, apperror.NewInternal(err)
53
+ return nil, fmt.Errorf("create user: %w", err)
56
54
  }
57
55
  // Best-effort: a mail failure should not block registration. The user can
58
56
  // request another link through ResendVerificationEmail.
@@ -61,10 +59,10 @@ func (s *Service) Register(ctx context.Context, in registerInput) (*authResult,
61
59
  if err != nil {
62
60
  return nil, err
63
61
  }
64
- return tokenResult(auth), nil
62
+ return TokenResult(auth), nil
65
63
  }
66
64
 
67
- func (s *Service) Login(ctx context.Context, in loginInput) (*authResult, error) {
65
+ func (s *Service) Login(ctx context.Context, in LoginInput) (*AuthResult, error) {
68
66
  key := throttleKey("login", in.Email)
69
67
  if s.throttled(ctx, key) {
70
68
  return nil, errTooManyAttempts()
@@ -72,7 +70,7 @@ func (s *Service) Login(ctx context.Context, in loginInput) (*authResult, error)
72
70
 
73
71
  // Every failure records the same key and returns the same error whether or
74
72
  // not the account exists, preventing account enumeration through the counter.
75
- fail := func() (*authResult, error) {
73
+ fail := func() (*AuthResult, error) {
76
74
  if err := s.repo.RecordLoginFailure(ctx, key, loginFreeAttempts, loginMaxLock); err != nil {
77
75
  slog.Error("record login failure", "error", err)
78
76
  }
@@ -83,11 +81,11 @@ func (s *Service) Login(ctx context.Context, in loginInput) (*authResult, error)
83
81
  if err != nil {
84
82
  return fail()
85
83
  }
86
- ident, err := s.repo.FindIdentity(ctx, u.ID, model.ProviderLocal)
84
+ ident, err := s.repo.FindIdentity(ctx, u.ID, domain.ProviderLocal)
87
85
  if err != nil || ident.PasswordHash == nil {
88
86
  return fail()
89
87
  }
90
- if err := bcrypt.CompareHashAndPassword([]byte(*ident.PasswordHash), []byte(in.Password)); err != nil {
88
+ if err := s.passwords.Compare(*ident.PasswordHash, in.Password); err != nil {
91
89
  return fail()
92
90
  }
93
91
 
@@ -1,4 +1,4 @@
1
- package user
1
+ package application
2
2
 
3
3
  import (
4
4
  "context"
@@ -18,8 +18,7 @@ import (
18
18
  "strings"
19
19
  "time"
20
20
 
21
- "{{goModule}}/internal/app/user/model"
22
- "{{goModule}}/internal/shared/apperror"
21
+ "{{goModule}}/internal/app/user/domain"
23
22
 
24
23
  "github.com/google/uuid"
25
24
  )
@@ -87,41 +86,41 @@ func ValidateMFASettings(settings MFASettings) error {
87
86
  return nil
88
87
  }
89
88
 
90
- func (s *Service) completeLogin(ctx context.Context, u *model.User) (*authResult, error) {
89
+ func (s *Service) completeLogin(ctx context.Context, u *domain.User) (*AuthResult, error) {
91
90
  settings := s.mfaSettings()
92
91
  if !settings.Enabled {
93
92
  auth, err := s.issueTokens(ctx, u)
94
93
  if err != nil {
95
94
  return nil, err
96
95
  }
97
- return tokenResult(auth), nil
96
+ return TokenResult(auth), nil
98
97
  }
99
98
  if s.mfa == nil {
100
- return nil, apperror.NewInternal(errors.New("MFA is enabled but no MFA store was configured"))
99
+ return nil, errors.New("MFA is enabled but no MFA store was configured")
101
100
  }
102
101
  enrollment, found, err := s.mfa.GetEnrollment(ctx, u.ID)
103
102
  if err != nil {
104
- return nil, apperror.NewInternal(fmt.Errorf("read MFA enrollment: %w", err))
103
+ return nil, fmt.Errorf("read MFA enrollment: %w", err)
105
104
  }
106
105
  if !found || !enrollment.Enabled {
107
106
  auth, err := s.issueTokens(ctx, u)
108
107
  if err != nil {
109
108
  return nil, err
110
109
  }
111
- return tokenResult(auth), nil
110
+ return TokenResult(auth), nil
112
111
  }
113
112
 
114
113
  rawChallenge, err := randomToken()
115
114
  if err != nil {
116
- return nil, apperror.NewInternal(fmt.Errorf("create MFA challenge: %w", err))
115
+ return nil, fmt.Errorf("create MFA challenge: %w", err)
117
116
  }
118
117
  if err := s.mfa.CreateChallenge(ctx, hashToken(rawChallenge), MFAChallenge{
119
118
  UserID: u.ID,
120
119
  ExpiresAt: s.clock().Add(settings.ChallengeTTL),
121
120
  }); err != nil {
122
- return nil, apperror.NewInternal(fmt.Errorf("store MFA challenge: %w", err))
121
+ return nil, fmt.Errorf("store MFA challenge: %w", err)
123
122
  }
124
- return challengeResult(rawChallenge), nil
123
+ return ChallengeResult(rawChallenge), nil
125
124
  }
126
125
 
127
126
  func (s *Service) MFAStatus(ctx context.Context, userID uuid.UUID) (MFAStatus, error) {
@@ -131,7 +130,7 @@ func (s *Service) MFAStatus(ctx context.Context, userID uuid.UUID) (MFAStatus, e
131
130
  }
132
131
  enrollment, found, err := s.mfa.GetEnrollment(ctx, userID)
133
132
  if err != nil {
134
- return MFAStatus{}, apperror.NewInternal(fmt.Errorf("read MFA enrollment: %w", err))
133
+ return MFAStatus{}, fmt.Errorf("read MFA enrollment: %w", err)
135
134
  }
136
135
  return MFAStatus{Available: true, Enabled: found && enrollment.Enabled}, nil
137
136
  }
@@ -142,11 +141,11 @@ func (s *Service) SetupMFA(ctx context.Context, userID uuid.UUID) (*MFASetup, er
142
141
  return nil, err
143
142
  }
144
143
  if s.mfa == nil {
145
- return nil, apperror.NewInternal(errors.New("MFA store is not configured"))
144
+ return nil, errors.New("MFA store is not configured")
146
145
  }
147
146
  enrollment, found, err := s.mfa.GetEnrollment(ctx, userID)
148
147
  if err != nil {
149
- return nil, apperror.NewInternal(fmt.Errorf("read MFA enrollment: %w", err))
148
+ return nil, fmt.Errorf("read MFA enrollment: %w", err)
150
149
  }
151
150
  if found && enrollment.Enabled {
152
151
  return nil, errMFAAlreadyEnabled()
@@ -157,14 +156,14 @@ func (s *Service) SetupMFA(ctx context.Context, userID uuid.UUID) (*MFASetup, er
157
156
  }
158
157
  secret, err := newTOTPSecret()
159
158
  if err != nil {
160
- return nil, apperror.NewInternal(fmt.Errorf("generate MFA secret: %w", err))
159
+ return nil, fmt.Errorf("generate MFA secret: %w", err)
161
160
  }
162
161
  encrypted, err := encryptMFASecret(settings.EncryptionKey, secret)
163
162
  if err != nil {
164
163
  return nil, errMFAConfig()
165
164
  }
166
165
  if err := s.mfa.PutPendingEnrollment(ctx, userID, encrypted); err != nil {
167
- return nil, apperror.NewInternal(fmt.Errorf("store MFA enrollment: %w", err))
166
+ return nil, fmt.Errorf("store MFA enrollment: %w", err)
168
167
  }
169
168
  return &MFASetup{
170
169
  Secret: secret,
@@ -178,11 +177,11 @@ func (s *Service) ConfirmMFA(ctx context.Context, userID uuid.UUID, code string)
178
177
  return nil, err
179
178
  }
180
179
  if s.mfa == nil {
181
- return nil, apperror.NewInternal(errors.New("MFA store is not configured"))
180
+ return nil, errors.New("MFA store is not configured")
182
181
  }
183
182
  enrollment, found, err := s.mfa.GetEnrollment(ctx, userID)
184
183
  if err != nil {
185
- return nil, apperror.NewInternal(fmt.Errorf("read MFA enrollment: %w", err))
184
+ return nil, fmt.Errorf("read MFA enrollment: %w", err)
186
185
  }
187
186
  if !found {
188
187
  return nil, errMFASetupRequired()
@@ -200,10 +199,10 @@ func (s *Service) ConfirmMFA(ctx context.Context, userID uuid.UUID, code string)
200
199
 
201
200
  codes, hashes, err := newRecoveryCodes(settings.RecoveryCodeCount)
202
201
  if err != nil {
203
- return nil, apperror.NewInternal(fmt.Errorf("generate MFA recovery codes: %w", err))
202
+ return nil, fmt.Errorf("generate MFA recovery codes: %w", err)
204
203
  }
205
204
  if err := s.mfa.ConfirmEnrollment(ctx, userID, enrollment.EncryptedSecret, hashes); err != nil {
206
- return nil, apperror.NewInternal(fmt.Errorf("confirm MFA enrollment: %w", err))
205
+ return nil, fmt.Errorf("confirm MFA enrollment: %w", err)
207
206
  }
208
207
  return codes, nil
209
208
  }
@@ -217,11 +216,11 @@ func (s *Service) DisableMFA(ctx context.Context, userID uuid.UUID, code string)
217
216
  return err
218
217
  }
219
218
  if s.mfa == nil {
220
- return apperror.NewInternal(errors.New("MFA store is not configured"))
219
+ return errors.New("MFA store is not configured")
221
220
  }
222
221
  enrollment, found, err := s.mfa.GetEnrollment(ctx, userID)
223
222
  if err != nil {
224
- return apperror.NewInternal(fmt.Errorf("read MFA enrollment: %w", err))
223
+ return fmt.Errorf("read MFA enrollment: %w", err)
225
224
  }
226
225
  if !found || !enrollment.Enabled {
227
226
  return errMFANotEnrolled()
@@ -234,7 +233,7 @@ func (s *Service) DisableMFA(ctx context.Context, userID uuid.UUID, code string)
234
233
  return errMFAInvalid()
235
234
  }
236
235
  if err := s.mfa.Disable(ctx, userID); err != nil {
237
- return apperror.NewInternal(fmt.Errorf("disable MFA: %w", err))
236
+ return fmt.Errorf("disable MFA: %w", err)
238
237
  }
239
238
  return nil
240
239
  }
@@ -242,24 +241,24 @@ func (s *Service) DisableMFA(ctx context.Context, userID uuid.UUID, code string)
242
241
  // VerifyMFA consumes the challenge before checking the code. This makes every
243
242
  // challenge single-use, including a failed attempt, and prevents an attacker
244
243
  // from turning a leaked challenge into an unlimited six-digit guessing oracle.
245
- func (s *Service) VerifyMFA(ctx context.Context, rawChallenge, code string) (*authResponse, error) {
244
+ func (s *Service) VerifyMFA(ctx context.Context, rawChallenge, code string) (*AuthResponse, error) {
246
245
  settings, err := s.requireMFA()
247
246
  if err != nil {
248
247
  return nil, err
249
248
  }
250
249
  if s.mfa == nil {
251
- return nil, apperror.NewInternal(errors.New("MFA store is not configured"))
250
+ return nil, errors.New("MFA store is not configured")
252
251
  }
253
252
  challenge, found, err := s.mfa.ConsumeChallenge(ctx, hashToken(rawChallenge))
254
253
  if err != nil {
255
- return nil, apperror.NewInternal(fmt.Errorf("consume MFA challenge: %w", err))
254
+ return nil, fmt.Errorf("consume MFA challenge: %w", err)
256
255
  }
257
256
  if !found {
258
257
  return nil, errMFAInvalid()
259
258
  }
260
259
  enrollment, found, err := s.mfa.GetEnrollment(ctx, challenge.UserID)
261
260
  if err != nil {
262
- return nil, apperror.NewInternal(fmt.Errorf("read MFA enrollment: %w", err))
261
+ return nil, fmt.Errorf("read MFA enrollment: %w", err)
263
262
  }
264
263
  if !found || !enrollment.Enabled {
265
264
  return nil, errMFAInvalid()
@@ -275,7 +274,7 @@ func (s *Service) VerifyMFA(ctx context.Context, rawChallenge, code string) (*au
275
274
  }
276
275
  used, err := s.mfa.ConsumeRecoveryCode(ctx, challenge.UserID, recoveryHash)
277
276
  if err != nil {
278
- return nil, apperror.NewInternal(fmt.Errorf("consume MFA recovery code: %w", err))
277
+ return nil, fmt.Errorf("consume MFA recovery code: %w", err)
279
278
  }
280
279
  if !used {
281
280
  return nil, errMFAInvalid()
@@ -1,13 +1,13 @@
1
- package user
1
+ package application
2
2
 
3
3
  import (
4
4
  "context"
5
+ "errors"
5
6
  "strings"
6
7
  "testing"
7
8
  "time"
8
9
 
9
- "{{goModule}}/internal/app/user/model"
10
- "{{goModule}}/internal/shared/apperror"
10
+ "{{goModule}}/internal/app/user/domain"
11
11
 
12
12
  "github.com/google/uuid"
13
13
  )
@@ -18,7 +18,7 @@ func configuredMFATestService(t *testing.T) (*Service, *fakeMFAStore, uuid.UUID,
18
18
  t.Helper()
19
19
  userID := uuid.New()
20
20
  mfa := newFakeMFAStore()
21
- svc := newTestService(&fakeRepo{user: &model.User{ID: userID, Email: "mfa@example.com"}}, newFakeTokenStore())
21
+ svc := newTestService(&fakeRepo{user: &domain.User{ID: userID, Email: "mfa@example.com"}}, newFakeTokenStore())
22
22
  now := time.Now()
23
23
  svc.mfa = mfa
24
24
  svc.now = func() time.Time { return now }
@@ -46,7 +46,7 @@ func TestTOTPCodeMatchesRFC6238SHA1Vector(t *testing.T) {
46
46
 
47
47
  func TestMFADefaultIsDisabled(t *testing.T) {
48
48
  userID := uuid.New()
49
- svc := newTestService(&fakeRepo{user: &model.User{ID: userID, Email: "default@example.com"}}, newFakeTokenStore())
49
+ svc := newTestService(&fakeRepo{user: &domain.User{ID: userID, Email: "default@example.com"}}, newFakeTokenStore())
50
50
 
51
51
  status, err := svc.MFAStatus(context.Background(), userID)
52
52
  if err != nil {
@@ -147,8 +147,8 @@ func TestMFAChallengeIsRequiredAndOneUse(t *testing.T) {
147
147
  t.Fatalf("confirm: %v", err)
148
148
  }
149
149
 
150
- result, err := svc.completeLogin(ctx, &model.User{ID: userID, Email: "mfa@example.com"})
151
- if err != nil || result.MFAChallenge == "" || result.authResponse != nil {
150
+ result, err := svc.completeLogin(ctx, &domain.User{ID: userID, Email: "mfa@example.com"})
151
+ if err != nil || result.MFAChallenge == "" || result.AuthResponse != nil {
152
152
  t.Fatalf("expected pre-session MFA challenge: %+v, %v", result, err)
153
153
  }
154
154
  auth, err := svc.VerifyMFA(ctx, result.MFAChallenge, code)
@@ -172,14 +172,14 @@ func TestMFARecoveryCodeIsOneUse(t *testing.T) {
172
172
  if err != nil {
173
173
  t.Fatalf("confirm: %v", err)
174
174
  }
175
- result, err := svc.completeLogin(ctx, &model.User{ID: userID, Email: "mfa@example.com"})
175
+ result, err := svc.completeLogin(ctx, &domain.User{ID: userID, Email: "mfa@example.com"})
176
176
  if err != nil {
177
177
  t.Fatalf("login challenge: %v", err)
178
178
  }
179
179
  if _, err := svc.VerifyMFA(ctx, result.MFAChallenge, recoveryCodes[0]); err != nil {
180
180
  t.Fatalf("verify with recovery code: %v", err)
181
181
  }
182
- result, err = svc.completeLogin(ctx, &model.User{ID: userID, Email: "mfa@example.com"})
182
+ result, err = svc.completeLogin(ctx, &domain.User{ID: userID, Email: "mfa@example.com"})
183
183
  if err != nil {
184
184
  t.Fatalf("second login challenge: %v", err)
185
185
  }
@@ -192,8 +192,9 @@ func codeOfMFAError(err error) string {
192
192
  if err == nil {
193
193
  return ""
194
194
  }
195
- if appErr, ok := err.(*apperror.AppError); ok {
196
- return appErr.Code
195
+ var ruleErr *domain.RuleError
196
+ if errors.As(err, &ruleErr) {
197
+ return ruleErr.Code
197
198
  }
198
199
  return ""
199
200
  }
@@ -74,10 +74,10 @@ func (r ProviderRegistry) Lookup(name string) (LoginProvider, bool) {
74
74
  type OAuthErrorCode string
75
75
 
76
76
  const (
77
- OAuthDenied OAuthErrorCode = "oauth_denied"
78
- OAuthStateInvalid OAuthErrorCode = "oauth_state_invalid"
77
+ OAuthDenied OAuthErrorCode = "oauth_denied"
78
+ OAuthStateInvalid OAuthErrorCode = "oauth_state_invalid"
79
79
  OAuthProviderUnavailable OAuthErrorCode = "oauth_provider_unavailable"
80
- OAuthFailed OAuthErrorCode = "oauth_failed"
80
+ OAuthFailed OAuthErrorCode = "oauth_failed"
81
81
  )
82
82
 
83
83
  // OAuthError is the public, controlled error contract for the browser login