@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,112 @@
1
+ package application
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "log/slog"
8
+
9
+ "{{goModule}}/internal/app/user/domain"
10
+
11
+ "github.com/google/uuid"
12
+ )
13
+
14
+ // ForgotPassword always returns success from the caller's perspective. Only a
15
+ // local password identity gets a token, so this endpoint cannot enumerate
16
+ // accounts or issue a reset for an external-only account.
17
+ func (s *Service) ForgotPassword(ctx context.Context, email string) error {
18
+ key := throttleKey("pwreset", email)
19
+ if s.throttled(ctx, key) {
20
+ return nil
21
+ }
22
+ if err := s.repo.RecordLoginFailure(ctx, key, loginFreeAttempts, loginMaxLock); err != nil {
23
+ slog.Error("record password reset attempt", "error", err)
24
+ }
25
+
26
+ u, err := s.repo.FindByEmail(ctx, normalizeEmail(email))
27
+ if err != nil {
28
+ return nil
29
+ }
30
+ ident, err := s.repo.FindIdentity(ctx, u.ID, domain.ProviderLocal)
31
+ if err != nil || ident.PasswordHash == nil {
32
+ return nil
33
+ }
34
+
35
+ raw, err := randomToken()
36
+ if err != nil {
37
+ return fmt.Errorf("generate password reset token: %w", err)
38
+ }
39
+ if err := s.recoveryTokens.SetPasswordResetToken(ctx, hashToken(raw), u.ID, s.config.PasswordResetTTL); err != nil {
40
+ return fmt.Errorf("store password reset token: %w", err)
41
+ }
42
+
43
+ link := s.config.PasswordResetURL + "?token=" + raw
44
+ if err := s.mailer.Send(ctx, u.Email, "Reset your password", "Reset your password: "+link); err != nil {
45
+ // The token is stored already. Log the delivery failure so it is visible
46
+ // operationally while preserving the anti-enumeration response.
47
+ slog.Error("send password reset email", "error", err)
48
+ }
49
+ return nil
50
+ }
51
+
52
+ // ResetPassword consumes a one-time token and updates the local identity in
53
+ // one transaction, then revokes all sessions. If the cross-store revoke fails,
54
+ // restoring the token lets the caller retry the complete operation.
55
+ func (s *Service) ResetPassword(ctx context.Context, rawToken, newPassword string) error {
56
+ tokenHash := hashToken(rawToken)
57
+ userID, err := s.recovery.ResetPassword(ctx, tokenHash, newPassword)
58
+ if err != nil {
59
+ if errors.Is(err, domain.ErrInvalidToken) {
60
+ return errInvalidToken()
61
+ }
62
+ return fmt.Errorf("reset password: %w", err)
63
+ }
64
+ if err := s.refreshTokens.RevokeAllRefreshTokens(ctx, userID); err != nil {
65
+ if restoreErr := s.recoveryTokens.SetPasswordResetToken(ctx, tokenHash, userID, s.config.PasswordResetTTL); restoreErr != nil {
66
+ return fmt.Errorf("revoke sessions after password reset: %w; restore reset token for retry: %v", err, restoreErr)
67
+ }
68
+ return fmt.Errorf("revoke sessions after password reset (reset token restored for retry): %w", err)
69
+ }
70
+ return nil
71
+ }
72
+
73
+ // sendVerificationEmail issues a one-time token and sends its link. It is
74
+ // shared by registration and the authenticated resend flow.
75
+ func (s *Service) sendVerificationEmail(ctx context.Context, u *domain.User) {
76
+ raw, err := randomToken()
77
+ if err != nil {
78
+ slog.Error("generate email verification token", "error", err)
79
+ return
80
+ }
81
+ if err := s.recoveryTokens.SetEmailVerifyToken(ctx, hashToken(raw), u.ID, s.config.EmailVerifyTTL); err != nil {
82
+ slog.Error("store email verification token", "error", err)
83
+ return
84
+ }
85
+ link := s.config.EmailVerifyURL + "?token=" + raw
86
+ if err := s.mailer.Send(ctx, u.Email, "Verify your email", "Verify your email: "+link); err != nil {
87
+ slog.Error("send email verification email", "error", err)
88
+ }
89
+ }
90
+
91
+ func (s *Service) ResendVerificationEmail(ctx context.Context, userID uuid.UUID) error {
92
+ u, err := s.repo.FindByID(ctx, userID)
93
+ if err != nil {
94
+ return wrapFindErr(err)
95
+ }
96
+ if u.EmailVerified {
97
+ return errAlreadyVerified()
98
+ }
99
+ s.sendVerificationEmail(ctx, u)
100
+ return nil
101
+ }
102
+
103
+ func (s *Service) VerifyEmail(ctx context.Context, rawToken string) error {
104
+ _, err := s.recovery.VerifyEmail(ctx, hashToken(rawToken))
105
+ if err != nil {
106
+ if errors.Is(err, domain.ErrInvalidToken) {
107
+ return errInvalidToken()
108
+ }
109
+ return fmt.Errorf("verify email: %w", err)
110
+ }
111
+ return nil
112
+ }
@@ -0,0 +1,145 @@
1
+ package application
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strings"
7
+ "time"
8
+
9
+ "{{goModule}}/internal/app/user/domain"
10
+ "{{goModule}}/internal/app/user/ports"
11
+
12
+ "github.com/google/uuid"
13
+ )
14
+
15
+ // repository is a package-local name used by focused application tests and
16
+ // generated method extensions. The actual contract is owned by ports.
17
+ type repository = ports.UserRepository
18
+
19
+ // Service coordinates user authentication use cases. Transport concerns stay
20
+ // in the inbound adapter and persistence concerns stay in outbound adapters.
21
+ type Service struct {
22
+ repo ports.UserRepository
23
+ passwords ports.PasswordHasher
24
+ refreshTokens ports.RefreshTokenStore
25
+ oauthTransactions ports.OAuthTransactionStore
26
+ recoveryTokens ports.RecoveryTokenStore
27
+ mfa ports.MFAStore
28
+ mailer ports.AuthMailer
29
+ providers ProviderRegistry
30
+ recovery *Recovery
31
+ config AuthConfig
32
+ roles ports.RoleChecker
33
+ now func() time.Time
34
+ // go-scaffold:user-service-fields
35
+ }
36
+
37
+ // NewService is the application composition constructor. It accepts explicit
38
+ // ports and policy rather than a framework or catch-all infrastructure object.
39
+ func NewService(deps Dependencies, cfg AuthConfig) *Service {
40
+ if deps.Passwords == nil {
41
+ panic("user application: password hasher is required")
42
+ }
43
+ if cfg.JWTAccessTTL <= 0 {
44
+ cfg.JWTAccessTTL = 15 * time.Minute
45
+ }
46
+ if cfg.JWTRefreshTTL <= 0 {
47
+ cfg.JWTRefreshTTL = 12 * time.Hour
48
+ }
49
+ cfg.JWTRefreshMaxTTL = defaultDuration(cfg.JWTRefreshMaxTTL, cfg.JWTRefreshTTL)
50
+ cfg.OAuthStateTTL = defaultDuration(cfg.OAuthStateTTL, 10*time.Minute)
51
+ if cfg.MFA.RecoveryCodeCount == 0 {
52
+ cfg.MFA.RecoveryCodeCount = defaultMFACodes
53
+ }
54
+ clock := deps.Clock
55
+ if clock == nil {
56
+ clock = time.Now
57
+ }
58
+ return &Service{
59
+ repo: deps.Repository,
60
+ passwords: deps.Passwords,
61
+ refreshTokens: deps.RefreshTokens,
62
+ oauthTransactions: deps.OAuthTransactions,
63
+ recoveryTokens: deps.RecoveryTokens,
64
+ mfa: deps.MFA,
65
+ mailer: deps.Mailer,
66
+ providers: deps.Providers,
67
+ recovery: NewRecovery(deps.Repository, deps.RecoveryTokens, deps.Passwords),
68
+ config: cfg,
69
+ roles: deps.Roles,
70
+ now: clock,
71
+ // go-scaffold:user-service-init
72
+ }
73
+ }
74
+
75
+ func defaultDuration(value, fallback time.Duration) time.Duration {
76
+ if value > 0 {
77
+ return value
78
+ }
79
+ return fallback
80
+ }
81
+
82
+ func (s *Service) clock() time.Time {
83
+ if s.now == nil {
84
+ return time.Now()
85
+ }
86
+ return s.now()
87
+ }
88
+
89
+ // normalizeEmail is applied at every boundary an address enters the service
90
+ // through. The database stores the canonical lower-case representation.
91
+ func normalizeEmail(email string) string {
92
+ return strings.ToLower(strings.TrimSpace(email))
93
+ }
94
+
95
+ // SetRole validates the role against the role catalog before persisting it.
96
+ func (s *Service) SetRole(ctx context.Context, userID uuid.UUID, roleCode string) (*domain.User, error) {
97
+ u, err := s.repo.FindByID(ctx, userID)
98
+ if err != nil {
99
+ return nil, wrapFindErr(err)
100
+ }
101
+ if s.roles == nil {
102
+ return nil, fmt.Errorf("role checker is not configured")
103
+ }
104
+ exists, err := s.roles.CodeExists(ctx, roleCode)
105
+ if err != nil {
106
+ return nil, fmt.Errorf("check role code: %w", err)
107
+ }
108
+ if !exists {
109
+ return nil, errUnknownRole()
110
+ }
111
+ u.Role = roleCode
112
+ if err := s.repo.UpdateUser(ctx, u); err != nil {
113
+ return nil, fmt.Errorf("update user role: %w", err)
114
+ }
115
+ return u, nil
116
+ }
117
+
118
+ // ServicePort is the inbound application boundary. HTTP, jobs, and future
119
+ // transports depend on this capability set instead of the concrete service.
120
+ type ServicePort interface {
121
+ // go-scaffold:service-interface
122
+ Register(context.Context, RegisterInput) (*AuthResult, error)
123
+ Login(context.Context, LoginInput) (*AuthResult, error)
124
+ Refresh(context.Context, string) (*AuthResponse, error)
125
+ Logout(context.Context, string) error
126
+ LogoutAll(context.Context, uuid.UUID) error
127
+ ForgotPassword(context.Context, string) error
128
+ ResetPassword(context.Context, string, string) error
129
+ VerifyEmail(context.Context, string) error
130
+ ResendVerificationEmail(context.Context, uuid.UUID) error
131
+ BeginLogin(context.Context, string, LoginStartInput) (*Authorization, error)
132
+ ExchangeLogin(context.Context, string, LoginExchangeInput) (*AuthResult, error)
133
+ VerifyMFA(context.Context, string, string) (*AuthResponse, error)
134
+ MFAStatus(context.Context, uuid.UUID) (MFAStatus, error)
135
+ SetupMFA(context.Context, uuid.UUID) (*MFASetup, error)
136
+ ConfirmMFA(context.Context, uuid.UUID, string) ([]string, error)
137
+ DisableMFA(context.Context, uuid.UUID, string) error
138
+ Get(context.Context, uuid.UUID) (*domain.User, error)
139
+ List(context.Context, int, int) ([]domain.User, error)
140
+ SetRole(context.Context, uuid.UUID, string) (*domain.User, error)
141
+ }
142
+
143
+ var _ ServicePort = (*Service)(nil)
144
+
145
+ // go-scaffold:service-methods