@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.
- package/README.md +567 -513
- package/dist/commands/auth.js +12 -1
- package/dist/commands/check.js +281 -0
- package/dist/commands/create.js +2 -1
- package/dist/commands/generate.js +5 -2
- package/dist/commands/method.js +66 -70
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +19 -8
- package/dist/commands/undo.js +6 -3
- package/dist/commands/worker.js +14 -4
- package/dist/index.js +13 -1
- package/dist/templates/auth-manifest.js +46 -45
- package/dist/templates/create-manifest.js +4 -0
- package/dist/templates/module-manifest.js +82 -60
- package/dist/templates/rbac-manifest.js +15 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +2 -0
- package/dist/utils/auth-patcher.js +22 -22
- package/dist/utils/config.js +24 -5
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +3 -3
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/platform-patcher.js +27 -0
- package/dist/utils/rbac-patcher.js +73 -216
- package/package.json +1 -1
- package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
- package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
- package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
- package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
- package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
- package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
- package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
- package/templates/add/auth/internal/app/user/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
- package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
- package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
- package/templates/add/auth/internal/app/user/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
- package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
- package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
- package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
- package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
- package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
- package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
- package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
- package/templates/add/auth/internal/app/user/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
- package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
- package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
- package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
- package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
- package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
- package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
- package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
- package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
- package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
- package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
- package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
- package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
- package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
- package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
- package/templates/add/rbac/internal/app/role/composition.go.hbs +23 -10
- package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
- package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
- package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
- package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +54 -36
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +55 -31
- package/templates/create/base/README.md.hbs +77 -22
- package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/features/docs/architecture.md.hbs +64 -26
- package/templates/create/features/docs/patterns.md.hbs +117 -90
- package/templates/create/features/docs/techstack.md.hbs +17 -2
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
- package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
- package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
- package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
- package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
- package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
- package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
- package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
- package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
- package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
- package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
- package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/dist/utils/method-patcher.js +0 -537
- package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
- package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
- package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
- package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
- package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
- package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
- package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
- package/templates/generate/module/commands.go.hbs +0 -95
- package/templates/generate/module/composition.go.hbs +0 -23
- package/templates/generate/module/cqrs_test.go.hbs +0 -7
- package/templates/generate/module/dto.go.hbs +0 -36
- package/templates/generate/module/errors.go.hbs +0 -33
- package/templates/generate/module/handler.go.hbs +0 -179
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/commands.go.hbs +0 -34
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -82
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/queries.go.hbs +0 -45
- package/templates/generate/module/minimal/service.go.hbs +0 -71
- package/templates/generate/module/minimal/service_test.go.hbs +0 -77
- package/templates/generate/module/model/model.go.hbs +0 -36
- package/templates/generate/module/queries.go.hbs +0 -62
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -164
- package/templates/generate/module/service_test.go.hbs +0 -161
package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package application
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
@@ -9,12 +9,9 @@ import (
|
|
|
9
9
|
"testing"
|
|
10
10
|
"time"
|
|
11
11
|
|
|
12
|
-
"{{goModule}}/internal/app/user/
|
|
13
|
-
"{{goModule}}/internal/app/user/application"
|
|
14
|
-
"{{goModule}}/internal/shared/apperror"
|
|
12
|
+
"{{goModule}}/internal/app/user/domain"
|
|
15
13
|
|
|
16
14
|
"github.com/google/uuid"
|
|
17
|
-
"gorm.io/gorm"
|
|
18
15
|
)
|
|
19
16
|
|
|
20
17
|
// testTokenStore is the deliberately broad test-only composition used to
|
|
@@ -29,29 +26,29 @@ type testTokenStore interface {
|
|
|
29
26
|
// three-map shape (active / used-tombstone / per-user session set) closely
|
|
30
27
|
// enough to exercise rotation + reuse-detection without a real Redis.
|
|
31
28
|
type fakeTokenStore struct {
|
|
32
|
-
mu
|
|
33
|
-
txMu
|
|
34
|
-
active
|
|
35
|
-
used
|
|
36
|
-
sessions
|
|
37
|
-
transactions map[string]
|
|
38
|
-
reset
|
|
39
|
-
verify
|
|
29
|
+
mu sync.Mutex
|
|
30
|
+
txMu sync.Mutex
|
|
31
|
+
active map[string]RefreshTokenRecord
|
|
32
|
+
used map[string]uuid.UUID
|
|
33
|
+
sessions map[uuid.UUID]map[string]struct{}
|
|
34
|
+
transactions map[string]LoginTransaction
|
|
35
|
+
reset map[string]uuid.UUID
|
|
36
|
+
verify map[string]uuid.UUID
|
|
40
37
|
revokeAllErr error
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
func newFakeTokenStore() *fakeTokenStore {
|
|
44
41
|
return &fakeTokenStore{
|
|
45
|
-
active: map[string]
|
|
42
|
+
active: map[string]RefreshTokenRecord{},
|
|
46
43
|
used: map[string]uuid.UUID{},
|
|
47
44
|
sessions: map[uuid.UUID]map[string]struct{}{},
|
|
48
|
-
transactions: map[string]
|
|
45
|
+
transactions: map[string]LoginTransaction{},
|
|
49
46
|
reset: map[string]uuid.UUID{},
|
|
50
47
|
verify: map[string]uuid.UUID{},
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
func (f *fakeTokenStore) SetRefreshToken(_ context.Context, hash string, token
|
|
51
|
+
func (f *fakeTokenStore) SetRefreshToken(_ context.Context, hash string, token RefreshTokenRecord) error {
|
|
55
52
|
f.mu.Lock()
|
|
56
53
|
defer f.mu.Unlock()
|
|
57
54
|
f.active[hash] = token
|
|
@@ -71,12 +68,12 @@ func (f *fakeTokenStore) GetRefreshToken(_ context.Context, hash string) (uuid.U
|
|
|
71
68
|
return token.UserID, true, nil
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
func (f *fakeTokenStore) ConsumeRefreshToken(_ context.Context, hash string) (
|
|
71
|
+
func (f *fakeTokenStore) ConsumeRefreshToken(_ context.Context, hash string) (RefreshTokenRecord, bool, error) {
|
|
75
72
|
f.mu.Lock()
|
|
76
73
|
defer f.mu.Unlock()
|
|
77
74
|
token, ok := f.active[hash]
|
|
78
75
|
if !ok || !token.ExpiresAt.After(time.Now()) {
|
|
79
|
-
return
|
|
76
|
+
return RefreshTokenRecord{}, false, nil
|
|
80
77
|
}
|
|
81
78
|
delete(f.active, hash)
|
|
82
79
|
delete(f.sessions[token.UserID], hash)
|
|
@@ -138,8 +135,8 @@ func (f *fakeTokenStore) WithTransaction(ctx context.Context, fn func(context.Co
|
|
|
138
135
|
return err
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
func cloneRefreshTokenMap(in map[string]
|
|
142
|
-
out := make(map[string]
|
|
138
|
+
func cloneRefreshTokenMap(in map[string]RefreshTokenRecord) map[string]RefreshTokenRecord {
|
|
139
|
+
out := make(map[string]RefreshTokenRecord, len(in))
|
|
143
140
|
for key, value := range in {
|
|
144
141
|
out[key] = value
|
|
145
142
|
}
|
|
@@ -154,27 +151,27 @@ func cloneTokenMap(in map[string]uuid.UUID) map[string]uuid.UUID {
|
|
|
154
151
|
return out
|
|
155
152
|
}
|
|
156
153
|
|
|
157
|
-
func cloneLoginTransactions(in map[string]
|
|
158
|
-
out := make(map[string]
|
|
154
|
+
func cloneLoginTransactions(in map[string]LoginTransaction) map[string]LoginTransaction {
|
|
155
|
+
out := make(map[string]LoginTransaction, len(in))
|
|
159
156
|
for key, value := range in {
|
|
160
157
|
out[key] = value
|
|
161
158
|
}
|
|
162
159
|
return out
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
func (f *fakeTokenStore) SetLoginTransaction(_ context.Context, hash string, transaction
|
|
162
|
+
func (f *fakeTokenStore) SetLoginTransaction(_ context.Context, hash string, transaction LoginTransaction) error {
|
|
166
163
|
f.mu.Lock()
|
|
167
164
|
defer f.mu.Unlock()
|
|
168
165
|
f.transactions[hash] = transaction
|
|
169
166
|
return nil
|
|
170
167
|
}
|
|
171
168
|
|
|
172
|
-
func (f *fakeTokenStore) ConsumeLoginTransaction(_ context.Context, hash string) (
|
|
169
|
+
func (f *fakeTokenStore) ConsumeLoginTransaction(_ context.Context, hash string) (LoginTransaction, bool, error) {
|
|
173
170
|
f.mu.Lock()
|
|
174
171
|
defer f.mu.Unlock()
|
|
175
172
|
transaction, ok := f.transactions[hash]
|
|
176
173
|
if !ok || !transaction.ExpiresAt.After(time.Now()) {
|
|
177
|
-
return
|
|
174
|
+
return LoginTransaction{}, false, nil
|
|
178
175
|
}
|
|
179
176
|
delete(f.transactions, hash)
|
|
180
177
|
return transaction, true, nil
|
|
@@ -309,12 +306,13 @@ func (f *fakeMFAStore) ConsumeRecoveryCode(_ context.Context, userID uuid.UUID,
|
|
|
309
306
|
// by the tests below (Refresh looks the user up after validating the
|
|
310
307
|
// token), the rest just satisfy the interface.
|
|
311
308
|
type fakeRepo struct {
|
|
312
|
-
user *
|
|
313
|
-
identity *
|
|
309
|
+
user *domain.User
|
|
310
|
+
identity *domain.Identity
|
|
314
311
|
updateIdentityErr error
|
|
315
312
|
updateUserErr error
|
|
316
313
|
failures map[string]int
|
|
317
314
|
lockedUntil map[string]time.Time
|
|
315
|
+
// go-scaffold:repository-stub-fields
|
|
318
316
|
}
|
|
319
317
|
|
|
320
318
|
// throttle: the fake keeps the counter in memory so the lockout path can be
|
|
@@ -346,36 +344,38 @@ func (f *fakeRepo) ClearLoginFailures(_ context.Context, key string) error {
|
|
|
346
344
|
return nil
|
|
347
345
|
}
|
|
348
346
|
|
|
349
|
-
func (f *fakeRepo) FindByEmail(context.Context, string) (*
|
|
350
|
-
return nil,
|
|
347
|
+
func (f *fakeRepo) FindByEmail(context.Context, string) (*domain.User, error) {
|
|
348
|
+
return nil, domain.ErrNotFound
|
|
351
349
|
}
|
|
352
|
-
func (f *fakeRepo) FindByID(_ context.Context, id uuid.UUID) (*
|
|
350
|
+
func (f *fakeRepo) FindByID(_ context.Context, id uuid.UUID) (*domain.User, error) {
|
|
353
351
|
if f.user != nil && f.user.ID == id {
|
|
354
352
|
return f.user, nil
|
|
355
353
|
}
|
|
356
|
-
return nil,
|
|
354
|
+
return nil, domain.ErrNotFound
|
|
357
355
|
}
|
|
358
|
-
func (f *fakeRepo) UpdateUser(_ context.Context, u *
|
|
356
|
+
func (f *fakeRepo) UpdateUser(_ context.Context, u *domain.User) error {
|
|
359
357
|
if f.updateUserErr != nil {
|
|
360
358
|
return f.updateUserErr
|
|
361
359
|
}
|
|
362
360
|
f.user = u
|
|
363
361
|
return nil
|
|
364
362
|
}
|
|
365
|
-
func (f *fakeRepo) FindAll(context.Context, int, int) ([]
|
|
366
|
-
func (f *fakeRepo) FindIdentity(_ context.Context, userID uuid.UUID, _
|
|
363
|
+
func (f *fakeRepo) FindAll(context.Context, int, int) ([]domain.User, error) { return nil, nil }
|
|
364
|
+
func (f *fakeRepo) FindIdentity(_ context.Context, userID uuid.UUID, _ domain.Provider) (*domain.Identity, error) {
|
|
367
365
|
if f.identity != nil && f.identity.UserID == userID {
|
|
368
366
|
copy := *f.identity
|
|
369
367
|
return ©, nil
|
|
370
368
|
}
|
|
371
|
-
return nil,
|
|
369
|
+
return nil, domain.ErrNotFound
|
|
370
|
+
}
|
|
371
|
+
func (f *fakeRepo) FindIdentityByProviderUID(context.Context, domain.Provider, string) (*domain.Identity, error) {
|
|
372
|
+
return nil, domain.ErrNotFound
|
|
372
373
|
}
|
|
373
|
-
func (f *fakeRepo)
|
|
374
|
-
return nil
|
|
374
|
+
func (f *fakeRepo) CreateUserWithIdentity(context.Context, *domain.User, *domain.Identity) error {
|
|
375
|
+
return nil
|
|
375
376
|
}
|
|
376
|
-
func (f *fakeRepo)
|
|
377
|
-
func (f *fakeRepo)
|
|
378
|
-
func (f *fakeRepo) UpdateIdentity(_ context.Context, i *model.Identity) error {
|
|
377
|
+
func (f *fakeRepo) CreateIdentity(context.Context, *domain.Identity) error { return nil }
|
|
378
|
+
func (f *fakeRepo) UpdateIdentity(_ context.Context, i *domain.Identity) error {
|
|
379
379
|
if f.updateIdentityErr != nil {
|
|
380
380
|
return f.updateIdentityErr
|
|
381
381
|
}
|
|
@@ -384,11 +384,28 @@ func (f *fakeRepo) UpdateIdentity(_ context.Context, i *model.Identity) error {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
// go-scaffold:user-fake-repo-methods
|
|
387
|
+
// go-scaffold:repository-stub-methods
|
|
387
388
|
|
|
388
389
|
type fakeMailer struct{}
|
|
389
390
|
|
|
390
391
|
func (fakeMailer) Send(context.Context, string, string, string) error { return nil }
|
|
391
392
|
|
|
393
|
+
type fakePasswordHasher struct{}
|
|
394
|
+
|
|
395
|
+
func (fakePasswordHasher) Hash(password string) (string, error) { return "hashed:" + password, nil }
|
|
396
|
+
|
|
397
|
+
func (fakePasswordHasher) Compare(hash, password string) error {
|
|
398
|
+
if hash != "hashed:"+password {
|
|
399
|
+
return errors.New("password does not match")
|
|
400
|
+
}
|
|
401
|
+
return nil
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// fakeRoles satisfies role's roleChecker interface structurally.
|
|
405
|
+
type fakeRoles struct{}
|
|
406
|
+
|
|
407
|
+
func (fakeRoles) CodeExists(context.Context, string) (bool, error) { return true, nil }
|
|
408
|
+
|
|
392
409
|
// go-scaffold:user-service-test-types
|
|
393
410
|
|
|
394
411
|
// newTestService is the one NewService call site every test below shares —
|
|
@@ -398,12 +415,14 @@ func (fakeMailer) Send(context.Context, string, string, string) error { return n
|
|
|
398
415
|
func newTestService(repo repository, tokens testTokenStore) *Service {
|
|
399
416
|
return NewService(Dependencies{
|
|
400
417
|
Repository: repo,
|
|
418
|
+
Passwords: fakePasswordHasher{},
|
|
401
419
|
RefreshTokens: tokens,
|
|
402
420
|
OAuthTransactions: tokens,
|
|
403
421
|
RecoveryTokens: tokens,
|
|
404
422
|
MFA: newFakeMFAStore(),
|
|
405
423
|
Mailer: fakeMailer{},
|
|
406
|
-
Providers:
|
|
424
|
+
Providers: NewProviderRegistry(),
|
|
425
|
+
Roles: fakeRoles{},
|
|
407
426
|
// go-scaffold:user-service-test-deps
|
|
408
427
|
}, AuthConfig{
|
|
409
428
|
JWTSecret: "test-secret",
|
|
@@ -416,25 +435,25 @@ func newTestService(repo repository, tokens testTokenStore) *Service {
|
|
|
416
435
|
|
|
417
436
|
func seedRefreshToken(ctx context.Context, tokens *fakeTokenStore, raw string, userID uuid.UUID, ttl time.Duration) error {
|
|
418
437
|
now := time.Now()
|
|
419
|
-
return tokens.SetRefreshToken(ctx, hashToken(raw),
|
|
438
|
+
return tokens.SetRefreshToken(ctx, hashToken(raw), RefreshTokenRecord{
|
|
420
439
|
UserID: userID,
|
|
421
440
|
ExpiresAt: now.Add(ttl),
|
|
422
441
|
AbsoluteExpiresAt: now.Add(24 * time.Hour),
|
|
423
442
|
})
|
|
424
443
|
}
|
|
425
444
|
|
|
426
|
-
func testRefreshTokenRecord(userID uuid.UUID) refreshTokenRecord {
|
|
427
|
-
now := time.Now()
|
|
428
|
-
return refreshTokenRecord{UserID: userID, ExpiresAt: now.Add(time.Hour), AbsoluteExpiresAt: now.Add(24 * time.Hour)}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
445
|
func status(t *testing.T, err error) int {
|
|
432
446
|
t.Helper()
|
|
433
|
-
var
|
|
434
|
-
if
|
|
435
|
-
|
|
447
|
+
var ruleErr *domain.RuleError
|
|
448
|
+
if errors.As(err, &ruleErr) {
|
|
449
|
+
switch ruleErr.Code {
|
|
450
|
+
case "AUTH_INVALID_TOKEN", "AUTH_INVALID_CREDENTIALS":
|
|
451
|
+
return http.StatusUnauthorized
|
|
452
|
+
case "AUTH_TOO_MANY_ATTEMPTS":
|
|
453
|
+
return http.StatusTooManyRequests
|
|
454
|
+
}
|
|
436
455
|
}
|
|
437
|
-
return
|
|
456
|
+
return http.StatusInternalServerError
|
|
438
457
|
}
|
|
439
458
|
|
|
440
459
|
func TestService_Refresh_RotatesTheToken(t *testing.T) {
|
|
@@ -445,7 +464,7 @@ func TestService_Refresh_RotatesTheToken(t *testing.T) {
|
|
|
445
464
|
if err := seedRefreshToken(ctx, tokens, rawOld, userID, time.Hour); err != nil {
|
|
446
465
|
t.Fatalf("seed: %v", err)
|
|
447
466
|
}
|
|
448
|
-
svc := newTestService(&fakeRepo{user: &
|
|
467
|
+
svc := newTestService(&fakeRepo{user: &domain.User{ID: userID, Email: "a@example.com"}}, tokens)
|
|
449
468
|
|
|
450
469
|
auth, err := svc.Refresh(ctx, rawOld)
|
|
451
470
|
if err != nil {
|
|
@@ -470,7 +489,7 @@ func TestService_Refresh_ConcurrentPresentationHasOneWinner(t *testing.T) {
|
|
|
470
489
|
if err := seedRefreshToken(ctx, tokens, raw, userID, time.Hour); err != nil {
|
|
471
490
|
t.Fatalf("seed: %v", err)
|
|
472
491
|
}
|
|
473
|
-
svc := newTestService(&fakeRepo{user: &
|
|
492
|
+
svc := newTestService(&fakeRepo{user: &domain.User{ID: userID, Email: "a@example.com"}}, tokens)
|
|
474
493
|
|
|
475
494
|
const callers = 32
|
|
476
495
|
start := make(chan struct{})
|
|
@@ -509,7 +528,7 @@ func TestService_Refresh_ReplayingARotatedOutTokenRevokesEverySession(t *testing
|
|
|
509
528
|
if err := seedRefreshToken(ctx, tokens, rawOtherSession, userID, time.Hour); err != nil {
|
|
510
529
|
t.Fatalf("seed: %v", err)
|
|
511
530
|
}
|
|
512
|
-
svc := newTestService(&fakeRepo{user: &
|
|
531
|
+
svc := newTestService(&fakeRepo{user: &domain.User{ID: userID, Email: "a@example.com"}}, tokens)
|
|
513
532
|
|
|
514
533
|
// rotate rawOld once — legitimate use, consumes + tombstones it
|
|
515
534
|
if _, err := svc.Refresh(ctx, rawOld); err != nil {
|
|
@@ -533,14 +552,14 @@ func TestService_Refresh_DoesNotExtendTheAbsoluteLifetime(t *testing.T) {
|
|
|
533
552
|
raw := "bounded-refresh-token"
|
|
534
553
|
issuedAt := time.Now()
|
|
535
554
|
absoluteExpiry := issuedAt.Add(2 * time.Hour)
|
|
536
|
-
if err := tokens.SetRefreshToken(ctx, hashToken(raw),
|
|
555
|
+
if err := tokens.SetRefreshToken(ctx, hashToken(raw), RefreshTokenRecord{
|
|
537
556
|
UserID: userID,
|
|
538
557
|
ExpiresAt: issuedAt.Add(time.Hour),
|
|
539
558
|
AbsoluteExpiresAt: absoluteExpiry,
|
|
540
559
|
}); err != nil {
|
|
541
560
|
t.Fatalf("seed: %v", err)
|
|
542
561
|
}
|
|
543
|
-
svc := newTestService(&fakeRepo{user: &
|
|
562
|
+
svc := newTestService(&fakeRepo{user: &domain.User{ID: userID}}, tokens)
|
|
544
563
|
rotationTime := issuedAt.Add(90 * time.Minute)
|
|
545
564
|
svc.now = func() time.Time { return rotationTime }
|
|
546
565
|
|
|
@@ -571,7 +590,7 @@ func TestService_Refresh_ReuseRevokeFailureFailsClosed(t *testing.T) {
|
|
|
571
590
|
if err := seedRefreshToken(ctx, tokens, rawOther, userID, time.Hour); err != nil {
|
|
572
591
|
t.Fatalf("seed other token: %v", err)
|
|
573
592
|
}
|
|
574
|
-
svc := newTestService(&fakeRepo{user: &
|
|
593
|
+
svc := newTestService(&fakeRepo{user: &domain.User{ID: userID}}, tokens)
|
|
575
594
|
if _, err := svc.Refresh(ctx, rawOld); err != nil {
|
|
576
595
|
t.Fatalf("initial rotation: %v", err)
|
|
577
596
|
}
|
|
@@ -592,7 +611,7 @@ func TestService_Refresh_UnknownTokenIsRejectedWithoutRevoking(t *testing.T) {
|
|
|
592
611
|
if err := seedRefreshToken(ctx, tokens, rawLegit, userID, time.Hour); err != nil {
|
|
593
612
|
t.Fatalf("seed: %v", err)
|
|
594
613
|
}
|
|
595
|
-
svc := newTestService(&fakeRepo{user: &
|
|
614
|
+
svc := newTestService(&fakeRepo{user: &domain.User{ID: userID}}, tokens)
|
|
596
615
|
|
|
597
616
|
_, err := svc.Refresh(ctx, "never-issued-token")
|
|
598
617
|
if got := status(t, err); got != http.StatusUnauthorized {
|
|
@@ -616,7 +635,7 @@ func TestService_LogoutAll_RevokesEverySessionButLeavesOthersAlone(t *testing.T)
|
|
|
616
635
|
t.Fatalf("seed: %v", err)
|
|
617
636
|
}
|
|
618
637
|
}
|
|
619
|
-
svc := newTestService(&fakeRepo{user: &
|
|
638
|
+
svc := newTestService(&fakeRepo{user: &domain.User{ID: userID}}, tokens)
|
|
620
639
|
|
|
621
640
|
if err := svc.LogoutAll(ctx, userID); err != nil {
|
|
622
641
|
t.Fatalf("unexpected error: %v", err)
|
|
@@ -637,8 +656,8 @@ func TestService_ResetPassword_DBFailureAllowsRetry(t *testing.T) {
|
|
|
637
656
|
userID := uuid.New()
|
|
638
657
|
passwordHash := "old-hash"
|
|
639
658
|
repo := &fakeRepo{
|
|
640
|
-
user:
|
|
641
|
-
identity:
|
|
659
|
+
user: &domain.User{ID: userID},
|
|
660
|
+
identity: &domain.Identity{ID: uuid.New(), UserID: userID, Provider: domain.ProviderLocal, PasswordHash: &passwordHash},
|
|
642
661
|
updateIdentityErr: errors.New("database unavailable"),
|
|
643
662
|
}
|
|
644
663
|
tokens := newFakeTokenStore()
|
|
@@ -662,8 +681,8 @@ func TestService_ResetPassword_SessionRevokeFailureRestoresTokenForRetry(t *test
|
|
|
662
681
|
userID := uuid.New()
|
|
663
682
|
passwordHash := "old-hash"
|
|
664
683
|
repo := &fakeRepo{
|
|
665
|
-
user: &
|
|
666
|
-
identity: &
|
|
684
|
+
user: &domain.User{ID: userID},
|
|
685
|
+
identity: &domain.Identity{ID: uuid.New(), UserID: userID, Provider: domain.ProviderLocal, PasswordHash: &passwordHash},
|
|
667
686
|
}
|
|
668
687
|
tokens := newFakeTokenStore()
|
|
669
688
|
raw := "reset-token-session-store-outage"
|
|
@@ -691,7 +710,7 @@ func TestService_VerifyEmail_DBFailureAllowsRetry(t *testing.T) {
|
|
|
691
710
|
ctx := context.Background()
|
|
692
711
|
userID := uuid.New()
|
|
693
712
|
repo := &fakeRepo{
|
|
694
|
-
user: &
|
|
713
|
+
user: &domain.User{ID: userID},
|
|
695
714
|
updateUserErr: errors.New("database unavailable"),
|
|
696
715
|
}
|
|
697
716
|
tokens := newFakeTokenStore()
|
|
@@ -715,8 +734,8 @@ func TestService_ResetPassword_DuplicateTokenIsRejected(t *testing.T) {
|
|
|
715
734
|
userID := uuid.New()
|
|
716
735
|
passwordHash := "old-hash"
|
|
717
736
|
repo := &fakeRepo{
|
|
718
|
-
user: &
|
|
719
|
-
identity: &
|
|
737
|
+
user: &domain.User{ID: userID},
|
|
738
|
+
identity: &domain.Identity{ID: uuid.New(), UserID: userID, Provider: domain.ProviderLocal, PasswordHash: &passwordHash},
|
|
720
739
|
}
|
|
721
740
|
tokens := newFakeTokenStore()
|
|
722
741
|
raw := "one-time-reset-token"
|
|
@@ -737,8 +756,8 @@ func TestService_ResetPassword_ConcurrentPresentationHasOneWinner(t *testing.T)
|
|
|
737
756
|
userID := uuid.New()
|
|
738
757
|
passwordHash := "old-hash"
|
|
739
758
|
repo := &fakeRepo{
|
|
740
|
-
user: &
|
|
741
|
-
identity: &
|
|
759
|
+
user: &domain.User{ID: userID},
|
|
760
|
+
identity: &domain.Identity{ID: uuid.New(), UserID: userID, Provider: domain.ProviderLocal, PasswordHash: &passwordHash},
|
|
742
761
|
}
|
|
743
762
|
tokens := newFakeTokenStore()
|
|
744
763
|
raw := "concurrent-reset-token"
|
|
@@ -774,7 +793,7 @@ func TestService_ResetPassword_ConcurrentPresentationHasOneWinner(t *testing.T)
|
|
|
774
793
|
func TestService_VerifyEmail_DuplicateTokenIsRejected(t *testing.T) {
|
|
775
794
|
ctx := context.Background()
|
|
776
795
|
userID := uuid.New()
|
|
777
|
-
repo := &fakeRepo{user: &
|
|
796
|
+
repo := &fakeRepo{user: &domain.User{ID: userID}}
|
|
778
797
|
tokens := newFakeTokenStore()
|
|
779
798
|
raw := "one-time-verification-token"
|
|
780
799
|
if err := tokens.SetEmailVerifyToken(ctx, hashToken(raw), userID, time.Hour); err != nil {
|
|
@@ -792,7 +811,7 @@ func TestService_VerifyEmail_DuplicateTokenIsRejected(t *testing.T) {
|
|
|
792
811
|
func TestService_VerifyEmail_ConcurrentPresentationHasOneWinner(t *testing.T) {
|
|
793
812
|
ctx := context.Background()
|
|
794
813
|
userID := uuid.New()
|
|
795
|
-
repo := &fakeRepo{user: &
|
|
814
|
+
repo := &fakeRepo{user: &domain.User{ID: userID}}
|
|
796
815
|
tokens := newFakeTokenStore()
|
|
797
816
|
raw := "concurrent-verification-token"
|
|
798
817
|
if err := tokens.SetEmailVerifyToken(ctx, hashToken(raw), userID, time.Hour); err != nil {
|
|
@@ -828,11 +847,11 @@ func TestService_VerifyEmail_ConcurrentPresentationHasOneWinner(t *testing.T) {
|
|
|
828
847
|
// account, so spreading attempts across a proxy pool doesn't help.
|
|
829
848
|
func TestService_Login_LocksTheAccountAfterRepeatedFailures(t *testing.T) {
|
|
830
849
|
repo := &fakeRepo{
|
|
831
|
-
user: &
|
|
850
|
+
user: &domain.User{ID: uuid.New(), Email: "a@example.com"},
|
|
832
851
|
failures: map[string]int{},
|
|
833
852
|
}
|
|
834
853
|
svc := newTestService(repo, newFakeTokenStore())
|
|
835
|
-
in :=
|
|
854
|
+
in := LoginInput{Email: "a@example.com", Password: "wrong"}
|
|
836
855
|
|
|
837
856
|
// the free attempts answer "wrong password", not "locked"
|
|
838
857
|
for i := 0; i < loginFreeAttempts; i++ {
|
|
@@ -864,9 +883,9 @@ func TestService_ForgotPassword_HasItsOwnCounter(t *testing.T) {
|
|
|
864
883
|
// code pulls the AppError code out, so a test asserting on behaviour doesn't
|
|
865
884
|
// have to care how the error is wrapped.
|
|
866
885
|
func code(err error) string {
|
|
867
|
-
var
|
|
868
|
-
if errors.As(err, &
|
|
869
|
-
return
|
|
886
|
+
var ruleErr *domain.RuleError
|
|
887
|
+
if errors.As(err, &ruleErr) {
|
|
888
|
+
return ruleErr.Code
|
|
870
889
|
}
|
|
871
890
|
return fmt.Sprintf("%v", err)
|
|
872
891
|
}
|
package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs}
RENAMED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
package
|
|
1
|
+
package application
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
6
|
"time"
|
|
7
7
|
|
|
8
|
-
"{{goModule}}/internal/app/user/
|
|
9
|
-
"{{goModule}}/internal/shared/apperror"
|
|
8
|
+
"{{goModule}}/internal/app/user/domain"
|
|
10
9
|
|
|
11
10
|
"github.com/google/uuid"
|
|
12
11
|
)
|
|
@@ -14,20 +13,20 @@ import (
|
|
|
14
13
|
// Refresh rotates a refresh token. A replayed, already-used token revokes
|
|
15
14
|
// every session for that user; an intentionally logged-out token is rejected
|
|
16
15
|
// without triggering reuse detection.
|
|
17
|
-
func (s *Service) Refresh(ctx context.Context, rawRefreshToken string) (*
|
|
16
|
+
func (s *Service) Refresh(ctx context.Context, rawRefreshToken string) (*AuthResponse, error) {
|
|
18
17
|
hash := hashToken(rawRefreshToken)
|
|
19
18
|
token, ok, err := s.refreshTokens.ConsumeRefreshToken(ctx, hash)
|
|
20
19
|
if err != nil {
|
|
21
|
-
return nil,
|
|
20
|
+
return nil, fmt.Errorf("consume refresh token: %w", err)
|
|
22
21
|
}
|
|
23
22
|
if !ok {
|
|
24
23
|
reusedBy, used, reuseErr := s.refreshTokens.IsRefreshTokenUsed(ctx, hash)
|
|
25
24
|
if reuseErr != nil {
|
|
26
|
-
return nil,
|
|
25
|
+
return nil, fmt.Errorf("check refresh token reuse: %w", reuseErr)
|
|
27
26
|
}
|
|
28
27
|
if used {
|
|
29
28
|
if revokeErr := s.refreshTokens.RevokeAllRefreshTokens(ctx, reusedBy); revokeErr != nil {
|
|
30
|
-
return nil,
|
|
29
|
+
return nil, fmt.Errorf("revoke sessions after refresh token reuse: %w", revokeErr)
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
return nil, errInvalidToken()
|
|
@@ -59,17 +58,18 @@ func (s *Service) LogoutAll(ctx context.Context, userID uuid.UUID) error {
|
|
|
59
58
|
return s.refreshTokens.RevokeAllRefreshTokens(ctx, userID)
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
func (s *Service) issueTokens(ctx context.Context, u *
|
|
61
|
+
func (s *Service) issueTokens(ctx context.Context, u *domain.User, absoluteExpiresAt ...time.Time) (*AuthResponse, error) {
|
|
63
62
|
access, err := s.issueAccessToken(
|
|
64
63
|
u.ID,
|
|
64
|
+
u.Role,
|
|
65
65
|
// go-scaffold:issue-access-token-args
|
|
66
66
|
)
|
|
67
67
|
if err != nil {
|
|
68
|
-
return nil,
|
|
68
|
+
return nil, fmt.Errorf("issue access token: %w", err)
|
|
69
69
|
}
|
|
70
70
|
refresh, err := randomToken()
|
|
71
71
|
if err != nil {
|
|
72
|
-
return nil,
|
|
72
|
+
return nil, fmt.Errorf("generate refresh token: %w", err)
|
|
73
73
|
}
|
|
74
74
|
clock := s.now
|
|
75
75
|
if clock == nil {
|
|
@@ -85,12 +85,12 @@ func (s *Service) issueTokens(ctx context.Context, u *model.User, absoluteExpire
|
|
|
85
85
|
expiresAt = abs
|
|
86
86
|
}
|
|
87
87
|
if !expiresAt.After(now) || !abs.After(now) {
|
|
88
|
-
return nil,
|
|
88
|
+
return nil, fmt.Errorf("refresh token lifetime is exhausted")
|
|
89
89
|
}
|
|
90
|
-
if err := s.refreshTokens.SetRefreshToken(ctx, hashToken(refresh),
|
|
91
|
-
return nil,
|
|
90
|
+
if err := s.refreshTokens.SetRefreshToken(ctx, hashToken(refresh), RefreshTokenRecord{UserID: u.ID, ExpiresAt: expiresAt, AbsoluteExpiresAt: abs}); err != nil {
|
|
91
|
+
return nil, fmt.Errorf("store refresh token: %w", err)
|
|
92
92
|
}
|
|
93
|
-
return &
|
|
93
|
+
return &AuthResponse{
|
|
94
94
|
AccessToken: access,
|
|
95
95
|
RefreshToken: refresh,
|
|
96
96
|
TokenType: "Bearer",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import "{{goModule}}/internal/app/user/ports"
|
|
4
|
+
|
|
5
|
+
// These aliases keep the use-case files readable while the actual outbound
|
|
6
|
+
// contracts remain owned by the user module's ports package.
|
|
7
|
+
type RefreshTokenStore = ports.RefreshTokenStore
|
|
8
|
+
type OAuthTransactionStore = ports.OAuthTransactionStore
|
|
9
|
+
type RecoveryTokenStore = ports.RecoveryTokenStore
|
|
10
|
+
type RefreshTokenRecord = ports.RefreshTokenRecord
|
|
11
|
+
type LoginTransaction = ports.LoginTransaction
|
|
12
|
+
type MFAEnrollment = ports.MFAEnrollment
|
|
13
|
+
type MFAChallenge = ports.MFAChallenge
|
|
14
|
+
type MFAStore = ports.MFAStore
|
package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs}
RENAMED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
package
|
|
1
|
+
package application
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"errors"
|
|
6
|
+
"fmt"
|
|
6
7
|
|
|
7
|
-
"{{goModule}}/internal/app/user/
|
|
8
|
-
"{{goModule}}/internal/shared/apperror"
|
|
8
|
+
"{{goModule}}/internal/app/user/domain"
|
|
9
9
|
"{{goModule}}/internal/shared/id"
|
|
10
10
|
|
|
11
11
|
"github.com/google/uuid"
|
|
12
|
-
"golang.org/x/crypto/bcrypt"
|
|
13
|
-
"gorm.io/gorm"
|
|
14
12
|
)
|
|
15
13
|
|
|
16
14
|
// wrapFindErr is shared by methods added through `generate method`. Keeping
|
|
@@ -18,13 +16,13 @@ import (
|
|
|
18
16
|
// does not need to import database-specific error packages just because a
|
|
19
17
|
// later method was generated.
|
|
20
18
|
func wrapFindErr(err error) error {
|
|
21
|
-
if errors.Is(err,
|
|
19
|
+
if errors.Is(err, domain.ErrNotFound) {
|
|
22
20
|
return errNotFound()
|
|
23
21
|
}
|
|
24
|
-
return
|
|
22
|
+
return fmt.Errorf("find user: %w", err)
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
func (s *Service) Get(ctx context.Context, userID uuid.UUID) (*
|
|
25
|
+
func (s *Service) Get(ctx context.Context, userID uuid.UUID) (*domain.User, error) {
|
|
28
26
|
u, err := s.repo.FindByID(ctx, userID)
|
|
29
27
|
if err != nil {
|
|
30
28
|
return nil, wrapFindErr(err)
|
|
@@ -34,32 +32,34 @@ func (s *Service) Get(ctx context.Context, userID uuid.UUID) (*model.User, error
|
|
|
34
32
|
|
|
35
33
|
// List is kept for the optional RBAC admin routes. It is not part of the
|
|
36
34
|
// public self-service auth surface until that feature is installed.
|
|
37
|
-
func (s *Service) List(ctx context.Context, limit, offset int) ([]
|
|
35
|
+
func (s *Service) List(ctx context.Context, limit, offset int) ([]domain.User, error) {
|
|
38
36
|
items, err := s.repo.FindAll(ctx, limit, offset)
|
|
39
37
|
if err != nil {
|
|
40
|
-
return nil,
|
|
38
|
+
return nil, fmt.Errorf("list users: %w", err)
|
|
41
39
|
}
|
|
42
40
|
return items, nil
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
// EnsureUser is idempotent and used only by cmd/seed. It never resets an
|
|
46
44
|
// existing user's password as a side effect of a later deployment.
|
|
47
|
-
func (s *Service) EnsureUser(ctx context.Context, email, password, name string) (*
|
|
45
|
+
func (s *Service) EnsureUser(ctx context.Context, email, password, name string) (*domain.User, error) {
|
|
48
46
|
email = normalizeEmail(email)
|
|
49
47
|
if u, err := s.repo.FindByEmail(ctx, email); err == nil {
|
|
50
48
|
return u, nil
|
|
49
|
+
} else if !errors.Is(err, domain.ErrNotFound) {
|
|
50
|
+
return nil, fmt.Errorf("find existing user: %w", err)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
hash, err :=
|
|
53
|
+
hash, err := s.passwords.Hash(password)
|
|
54
54
|
if err != nil {
|
|
55
|
-
return nil,
|
|
55
|
+
return nil, fmt.Errorf("hash password: %w", err)
|
|
56
56
|
}
|
|
57
57
|
hashStr := string(hash)
|
|
58
58
|
|
|
59
|
-
u := &
|
|
60
|
-
i := &
|
|
59
|
+
u := &domain.User{ID: id.New(), Email: email, Name: name, Role: domain.DefaultRole}
|
|
60
|
+
i := &domain.Identity{ID: id.New(), Provider: domain.ProviderLocal, PasswordHash: &hashStr}
|
|
61
61
|
if err := s.repo.CreateUserWithIdentity(ctx, u, i); err != nil {
|
|
62
|
-
return nil,
|
|
62
|
+
return nil, fmt.Errorf("create user: %w", err)
|
|
63
63
|
}
|
|
64
64
|
return u, nil
|
|
65
65
|
}
|