@nakedev/go-scaffold 0.4.3 → 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 (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
@@ -6,7 +6,7 @@
6
6
  // per-user session set is just a WHERE clause, and expiry is a column every
7
7
  // read filters on rather than a TTL the store enforces. What that costs is a
8
8
  // sweep to keep the table from growing — see DeleteExpired.
9
- package user
9
+ package postgres
10
10
 
11
11
  import (
12
12
  "context"
@@ -16,7 +16,7 @@ import (
16
16
  "strings"
17
17
  "time"
18
18
 
19
- "{{goModule}}/internal/app/user/model"
19
+ "{{goModule}}/internal/app/user/ports"
20
20
  "{{goModule}}/internal/shared/tx"
21
21
 
22
22
  "github.com/google/uuid"
@@ -29,19 +29,23 @@ const (
29
29
  kindOAuthState = "oauth_state"
30
30
  )
31
31
 
32
- type pgTokenStore struct {
32
+ type PgTokenStore struct {
33
33
  db *gorm.DB
34
34
  recovery *recoveryTokenStore
35
35
  }
36
36
 
37
- func NewPgTokenStore(db *gorm.DB) *pgTokenStore {
38
- return &pgTokenStore{db: db, recovery: newRecoveryTokenStore(db)}
37
+ func NewPgTokenStore(db *gorm.DB) *PgTokenStore {
38
+ return &PgTokenStore{db: db, recovery: newRecoveryTokenStore(db)}
39
39
  }
40
40
 
41
+ var _ ports.RefreshTokenStore = (*PgTokenStore)(nil)
42
+ var _ ports.OAuthTransactionStore = (*PgTokenStore)(nil)
43
+ var _ ports.RecoveryTokenStore = (*PgTokenStore)(nil)
44
+
41
45
  // lookup finds a live token of the given kind. Expiry is enforced here rather
42
46
  // than by a sweep, so a row that outlived its TTL is already invisible.
43
- func (s *pgTokenStore) lookup(ctx context.Context, hash, kind string) (uuid.UUID, bool, error) {
44
- var row model.AuthToken
47
+ func (s *PgTokenStore) lookup(ctx context.Context, hash, kind string) (uuid.UUID, bool, error) {
48
+ var row AuthToken
45
49
  err := tx.From(ctx, s.db).WithContext(ctx).
46
50
  Where("token_hash = ? AND kind = ? AND expires_at > now()", hash, kind).
47
51
  Take(&row).Error
@@ -54,11 +58,11 @@ func (s *pgTokenStore) lookup(ctx context.Context, hash, kind string) (uuid.UUID
54
58
  return row.UserID, true, nil
55
59
  }
56
60
 
57
- func (s *pgTokenStore) SetRefreshToken(ctx context.Context, tokenHash string, token refreshTokenRecord) error {
61
+ func (s *PgTokenStore) SetRefreshToken(ctx context.Context, tokenHash string, token ports.RefreshTokenRecord) error {
58
62
  if token.UserID == uuid.Nil || token.ExpiresAt.IsZero() || token.AbsoluteExpiresAt.IsZero() || token.AbsoluteExpiresAt.Before(token.ExpiresAt) {
59
63
  return fmt.Errorf("refresh token record expiry is invalid")
60
64
  }
61
- row := model.AuthToken{
65
+ row := AuthToken{
62
66
  TokenHash: tokenHash,
63
67
  UserID: token.UserID,
64
68
  Kind: kindRefresh,
@@ -78,15 +82,15 @@ func (s *pgTokenStore) SetRefreshToken(ctx context.Context, tokenHash string, to
78
82
  }).FirstOrCreate(&row).Error
79
83
  }
80
84
 
81
- func (s *pgTokenStore) GetRefreshToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
85
+ func (s *PgTokenStore) GetRefreshToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
82
86
  return s.lookup(ctx, tokenHash, kindRefresh)
83
87
  }
84
88
 
85
89
  // ConsumeRefreshToken removes the active row and writes the reuse-detection
86
90
  // tombstone in one statement. A concurrent caller can observe either the
87
91
  // active row or the tombstone, never the gap between separate operations.
88
- func (s *pgTokenStore) ConsumeRefreshToken(ctx context.Context, tokenHash string) (refreshTokenRecord, bool, error) {
89
- var token refreshTokenRecord
92
+ func (s *PgTokenStore) ConsumeRefreshToken(ctx context.Context, tokenHash string) (ports.RefreshTokenRecord, bool, error) {
93
+ var token ports.RefreshTokenRecord
90
94
  err := tx.From(ctx, s.db).WithContext(ctx).Raw(
91
95
  `WITH consumed AS (
92
96
  DELETE FROM user_svc.auth_tokens
@@ -99,41 +103,41 @@ func (s *pgTokenStore) ConsumeRefreshToken(ctx context.Context, tokenHash string
99
103
  RETURNING user_id, expires_at, absolute_expires_at`,
100
104
  tokenHash, kindRefresh, tokenHash, kindRefreshUsed).Row().Scan(&token.UserID, &token.ExpiresAt, &token.AbsoluteExpiresAt)
101
105
  if errors.Is(err, sql.ErrNoRows) {
102
- return refreshTokenRecord{}, false, nil
106
+ return ports.RefreshTokenRecord{}, false, nil
103
107
  }
104
108
  if err != nil {
105
- return refreshTokenRecord{}, false, err
109
+ return ports.RefreshTokenRecord{}, false, err
106
110
  }
107
111
  return token, true, nil
108
112
  }
109
113
 
110
114
  // DeleteRefreshToken removes the row outright and leaves nothing behind — see
111
115
  // the AuthToken doc comment for why logout must not leave a tombstone.
112
- func (s *pgTokenStore) DeleteRefreshToken(ctx context.Context, tokenHash string, _ uuid.UUID) error {
116
+ func (s *PgTokenStore) DeleteRefreshToken(ctx context.Context, tokenHash string, _ uuid.UUID) error {
113
117
  return tx.From(ctx, s.db).WithContext(ctx).
114
118
  Where("token_hash = ? AND kind = ?", tokenHash, kindRefresh).
115
- Delete(&model.AuthToken{}).Error
119
+ Delete(&AuthToken{}).Error
116
120
  }
117
121
 
118
122
  // RevokeAllRefreshTokens is one statement here because Postgres can query by
119
123
  // value — the Redis store has to keep a separate per-user set to answer this,
120
124
  // and that set accumulates stale members its own comment calls out.
121
125
  // Tombstones are left in place so reuse detection still works afterwards.
122
- func (s *pgTokenStore) RevokeAllRefreshTokens(ctx context.Context, userID uuid.UUID) error {
126
+ func (s *PgTokenStore) RevokeAllRefreshTokens(ctx context.Context, userID uuid.UUID) error {
123
127
  return tx.From(ctx, s.db).WithContext(ctx).
124
128
  Where("user_id = ? AND kind = ?", userID, kindRefresh).
125
- Delete(&model.AuthToken{}).Error
129
+ Delete(&AuthToken{}).Error
126
130
  }
127
131
 
128
- func (s *pgTokenStore) IsRefreshTokenUsed(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
132
+ func (s *PgTokenStore) IsRefreshTokenUsed(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
129
133
  return s.lookup(ctx, tokenHash, kindRefreshUsed)
130
134
  }
131
135
 
132
- func (s *pgTokenStore) SetLoginTransaction(ctx context.Context, stateHash string, transaction loginTransaction) error {
136
+ func (s *PgTokenStore) SetLoginTransaction(ctx context.Context, stateHash string, transaction ports.LoginTransaction) error {
133
137
  if strings.TrimSpace(stateHash) == "" || strings.TrimSpace(transaction.Provider) == "" || strings.TrimSpace(transaction.CodeChallenge) == "" || strings.TrimSpace(transaction.Nonce) == "" || !transaction.ExpiresAt.After(time.Now()) {
134
138
  return fmt.Errorf("oauth login transaction is invalid")
135
139
  }
136
- row := model.AuthToken{
140
+ row := AuthToken{
137
141
  TokenHash: stateHash,
138
142
  Kind: kindOAuthState,
139
143
  ExpiresAt: transaction.ExpiresAt,
@@ -153,8 +157,8 @@ func (s *pgTokenStore) SetLoginTransaction(ctx context.Context, stateHash string
153
157
  }).FirstOrCreate(&row).Error
154
158
  }
155
159
 
156
- func (s *pgTokenStore) ConsumeLoginTransaction(ctx context.Context, stateHash string) (loginTransaction, bool, error) {
157
- var transaction loginTransaction
160
+ func (s *PgTokenStore) ConsumeLoginTransaction(ctx context.Context, stateHash string) (ports.LoginTransaction, bool, error) {
161
+ var transaction ports.LoginTransaction
158
162
  err := tx.From(ctx, s.db).WithContext(ctx).Raw(
159
163
  `DELETE FROM user_svc.auth_tokens
160
164
  WHERE token_hash = ? AND kind = ? AND expires_at > now()
@@ -166,31 +170,31 @@ func (s *pgTokenStore) ConsumeLoginTransaction(ctx context.Context, stateHash st
166
170
  &transaction.ExpiresAt,
167
171
  )
168
172
  if errors.Is(err, sql.ErrNoRows) {
169
- return loginTransaction{}, false, nil
173
+ return ports.LoginTransaction{}, false, nil
170
174
  }
171
175
  if err != nil {
172
- return loginTransaction{}, false, err
176
+ return ports.LoginTransaction{}, false, err
173
177
  }
174
178
  return transaction, true, nil
175
179
  }
176
180
 
177
- func (s *pgTokenStore) SetPasswordResetToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
181
+ func (s *PgTokenStore) SetPasswordResetToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
178
182
  return s.recovery.put(ctx, tokenHash, kindPasswordSet, userID, ttl)
179
183
  }
180
184
 
181
- func (s *pgTokenStore) ConsumePasswordResetToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
185
+ func (s *PgTokenStore) ConsumePasswordResetToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
182
186
  return s.recovery.consume(ctx, tokenHash, kindPasswordSet)
183
187
  }
184
188
 
185
- func (s *pgTokenStore) SetEmailVerifyToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
189
+ func (s *PgTokenStore) SetEmailVerifyToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
186
190
  return s.recovery.put(ctx, tokenHash, kindEmailVerify, userID, ttl)
187
191
  }
188
192
 
189
- func (s *pgTokenStore) ConsumeEmailVerifyToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
193
+ func (s *PgTokenStore) ConsumeEmailVerifyToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
190
194
  return s.recovery.consume(ctx, tokenHash, kindEmailVerify)
191
195
  }
192
196
 
193
- func (s *pgTokenStore) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
197
+ func (s *PgTokenStore) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
194
198
  return s.recovery.withTransaction(ctx, fn)
195
199
  }
196
200
 
@@ -201,9 +205,9 @@ func (s *pgTokenStore) WithTransaction(ctx context.Context, fn func(context.Cont
201
205
  // ponytail: not scheduled for you. Wire it up the day the table is big enough
202
206
  // to notice; a scaffold that silently runs background sweeps nobody asked for
203
207
  // is harder to reason about than one line you add when it matters.
204
- func (s *pgTokenStore) DeleteExpired(ctx context.Context) (int64, error) {
208
+ func (s *PgTokenStore) DeleteExpired(ctx context.Context) (int64, error) {
205
209
  res := tx.From(ctx, s.db).WithContext(ctx).
206
210
  Where("expires_at <= now()").
207
- Delete(&model.AuthToken{})
211
+ Delete(&AuthToken{})
208
212
  return res.RowsAffected, res.Error
209
213
  }
@@ -1,10 +1,12 @@
1
- package user
1
+ package postgres
2
2
 
3
3
  import (
4
4
  "context"
5
5
  "os"
6
6
  "testing"
7
+ "time"
7
8
 
9
+ "{{goModule}}/internal/app/user/ports"
8
10
  "github.com/google/uuid"
9
11
  "gorm.io/driver/postgres"
10
12
  "gorm.io/gorm"
@@ -50,7 +52,7 @@ func TestPgTokenStore_ConsumeRefreshToken_ConcurrentRealDatabase(t *testing.T) {
50
52
  ctx := context.Background()
51
53
  userID := uuid.New()
52
54
  hash := "real-pg-refresh-" + uuid.NewString()
53
- if err := store.SetRefreshToken(ctx, hash, testRefreshTokenRecord(userID)); err != nil {
55
+ if err := store.SetRefreshToken(ctx, hash, refreshTokenRecordForTest(userID)); err != nil {
54
56
  t.Fatalf("seed refresh token: %v", err)
55
57
  }
56
58
  t.Cleanup(func() {
@@ -59,7 +61,7 @@ func TestPgTokenStore_ConsumeRefreshToken_ConcurrentRealDatabase(t *testing.T) {
59
61
 
60
62
  const callers = 32
61
63
  type result struct {
62
- token refreshTokenRecord
64
+ token ports.RefreshTokenRecord
63
65
  ok bool
64
66
  err error
65
67
  }
@@ -94,3 +96,8 @@ func TestPgTokenStore_ConsumeRefreshToken_ConcurrentRealDatabase(t *testing.T) {
94
96
  t.Fatalf("consumed refresh token remained active: ok=%t err=%v", ok, err)
95
97
  }
96
98
  }
99
+
100
+ func refreshTokenRecordForTest(userID uuid.UUID) ports.RefreshTokenRecord {
101
+ now := time.Now()
102
+ return ports.RefreshTokenRecord{UserID: userID, ExpiresAt: now.Add(time.Hour), AbsoluteExpiresAt: now.Add(24 * time.Hour)}
103
+ }
@@ -1,4 +1,4 @@
1
- package user
1
+ package postgres
2
2
 
3
3
  import (
4
4
  "context"
@@ -6,7 +6,7 @@ import (
6
6
  "errors"
7
7
  "time"
8
8
 
9
- "{{goModule}}/internal/app/user/model"
9
+ "{{goModule}}/internal/app/user/ports"
10
10
  "{{goModule}}/internal/shared/tx"
11
11
 
12
12
  "github.com/google/uuid"
@@ -25,6 +25,12 @@ type recoveryTokenStore struct {
25
25
  db *gorm.DB
26
26
  }
27
27
 
28
+ var _ ports.RecoveryTokenStore = (*recoveryTokenStore)(nil)
29
+
30
+ func NewRecoveryTokenStore(db *gorm.DB) ports.RecoveryTokenStore {
31
+ return newRecoveryTokenStore(db)
32
+ }
33
+
28
34
  func newRecoveryTokenStore(db *gorm.DB) *recoveryTokenStore {
29
35
  return &recoveryTokenStore{db: db}
30
36
  }
@@ -35,7 +41,7 @@ func (s *recoveryTokenStore) withTransaction(ctx context.Context, fn func(contex
35
41
 
36
42
  func (s *recoveryTokenStore) put(ctx context.Context, hash, kind string, userID uuid.UUID, ttl time.Duration) error {
37
43
  expiresAt := time.Now().Add(ttl)
38
- row := model.AuthToken{TokenHash: hash, UserID: userID, Kind: kind, ExpiresAt: expiresAt}
44
+ row := AuthToken{TokenHash: hash, UserID: userID, Kind: kind, ExpiresAt: expiresAt}
39
45
  return tx.From(ctx, s.db).WithContext(ctx).
40
46
  Where("token_hash = ?", hash).
41
47
  Assign(map[string]any{"user_id": userID, "kind": kind, "expires_at": expiresAt}).
@@ -56,3 +62,23 @@ func (s *recoveryTokenStore) consume(ctx context.Context, hash, kind string) (uu
56
62
  }
57
63
  return userID, true, nil
58
64
  }
65
+
66
+ func (s *recoveryTokenStore) SetPasswordResetToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
67
+ return s.put(ctx, tokenHash, kindPasswordSet, userID, ttl)
68
+ }
69
+
70
+ func (s *recoveryTokenStore) ConsumePasswordResetToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
71
+ return s.consume(ctx, tokenHash, kindPasswordSet)
72
+ }
73
+
74
+ func (s *recoveryTokenStore) SetEmailVerifyToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
75
+ return s.put(ctx, tokenHash, kindEmailVerify, userID, ttl)
76
+ }
77
+
78
+ func (s *recoveryTokenStore) ConsumeEmailVerifyToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
79
+ return s.consume(ctx, tokenHash, kindEmailVerify)
80
+ }
81
+
82
+ func (s *recoveryTokenStore) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
83
+ return s.withTransaction(ctx, fn)
84
+ }
@@ -1,7 +1,7 @@
1
1
  // Redis-backed tokenStore — installed by `add auth --store redis`.
2
2
  // The Postgres-backed alternative lives in tokenstore_pg.go; only one of the
3
3
  // two is ever written into a project, and Service never learns which.
4
- package user
4
+ package redisadapter
5
5
 
6
6
  import (
7
7
  "context"
@@ -9,6 +9,9 @@ import (
9
9
  "fmt"
10
10
  "time"
11
11
 
12
+ "{{goModule}}/internal/app/user/adapters/outbound/postgres"
13
+ "{{goModule}}/internal/app/user/ports"
14
+
12
15
  "github.com/google/uuid"
13
16
  "github.com/redis/go-redis/v9"
14
17
  "gorm.io/gorm"
@@ -17,21 +20,25 @@ import (
17
20
  const (
18
21
  // The shared {refresh} hash tag keeps all keys touched by the Lua rotation
19
22
  // script on one Redis Cluster slot as well as making the operation atomic.
20
- refreshKeyPrefix = "user:{refresh}:" // +hash -> refreshTokenRecord, TTL = ExpiresAt
23
+ refreshKeyPrefix = "user:{refresh}:" // +hash -> ports.RefreshTokenRecord, TTL = ExpiresAt
21
24
  refreshUserKeyPrefix = "user:{refresh}:user:" // +userID -> SET of active token hashes
22
- refreshUsedKeyPrefix = "user:{refresh}:used:" // +hash -> refreshTokenRecord, TTL = source token expiry
25
+ refreshUsedKeyPrefix = "user:{refresh}:used:" // +hash -> ports.RefreshTokenRecord, TTL = source token expiry
23
26
  oauthStateKeyPrefix = "user:{oauth}:state:"
24
27
  )
25
28
 
26
- type redisTokenStore struct {
29
+ type RedisTokenStore struct {
27
30
  rdb *redis.Client
28
- recovery *recoveryTokenStore
31
+ recovery ports.RecoveryTokenStore
29
32
  }
30
33
 
31
- func NewRedisTokenStore(rdb *redis.Client, db *gorm.DB) *redisTokenStore {
32
- return &redisTokenStore{rdb: rdb, recovery: newRecoveryTokenStore(db)}
34
+ func NewRedisTokenStore(rdb *redis.Client, db *gorm.DB) *RedisTokenStore {
35
+ return &RedisTokenStore{rdb: rdb, recovery: postgres.NewRecoveryTokenStore(db)}
33
36
  }
34
37
 
38
+ var _ ports.RefreshTokenStore = (*RedisTokenStore)(nil)
39
+ var _ ports.OAuthTransactionStore = (*RedisTokenStore)(nil)
40
+ var _ ports.RecoveryTokenStore = (*RedisTokenStore)(nil)
41
+
35
42
  var consumeRefreshScript = redis.NewScript(`
36
43
  local token = redis.call("GET", KEYS[1])
37
44
  local ttl = redis.call("PTTL", KEYS[1])
@@ -69,7 +76,7 @@ redis.call("DEL", KEYS[1])
69
76
  return #hashes
70
77
  `)
71
78
 
72
- func (s *redisTokenStore) SetRefreshToken(ctx context.Context, tokenHash string, token refreshTokenRecord) error {
79
+ func (s *RedisTokenStore) SetRefreshToken(ctx context.Context, tokenHash string, token ports.RefreshTokenRecord) error {
73
80
  ttl := time.Until(token.ExpiresAt)
74
81
  if token.UserID == uuid.Nil || ttl <= 0 || token.AbsoluteExpiresAt.IsZero() || token.AbsoluteExpiresAt.Before(token.ExpiresAt) {
75
82
  return fmt.Errorf("refresh token expiry is invalid")
@@ -85,7 +92,7 @@ func (s *redisTokenStore) SetRefreshToken(ctx context.Context, tokenHash string,
85
92
  return err
86
93
  }
87
94
 
88
- func (s *redisTokenStore) GetRefreshToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
95
+ func (s *RedisTokenStore) GetRefreshToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
89
96
  raw, err := s.rdb.Get(ctx, refreshKeyPrefix+tokenHash).Result()
90
97
  if err == redis.Nil {
91
98
  return uuid.Nil, false, nil
@@ -103,26 +110,26 @@ func (s *redisTokenStore) GetRefreshToken(ctx context.Context, tokenHash string)
103
110
  // ConsumeRefreshToken uses one Lua script for active-token deletion,
104
111
  // per-user-session cleanup, and the reuse tombstone. A concurrent caller
105
112
  // cannot observe the active value after another caller has won the rotation.
106
- func (s *redisTokenStore) ConsumeRefreshToken(ctx context.Context, tokenHash string) (refreshTokenRecord, bool, error) {
113
+ func (s *RedisTokenStore) ConsumeRefreshToken(ctx context.Context, tokenHash string) (ports.RefreshTokenRecord, bool, error) {
107
114
  result, err := consumeRefreshScript.Run(ctx, s.rdb,
108
115
  []string{refreshKeyPrefix + tokenHash, refreshUsedKeyPrefix + tokenHash, refreshUserKeyPrefix},
109
116
  tokenHash,
110
117
  ).Result()
111
118
  if err != nil {
112
- return refreshTokenRecord{}, false, err
119
+ return ports.RefreshTokenRecord{}, false, err
113
120
  }
114
121
  raw, ok := result.(string)
115
122
  if !ok || raw == "" {
116
- return refreshTokenRecord{}, false, nil
123
+ return ports.RefreshTokenRecord{}, false, nil
117
124
  }
118
125
  token, err := decodeRefreshTokenRecord(raw)
119
126
  if err != nil {
120
- return refreshTokenRecord{}, false, fmt.Errorf("decode consumed refresh token record: %w", err)
127
+ return ports.RefreshTokenRecord{}, false, fmt.Errorf("decode consumed refresh token record: %w", err)
121
128
  }
122
129
  return token, true, nil
123
130
  }
124
131
 
125
- func (s *redisTokenStore) DeleteRefreshToken(ctx context.Context, tokenHash string, userID uuid.UUID) error {
132
+ func (s *RedisTokenStore) DeleteRefreshToken(ctx context.Context, tokenHash string, userID uuid.UUID) error {
126
133
  pipe := s.rdb.TxPipeline()
127
134
  pipe.Del(ctx, refreshKeyPrefix+tokenHash)
128
135
  pipe.SRem(ctx, refreshUserKeyPrefix+userID.String(), tokenHash)
@@ -135,7 +142,7 @@ func (s *redisTokenStore) DeleteRefreshToken(ctx context.Context, tokenHash stri
135
142
  // important because SetRefreshToken updates two keys: a SMEMBERS followed by
136
143
  // a separate pipeline could miss a token added between those operations and
137
144
  // leave an active token with no session-set member.
138
- func (s *redisTokenStore) RevokeAllRefreshTokens(ctx context.Context, userID uuid.UUID) error {
145
+ func (s *RedisTokenStore) RevokeAllRefreshTokens(ctx context.Context, userID uuid.UUID) error {
139
146
  setKey := refreshUserKeyPrefix + userID.String()
140
147
  _, err := revokeAllRefreshScript.Run(ctx, s.rdb,
141
148
  []string{setKey, refreshKeyPrefix},
@@ -144,7 +151,7 @@ func (s *redisTokenStore) RevokeAllRefreshTokens(ctx context.Context, userID uui
144
151
  return err
145
152
  }
146
153
 
147
- func (s *redisTokenStore) IsRefreshTokenUsed(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
154
+ func (s *RedisTokenStore) IsRefreshTokenUsed(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
148
155
  raw, err := s.rdb.Get(ctx, refreshUsedKeyPrefix+tokenHash).Result()
149
156
  if err == redis.Nil {
150
157
  return uuid.Nil, false, nil
@@ -159,7 +166,7 @@ func (s *redisTokenStore) IsRefreshTokenUsed(ctx context.Context, tokenHash stri
159
166
  return token.UserID, true, nil
160
167
  }
161
168
 
162
- func (s *redisTokenStore) SetLoginTransaction(ctx context.Context, stateHash string, transaction loginTransaction) error {
169
+ func (s *RedisTokenStore) SetLoginTransaction(ctx context.Context, stateHash string, transaction ports.LoginTransaction) error {
163
170
  if !transaction.ExpiresAt.After(time.Now()) {
164
171
  return fmt.Errorf("oauth login transaction expiry is invalid")
165
172
  }
@@ -170,52 +177,52 @@ func (s *redisTokenStore) SetLoginTransaction(ctx context.Context, stateHash str
170
177
  return s.rdb.Set(ctx, oauthStateKeyPrefix+stateHash, payload, time.Until(transaction.ExpiresAt)).Err()
171
178
  }
172
179
 
173
- func (s *redisTokenStore) ConsumeLoginTransaction(ctx context.Context, stateHash string) (loginTransaction, bool, error) {
180
+ func (s *RedisTokenStore) ConsumeLoginTransaction(ctx context.Context, stateHash string) (ports.LoginTransaction, bool, error) {
174
181
  raw, err := s.rdb.GetDel(ctx, oauthStateKeyPrefix+stateHash).Result()
175
182
  if err == redis.Nil {
176
- return loginTransaction{}, false, nil
183
+ return ports.LoginTransaction{}, false, nil
177
184
  }
178
185
  if err != nil {
179
- return loginTransaction{}, false, err
186
+ return ports.LoginTransaction{}, false, err
180
187
  }
181
- var transaction loginTransaction
188
+ var transaction ports.LoginTransaction
182
189
  if err := json.Unmarshal([]byte(raw), &transaction); err != nil {
183
- return loginTransaction{}, false, fmt.Errorf("decode oauth login transaction: %w", err)
190
+ return ports.LoginTransaction{}, false, fmt.Errorf("decode oauth login transaction: %w", err)
184
191
  }
185
192
  return transaction, true, nil
186
193
  }
187
194
 
188
- func decodeRefreshTokenRecord(raw string) (refreshTokenRecord, error) {
189
- var token refreshTokenRecord
195
+ func decodeRefreshTokenRecord(raw string) (ports.RefreshTokenRecord, error) {
196
+ var token ports.RefreshTokenRecord
190
197
  if err := json.Unmarshal([]byte(raw), &token); err != nil {
191
- return refreshTokenRecord{}, err
198
+ return ports.RefreshTokenRecord{}, err
192
199
  }
193
200
  if token.UserID == uuid.Nil || token.ExpiresAt.IsZero() || token.AbsoluteExpiresAt.IsZero() || token.AbsoluteExpiresAt.Before(token.ExpiresAt) {
194
- return refreshTokenRecord{}, fmt.Errorf("refresh token record is incomplete")
201
+ return ports.RefreshTokenRecord{}, fmt.Errorf("refresh token record is incomplete")
195
202
  }
196
203
  return token, nil
197
204
  }
198
205
 
199
- func (s *redisTokenStore) SetPasswordResetToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
200
- return s.recovery.put(ctx, tokenHash, kindPasswordSet, userID, ttl)
206
+ func (s *RedisTokenStore) SetPasswordResetToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
207
+ return s.recovery.SetPasswordResetToken(ctx, tokenHash, userID, ttl)
201
208
  }
202
209
 
203
210
  // ConsumePasswordResetToken is one-time-use by construction: the SQL DELETE
204
211
  // runs inside the same transaction as the user update.
205
- func (s *redisTokenStore) ConsumePasswordResetToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
206
- return s.recovery.consume(ctx, tokenHash, kindPasswordSet)
212
+ func (s *RedisTokenStore) ConsumePasswordResetToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
213
+ return s.recovery.ConsumePasswordResetToken(ctx, tokenHash)
207
214
  }
208
215
 
209
- func (s *redisTokenStore) SetEmailVerifyToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
210
- return s.recovery.put(ctx, tokenHash, kindEmailVerify, userID, ttl)
216
+ func (s *RedisTokenStore) SetEmailVerifyToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error {
217
+ return s.recovery.SetEmailVerifyToken(ctx, tokenHash, userID, ttl)
211
218
  }
212
219
 
213
220
  // ConsumeEmailVerifyToken is one-time-use by construction, same as
214
221
  // ConsumePasswordResetToken — the SQL delete participates in the transaction.
215
- func (s *redisTokenStore) ConsumeEmailVerifyToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
216
- return s.recovery.consume(ctx, tokenHash, kindEmailVerify)
222
+ func (s *RedisTokenStore) ConsumeEmailVerifyToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error) {
223
+ return s.recovery.ConsumeEmailVerifyToken(ctx, tokenHash)
217
224
  }
218
225
 
219
- func (s *redisTokenStore) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
220
- return s.recovery.withTransaction(ctx, fn)
226
+ func (s *RedisTokenStore) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
227
+ return s.recovery.WithTransaction(ctx, fn)
221
228
  }
@@ -1,4 +1,4 @@
1
- package user
1
+ package redisadapter
2
2
 
3
3
  import (
4
4
  "context"
@@ -8,10 +8,21 @@ import (
8
8
  "testing"
9
9
  "time"
10
10
 
11
+ "{{goModule}}/internal/app/user/ports"
12
+
11
13
  "github.com/google/uuid"
12
14
  "github.com/redis/go-redis/v9"
13
15
  )
14
16
 
17
+ func testRefreshTokenRecord(userID uuid.UUID) ports.RefreshTokenRecord {
18
+ now := time.Now()
19
+ return ports.RefreshTokenRecord{
20
+ UserID: userID,
21
+ ExpiresAt: now.Add(time.Hour),
22
+ AbsoluteExpiresAt: now.Add(24 * time.Hour),
23
+ }
24
+ }
25
+
15
26
  func redisClientForTest(t *testing.T) *redis.Client {
16
27
  t.Helper()
17
28
  url := os.Getenv("TEST_REDIS_URL")
@@ -73,7 +84,7 @@ func TestRedisTokenStore_ConsumeRefreshToken_ConcurrentRealRedis(t *testing.T) {
73
84
  const callers = 32
74
85
  start := make(chan struct{})
75
86
  results := make(chan struct {
76
- token refreshTokenRecord
87
+ token ports.RefreshTokenRecord
77
88
  ok bool
78
89
  err error
79
90
  }, callers)
@@ -82,7 +93,7 @@ func TestRedisTokenStore_ConsumeRefreshToken_ConcurrentRealRedis(t *testing.T) {
82
93
  <-start
83
94
  token, ok, err := store.ConsumeRefreshToken(ctx, hash)
84
95
  results <- struct {
85
- token refreshTokenRecord
96
+ token ports.RefreshTokenRecord
86
97
  ok bool
87
98
  err error
88
99
  }{token: token, ok: ok, err: err}
@@ -0,0 +1,52 @@
1
+ package application
2
+
3
+ import (
4
+ "time"
5
+
6
+ "{{goModule}}/internal/app/user/ports"
7
+ )
8
+
9
+ // Dependencies is the explicit application composition contract for auth.
10
+ // Each port represents one lifecycle or capability so an adapter does not
11
+ // have to implement unrelated token operations.
12
+ type Dependencies struct {
13
+ Repository ports.UserRepository
14
+ Passwords ports.PasswordHasher
15
+ RefreshTokens ports.RefreshTokenStore
16
+ OAuthTransactions ports.OAuthTransactionStore
17
+ RecoveryTokens ports.RecoveryTokenStore
18
+ MFA ports.MFAStore
19
+ Mailer ports.AuthMailer
20
+ Providers ProviderRegistry
21
+ Roles ports.RoleChecker
22
+ Clock func() time.Time
23
+ }
24
+
25
+ type MFASettings struct {
26
+ Enabled bool
27
+ Issuer string
28
+ EncryptionKey string
29
+ ChallengeTTL time.Duration
30
+ TOTPWindow int
31
+ RecoveryCodeCount int
32
+ }
33
+
34
+ type AuthConfig struct {
35
+ JWTSecret string
36
+ JWTAccessTTL time.Duration
37
+ JWTRefreshTTL time.Duration
38
+ JWTRefreshMaxTTL time.Duration
39
+ OAuthStateTTL time.Duration
40
+ PasswordResetTTL time.Duration
41
+ PasswordResetURL string
42
+ EmailVerifyTTL time.Duration
43
+ EmailVerifyURL string
44
+ MFA MFASettings
45
+ }
46
+
47
+ // These aliases keep use-case code concise while the contracts remain owned
48
+ // by the module's ports package. Adapters import ports directly.
49
+ type UserRepository = ports.UserRepository
50
+ type AuthMailer = ports.AuthMailer
51
+ type RoleChecker = ports.RoleChecker
52
+ type PasswordHasher = ports.PasswordHasher
@@ -0,0 +1,75 @@
1
+ package application
2
+
3
+ import (
4
+ "time"
5
+
6
+ "{{goModule}}/internal/app/user/domain"
7
+
8
+ "github.com/google/uuid"
9
+ )
10
+
11
+ type RegisterInput struct {
12
+ Email string
13
+ Password string
14
+ Name string
15
+ }
16
+
17
+ type LoginInput struct {
18
+ Email string
19
+ Password string
20
+ }
21
+
22
+ type LoginExchangeInput struct {
23
+ Code string
24
+ State string
25
+ CodeVerifier string
26
+ }
27
+
28
+ type AuthResponse struct {
29
+ AccessToken string
30
+ RefreshToken string
31
+ TokenType string
32
+ ExpiresIn int
33
+ }
34
+
35
+ type AuthResult struct {
36
+ *AuthResponse
37
+ MFAChallenge string
38
+ }
39
+
40
+ func TokenResult(auth *AuthResponse) *AuthResult {
41
+ return &AuthResult{AuthResponse: auth}
42
+ }
43
+
44
+ func ChallengeResult(challenge string) *AuthResult {
45
+ return &AuthResult{MFAChallenge: challenge}
46
+ }
47
+
48
+ // Response is the generic application response used by generated method
49
+ // extensions. Auth's HTTP adapter may expose a richer meResponse, but method
50
+ // generation still has one stable transport-neutral mapping seam.
51
+ type Response struct {
52
+ ID uuid.UUID
53
+ Email string
54
+ Name string
55
+ AvatarURL string
56
+ EmailVerified bool
57
+ Role string
58
+ CreatedAt time.Time
59
+ UpdatedAt time.Time
60
+ }
61
+
62
+ func ToResponse(user *domain.User) Response {
63
+ return Response{
64
+ ID: user.ID,
65
+ Email: user.Email,
66
+ Name: user.Name,
67
+ AvatarURL: user.AvatarURL,
68
+ EmailVerified: user.EmailVerified,
69
+ Role: user.Role,
70
+ CreatedAt: user.CreatedAt,
71
+ UpdatedAt: user.UpdatedAt,
72
+ }
73
+ }
74
+
75
+ // go-scaffold:dto