@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package application
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
@@ -7,39 +7,38 @@ import (
|
|
|
7
7
|
"strings"
|
|
8
8
|
"testing"
|
|
9
9
|
"time"
|
|
10
|
-
|
|
11
|
-
"{{goModule}}/internal/app/user/application"
|
|
12
10
|
)
|
|
13
11
|
|
|
14
12
|
type fakeLoginProvider struct {
|
|
15
13
|
name string
|
|
16
|
-
beginFn func(
|
|
17
|
-
completeFn func(
|
|
14
|
+
beginFn func(LoginStartInput) (Authorization, error)
|
|
15
|
+
completeFn func(LoginCompleteInput) (ExternalIdentity, error)
|
|
18
16
|
beginCnt int
|
|
19
17
|
completeCnt int
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
func (p *fakeLoginProvider) Name() string { return p.name }
|
|
23
21
|
|
|
24
|
-
func (p *fakeLoginProvider) Begin(_ context.Context, in
|
|
22
|
+
func (p *fakeLoginProvider) Begin(_ context.Context, in LoginStartInput) (Authorization, error) {
|
|
25
23
|
p.beginCnt++
|
|
26
24
|
return p.beginFn(in)
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
func (p *fakeLoginProvider) Complete(_ context.Context, in
|
|
27
|
+
func (p *fakeLoginProvider) Complete(_ context.Context, in LoginCompleteInput) (ExternalIdentity, error) {
|
|
30
28
|
p.completeCnt++
|
|
31
29
|
return p.completeFn(in)
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
func newTestServiceWithProviders(repo repository, tokens testTokenStore, providers ...
|
|
32
|
+
func newTestServiceWithProviders(repo repository, tokens testTokenStore, providers ...LoginProvider) *Service {
|
|
35
33
|
return NewService(Dependencies{
|
|
36
34
|
Repository: repo,
|
|
35
|
+
Passwords: fakePasswordHasher{},
|
|
37
36
|
RefreshTokens: tokens,
|
|
38
37
|
OAuthTransactions: tokens,
|
|
39
38
|
RecoveryTokens: tokens,
|
|
40
39
|
MFA: newFakeMFAStore(),
|
|
41
40
|
Mailer: fakeMailer{},
|
|
42
|
-
Providers:
|
|
41
|
+
Providers: NewProviderRegistry(providers...),
|
|
43
42
|
}, AuthConfig{
|
|
44
43
|
JWTSecret: "test-secret",
|
|
45
44
|
JWTAccessTTL: time.Minute,
|
|
@@ -47,49 +46,49 @@ func newTestServiceWithProviders(repo repository, tokens testTokenStore, provide
|
|
|
47
46
|
})
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
func oauthErrorCode(t *testing.T, err error)
|
|
49
|
+
func oauthErrorCode(t *testing.T, err error) OAuthErrorCode {
|
|
51
50
|
t.Helper()
|
|
52
|
-
var oauthErr *
|
|
51
|
+
var oauthErr *OAuthError
|
|
53
52
|
if !errors.As(err, &oauthErr) {
|
|
54
|
-
t.Fatalf("expected *
|
|
53
|
+
t.Fatalf("expected *OAuthError, got %T: %v", err, err)
|
|
55
54
|
}
|
|
56
55
|
return oauthErr.Code
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
const testOAuthVerifier = "client-code-verifier-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
60
59
|
|
|
61
|
-
func validLoginStart()
|
|
62
|
-
return
|
|
60
|
+
func validLoginStart() LoginStartInput {
|
|
61
|
+
return LoginStartInput{
|
|
63
62
|
State: "client state",
|
|
64
63
|
CodeChallenge: pkceChallenge(testOAuthVerifier),
|
|
65
64
|
CodeChallengeMethod: "S256",
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
func validLoginExchange()
|
|
70
|
-
return
|
|
68
|
+
func validLoginExchange() LoginExchangeInput {
|
|
69
|
+
return LoginExchangeInput{Code: "code", State: "client state", CodeVerifier: testOAuthVerifier}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
func validFakeProvider(t *testing.T) (*fakeLoginProvider, *Service) {
|
|
74
73
|
t.Helper()
|
|
75
74
|
provider := &fakeLoginProvider{name: "fake"}
|
|
76
|
-
provider.beginFn = func(in
|
|
75
|
+
provider.beginFn = func(in LoginStartInput) (Authorization, error) {
|
|
77
76
|
expected := validLoginStart()
|
|
78
77
|
if in.State != expected.State || in.CodeChallenge != expected.CodeChallenge || in.CodeChallengeMethod != expected.CodeChallengeMethod || strings.TrimSpace(in.Nonce) == "" {
|
|
79
78
|
t.Fatalf("unexpected begin input: %+v", in)
|
|
80
79
|
}
|
|
81
|
-
return
|
|
80
|
+
return Authorization{URL: "https://provider.example.test/authorize?" + url.Values{
|
|
82
81
|
"state": {in.State},
|
|
83
82
|
"code_challenge": {in.CodeChallenge},
|
|
84
83
|
"code_challenge_method": {in.CodeChallengeMethod},
|
|
85
84
|
"nonce": {in.Nonce},
|
|
86
85
|
}.Encode()}, nil
|
|
87
86
|
}
|
|
88
|
-
provider.completeFn = func(in
|
|
87
|
+
provider.completeFn = func(in LoginCompleteInput) (ExternalIdentity, error) {
|
|
89
88
|
if in.Code != "code" || in.CodeVerifier != testOAuthVerifier || strings.TrimSpace(in.Nonce) == "" {
|
|
90
89
|
t.Fatalf("unexpected complete input: %+v", in)
|
|
91
90
|
}
|
|
92
|
-
return
|
|
91
|
+
return ExternalIdentity{
|
|
93
92
|
Provider: "fake",
|
|
94
93
|
Subject: "subject-1",
|
|
95
94
|
Email: "user@example.com",
|
|
@@ -122,20 +121,20 @@ func TestService_BeginLoginRejectsMissingStateOrNonS256PKCE(t *testing.T) {
|
|
|
122
121
|
provider, svc := validFakeProvider(t)
|
|
123
122
|
tests := []struct {
|
|
124
123
|
name string
|
|
125
|
-
in
|
|
124
|
+
in LoginStartInput
|
|
126
125
|
}{
|
|
127
|
-
{name: "missing state", in:
|
|
128
|
-
{name: "missing challenge", in:
|
|
129
|
-
{name: "plain challenge method", in:
|
|
130
|
-
{name: "short challenge", in:
|
|
131
|
-
{name: "invalid challenge character", in:
|
|
132
|
-
{name: "lowercase challenge method", in:
|
|
126
|
+
{name: "missing state", in: LoginStartInput{CodeChallenge: "challenge", CodeChallengeMethod: "S256"}},
|
|
127
|
+
{name: "missing challenge", in: LoginStartInput{State: "state", CodeChallengeMethod: "S256"}},
|
|
128
|
+
{name: "plain challenge method", in: LoginStartInput{State: "state", CodeChallenge: "challenge", CodeChallengeMethod: "plain"}},
|
|
129
|
+
{name: "short challenge", in: LoginStartInput{State: "state", CodeChallenge: "short", CodeChallengeMethod: "S256"}},
|
|
130
|
+
{name: "invalid challenge character", in: LoginStartInput{State: "state", CodeChallenge: strings.Repeat("!", 43), CodeChallengeMethod: "S256"}},
|
|
131
|
+
{name: "lowercase challenge method", in: LoginStartInput{State: "state", CodeChallenge: strings.Repeat("A", 43), CodeChallengeMethod: "s256"}},
|
|
133
132
|
}
|
|
134
133
|
for _, tt := range tests {
|
|
135
134
|
t.Run(tt.name, func(t *testing.T) {
|
|
136
135
|
_, err := svc.BeginLogin(context.Background(), "fake", tt.in)
|
|
137
|
-
if got := oauthErrorCode(t, err); got !=
|
|
138
|
-
t.Fatalf("oauth code = %q, want %q", got,
|
|
136
|
+
if got := oauthErrorCode(t, err); got != OAuthStateInvalid {
|
|
137
|
+
t.Fatalf("oauth code = %q, want %q", got, OAuthStateInvalid)
|
|
139
138
|
}
|
|
140
139
|
if provider.beginCnt != 0 {
|
|
141
140
|
t.Fatal("provider must not receive an invalid state/PKCE request")
|
|
@@ -163,8 +162,8 @@ func TestService_ExchangeLoginRejectsIdentityFromAnotherProvider(t *testing.T) {
|
|
|
163
162
|
if _, err := svc.BeginLogin(context.Background(), "fake", validLoginStart()); err != nil {
|
|
164
163
|
t.Fatalf("begin login: %v", err)
|
|
165
164
|
}
|
|
166
|
-
provider.completeFn = func(
|
|
167
|
-
return
|
|
165
|
+
provider.completeFn = func(LoginCompleteInput) (ExternalIdentity, error) {
|
|
166
|
+
return ExternalIdentity{
|
|
168
167
|
Provider: "another-provider",
|
|
169
168
|
Subject: "subject-1",
|
|
170
169
|
Email: "user@example.com",
|
|
@@ -172,8 +171,8 @@ func TestService_ExchangeLoginRejectsIdentityFromAnotherProvider(t *testing.T) {
|
|
|
172
171
|
}, nil
|
|
173
172
|
}
|
|
174
173
|
_, err := svc.ExchangeLogin(context.Background(), "fake", validLoginExchange())
|
|
175
|
-
if got := oauthErrorCode(t, err); got !=
|
|
176
|
-
t.Fatalf("oauth code = %q, want %q", got,
|
|
174
|
+
if got := oauthErrorCode(t, err); got != OAuthFailed {
|
|
175
|
+
t.Fatalf("oauth code = %q, want %q", got, OAuthFailed)
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
178
|
|
|
@@ -181,20 +180,20 @@ func TestService_ExchangeLoginMapsProviderErrorsToControlledCodes(t *testing.T)
|
|
|
181
180
|
tests := []struct {
|
|
182
181
|
name string
|
|
183
182
|
providerError error
|
|
184
|
-
wantCode
|
|
183
|
+
wantCode OAuthErrorCode
|
|
185
184
|
}{
|
|
186
|
-
{name: "provider exchange failure", providerError:
|
|
187
|
-
{name: "provider unavailable", providerError:
|
|
185
|
+
{name: "provider exchange failure", providerError: NewProviderFailure(errors.New("exchange failed")), wantCode: OAuthFailed},
|
|
186
|
+
{name: "provider unavailable", providerError: NewProviderUnavailable(errors.New("upstream timeout")), wantCode: OAuthProviderUnavailable},
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
for _, tt := range tests {
|
|
191
|
-
|
|
190
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
192
191
|
provider, svc := validFakeProvider(t)
|
|
193
192
|
if _, err := svc.BeginLogin(context.Background(), "fake", validLoginStart()); err != nil {
|
|
194
193
|
t.Fatalf("begin login: %v", err)
|
|
195
194
|
}
|
|
196
|
-
provider.completeFn = func(
|
|
197
|
-
return
|
|
195
|
+
provider.completeFn = func(LoginCompleteInput) (ExternalIdentity, error) {
|
|
196
|
+
return ExternalIdentity{}, tt.providerError
|
|
198
197
|
}
|
|
199
198
|
_, err := svc.ExchangeLogin(context.Background(), "fake", validLoginExchange())
|
|
200
199
|
if got := oauthErrorCode(t, err); got != tt.wantCode {
|
|
@@ -208,12 +207,12 @@ func TestService_ExchangeLoginRejectsMissingStatePKCEAndCode(t *testing.T) {
|
|
|
208
207
|
_, svc := validFakeProvider(t)
|
|
209
208
|
tests := []struct {
|
|
210
209
|
name string
|
|
211
|
-
in
|
|
212
|
-
want
|
|
210
|
+
in LoginExchangeInput
|
|
211
|
+
want OAuthErrorCode
|
|
213
212
|
}{
|
|
214
|
-
{name: "missing state", in:
|
|
215
|
-
{name: "missing verifier", in:
|
|
216
|
-
{name: "missing code", in:
|
|
213
|
+
{name: "missing state", in: LoginExchangeInput{Code: "code", CodeVerifier: testOAuthVerifier}, want: OAuthStateInvalid},
|
|
214
|
+
{name: "missing verifier", in: LoginExchangeInput{Code: "code", State: "state"}, want: OAuthStateInvalid},
|
|
215
|
+
{name: "missing code", in: LoginExchangeInput{State: "state", CodeVerifier: testOAuthVerifier}, want: OAuthFailed},
|
|
217
216
|
}
|
|
218
217
|
for _, tt := range tests {
|
|
219
218
|
t.Run(tt.name, func(t *testing.T) {
|
|
@@ -232,10 +231,10 @@ func TestService_ExchangeLoginConsumesStateAndBindsThePKCEVerifier(t *testing.T)
|
|
|
232
231
|
}
|
|
233
232
|
wrong := validLoginExchange()
|
|
234
233
|
wrong.CodeVerifier = testOAuthVerifier + "x"
|
|
235
|
-
if _, err := svc.ExchangeLogin(context.Background(), "fake", wrong); oauthErrorCode(t, err) !=
|
|
234
|
+
if _, err := svc.ExchangeLogin(context.Background(), "fake", wrong); oauthErrorCode(t, err) != OAuthStateInvalid {
|
|
236
235
|
t.Fatalf("wrong verifier should be rejected as state invalid: %v", err)
|
|
237
236
|
}
|
|
238
|
-
if _, err := svc.ExchangeLogin(context.Background(), "fake", validLoginExchange()); oauthErrorCode(t, err) !=
|
|
237
|
+
if _, err := svc.ExchangeLogin(context.Background(), "fake", validLoginExchange()); oauthErrorCode(t, err) != OAuthStateInvalid {
|
|
239
238
|
t.Fatalf("a rejected exchange must consume the one-time transaction: %v", err)
|
|
240
239
|
}
|
|
241
240
|
if provider.completeCnt != 0 {
|
|
@@ -249,18 +248,18 @@ func TestService_UnconfiguredProviderDoesNotBuildAURL(t *testing.T) {
|
|
|
249
248
|
if start != nil {
|
|
250
249
|
t.Fatalf("unconfigured provider returned a login start: %+v", start)
|
|
251
250
|
}
|
|
252
|
-
if got := oauthErrorCode(t, err); got !=
|
|
253
|
-
t.Fatalf("oauth code = %q, want %q", got,
|
|
251
|
+
if got := oauthErrorCode(t, err); got != OAuthProviderUnavailable {
|
|
252
|
+
t.Fatalf("oauth code = %q, want %q", got, OAuthProviderUnavailable)
|
|
254
253
|
}
|
|
255
254
|
_, err = svc.ExchangeLogin(context.Background(), "google", validLoginExchange())
|
|
256
|
-
if got := oauthErrorCode(t, err); got !=
|
|
257
|
-
t.Fatalf("exchange oauth code = %q, want %q", got,
|
|
255
|
+
if got := oauthErrorCode(t, err); got != OAuthProviderUnavailable {
|
|
256
|
+
t.Fatalf("exchange oauth code = %q, want %q", got, OAuthProviderUnavailable)
|
|
258
257
|
}
|
|
259
258
|
}
|
|
260
259
|
|
|
261
260
|
func TestProviderRegistry_UsesProviderNameAsTheOnlyLookupKey(t *testing.T) {
|
|
262
261
|
provider := &fakeLoginProvider{name: "fake"}
|
|
263
|
-
registry :=
|
|
262
|
+
registry := NewProviderRegistry(provider)
|
|
264
263
|
if got, ok := registry.Lookup("fake"); !ok || got != provider {
|
|
265
264
|
t.Fatalf("registry lookup = (%v, %v), want fake provider", got, ok)
|
|
266
265
|
}
|
|
@@ -270,17 +269,17 @@ func TestProviderRegistry_UsesProviderNameAsTheOnlyLookupKey(t *testing.T) {
|
|
|
270
269
|
}
|
|
271
270
|
|
|
272
271
|
func TestService_BeginLoginRejectsEmptyProviderAuthorizationURL(t *testing.T) {
|
|
273
|
-
provider := &fakeLoginProvider{name: "fake", beginFn: func(
|
|
274
|
-
return
|
|
272
|
+
provider := &fakeLoginProvider{name: "fake", beginFn: func(LoginStartInput) (Authorization, error) {
|
|
273
|
+
return Authorization{}, nil
|
|
275
274
|
}}
|
|
276
275
|
svc := newTestServiceWithProviders(&fakeRepo{}, newFakeTokenStore(), provider)
|
|
277
276
|
_, err := svc.BeginLogin(context.Background(), "fake", validLoginStart())
|
|
278
|
-
if got := oauthErrorCode(t, err); got !=
|
|
279
|
-
t.Fatalf("oauth code = %q, want %q", got,
|
|
277
|
+
if got := oauthErrorCode(t, err); got != OAuthProviderUnavailable {
|
|
278
|
+
t.Fatalf("oauth code = %q, want %q", got, OAuthProviderUnavailable)
|
|
280
279
|
}
|
|
281
280
|
if strings.Contains(err.Error(), "Location") {
|
|
282
281
|
t.Fatalf("empty authorization must not become a redirect: %v", err)
|
|
283
282
|
}
|
|
284
283
|
}
|
|
285
284
|
|
|
286
|
-
var _
|
|
285
|
+
var _ LoginProvider = (*fakeLoginProvider)(nil)
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// Package application contains auth use cases and ports that are independent
|
|
2
|
-
// of Gin, GORM models, and HTTP error payloads. Adapters in the parent user
|
|
3
|
-
// package translate those contracts at the feature boundary.
|
|
4
1
|
package application
|
|
5
2
|
|
|
6
3
|
import (
|
|
@@ -8,55 +5,24 @@ import (
|
|
|
8
5
|
"errors"
|
|
9
6
|
"fmt"
|
|
10
7
|
|
|
8
|
+
"{{goModule}}/internal/app/user/domain"
|
|
9
|
+
"{{goModule}}/internal/app/user/ports"
|
|
10
|
+
|
|
11
11
|
"github.com/google/uuid"
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
var ErrInvalidToken = errors.New("invalid or expired one-time token")
|
|
15
|
-
|
|
16
|
-
type Identity struct {
|
|
17
|
-
ID uuid.UUID
|
|
18
|
-
UserID uuid.UUID
|
|
19
|
-
PasswordHash string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type User struct {
|
|
23
|
-
ID uuid.UUID
|
|
24
|
-
EmailVerified bool
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// RecoveryRepository is an outbound port owned by the recovery use case.
|
|
28
|
-
// Implementations map persistence rows to these plain application values.
|
|
29
|
-
type RecoveryRepository interface {
|
|
30
|
-
FindIdentity(context.Context, uuid.UUID) (*Identity, error)
|
|
31
|
-
UpdateIdentity(context.Context, *Identity) error
|
|
32
|
-
FindUser(context.Context, uuid.UUID) (*User, error)
|
|
33
|
-
UpdateUser(context.Context, *User) error
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type RecoveryTokens interface {
|
|
37
|
-
WithTransaction(context.Context, func(context.Context) error) error
|
|
38
|
-
ConsumePasswordResetToken(context.Context, string) (uuid.UUID, bool, error)
|
|
39
|
-
ConsumeEmailVerifyToken(context.Context, string) (uuid.UUID, bool, error)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type PasswordHasher interface {
|
|
43
|
-
Hash(string) (string, error)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
14
|
type Recovery struct {
|
|
47
|
-
repo
|
|
48
|
-
tokens
|
|
15
|
+
repo ports.UserRepository
|
|
16
|
+
tokens ports.RecoveryTokenStore
|
|
49
17
|
hasher PasswordHasher
|
|
50
18
|
}
|
|
51
19
|
|
|
52
|
-
func NewRecovery(repo
|
|
20
|
+
func NewRecovery(repo ports.UserRepository, tokens ports.RecoveryTokenStore, hasher PasswordHasher) *Recovery {
|
|
53
21
|
return &Recovery{repo: repo, tokens: tokens, hasher: hasher}
|
|
54
22
|
}
|
|
55
23
|
|
|
56
|
-
// ResetPassword consumes the token and updates the identity in one
|
|
57
|
-
// work.
|
|
58
|
-
// transaction; the contract is deliberately a port so another adapter can
|
|
59
|
-
// provide an equivalent retry-safe boundary.
|
|
24
|
+
// ResetPassword consumes the token and updates the local identity in one
|
|
25
|
+
// unit of work. The token adapter owns the transaction boundary.
|
|
60
26
|
func (r *Recovery) ResetPassword(ctx context.Context, tokenHash, newPassword string) (uuid.UUID, error) {
|
|
61
27
|
hash, err := r.hasher.Hash(newPassword)
|
|
62
28
|
if err != nil {
|
|
@@ -70,14 +36,17 @@ func (r *Recovery) ResetPassword(ctx context.Context, tokenHash, newPassword str
|
|
|
70
36
|
return fmt.Errorf("consume password reset token: %w", err)
|
|
71
37
|
}
|
|
72
38
|
if !ok {
|
|
73
|
-
return ErrInvalidToken
|
|
39
|
+
return domain.ErrInvalidToken
|
|
74
40
|
}
|
|
75
41
|
|
|
76
|
-
identity, err := r.repo.FindIdentity(txctx, id)
|
|
42
|
+
identity, err := r.repo.FindIdentity(txctx, id, domain.ProviderLocal)
|
|
77
43
|
if err != nil {
|
|
78
44
|
return fmt.Errorf("find local identity: %w", err)
|
|
79
45
|
}
|
|
80
|
-
identity.PasswordHash
|
|
46
|
+
if identity.PasswordHash == nil {
|
|
47
|
+
return errors.New("local identity has no password")
|
|
48
|
+
}
|
|
49
|
+
identity.PasswordHash = &hash
|
|
81
50
|
if err := r.repo.UpdateIdentity(txctx, identity); err != nil {
|
|
82
51
|
return fmt.Errorf("update password: %w", err)
|
|
83
52
|
}
|
|
@@ -95,10 +64,10 @@ func (r *Recovery) VerifyEmail(ctx context.Context, tokenHash string) (uuid.UUID
|
|
|
95
64
|
return fmt.Errorf("consume email verification token: %w", err)
|
|
96
65
|
}
|
|
97
66
|
if !ok {
|
|
98
|
-
return ErrInvalidToken
|
|
67
|
+
return domain.ErrInvalidToken
|
|
99
68
|
}
|
|
100
69
|
|
|
101
|
-
user, err := r.repo.
|
|
70
|
+
user, err := r.repo.FindByID(txctx, id)
|
|
102
71
|
if err != nil {
|
|
103
72
|
return fmt.Errorf("find user for email verification: %w", err)
|
|
104
73
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package application
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
@@ -6,9 +6,7 @@ import (
|
|
|
6
6
|
"fmt"
|
|
7
7
|
"log/slog"
|
|
8
8
|
|
|
9
|
-
"{{goModule}}/internal/app/user/
|
|
10
|
-
"{{goModule}}/internal/app/user/model"
|
|
11
|
-
"{{goModule}}/internal/shared/apperror"
|
|
9
|
+
"{{goModule}}/internal/app/user/domain"
|
|
12
10
|
|
|
13
11
|
"github.com/google/uuid"
|
|
14
12
|
)
|
|
@@ -29,17 +27,17 @@ func (s *Service) ForgotPassword(ctx context.Context, email string) error {
|
|
|
29
27
|
if err != nil {
|
|
30
28
|
return nil
|
|
31
29
|
}
|
|
32
|
-
ident, err := s.repo.FindIdentity(ctx, u.ID,
|
|
30
|
+
ident, err := s.repo.FindIdentity(ctx, u.ID, domain.ProviderLocal)
|
|
33
31
|
if err != nil || ident.PasswordHash == nil {
|
|
34
32
|
return nil
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
raw, err := randomToken()
|
|
38
36
|
if err != nil {
|
|
39
|
-
return
|
|
37
|
+
return fmt.Errorf("generate password reset token: %w", err)
|
|
40
38
|
}
|
|
41
39
|
if err := s.recoveryTokens.SetPasswordResetToken(ctx, hashToken(raw), u.ID, s.config.PasswordResetTTL); err != nil {
|
|
42
|
-
return
|
|
40
|
+
return fmt.Errorf("store password reset token: %w", err)
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
link := s.config.PasswordResetURL + "?token=" + raw
|
|
@@ -58,23 +56,23 @@ func (s *Service) ResetPassword(ctx context.Context, rawToken, newPassword strin
|
|
|
58
56
|
tokenHash := hashToken(rawToken)
|
|
59
57
|
userID, err := s.recovery.ResetPassword(ctx, tokenHash, newPassword)
|
|
60
58
|
if err != nil {
|
|
61
|
-
if errors.Is(err,
|
|
59
|
+
if errors.Is(err, domain.ErrInvalidToken) {
|
|
62
60
|
return errInvalidToken()
|
|
63
61
|
}
|
|
64
|
-
return
|
|
62
|
+
return fmt.Errorf("reset password: %w", err)
|
|
65
63
|
}
|
|
66
64
|
if err := s.refreshTokens.RevokeAllRefreshTokens(ctx, userID); err != nil {
|
|
67
65
|
if restoreErr := s.recoveryTokens.SetPasswordResetToken(ctx, tokenHash, userID, s.config.PasswordResetTTL); restoreErr != nil {
|
|
68
|
-
return
|
|
66
|
+
return fmt.Errorf("revoke sessions after password reset: %w; restore reset token for retry: %v", err, restoreErr)
|
|
69
67
|
}
|
|
70
|
-
return
|
|
68
|
+
return fmt.Errorf("revoke sessions after password reset (reset token restored for retry): %w", err)
|
|
71
69
|
}
|
|
72
70
|
return nil
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
// sendVerificationEmail issues a one-time token and sends its link. It is
|
|
76
74
|
// shared by registration and the authenticated resend flow.
|
|
77
|
-
func (s *Service) sendVerificationEmail(ctx context.Context, u *
|
|
75
|
+
func (s *Service) sendVerificationEmail(ctx context.Context, u *domain.User) {
|
|
78
76
|
raw, err := randomToken()
|
|
79
77
|
if err != nil {
|
|
80
78
|
slog.Error("generate email verification token", "error", err)
|
|
@@ -93,7 +91,7 @@ func (s *Service) sendVerificationEmail(ctx context.Context, u *model.User) {
|
|
|
93
91
|
func (s *Service) ResendVerificationEmail(ctx context.Context, userID uuid.UUID) error {
|
|
94
92
|
u, err := s.repo.FindByID(ctx, userID)
|
|
95
93
|
if err != nil {
|
|
96
|
-
return
|
|
94
|
+
return wrapFindErr(err)
|
|
97
95
|
}
|
|
98
96
|
if u.EmailVerified {
|
|
99
97
|
return errAlreadyVerified()
|
|
@@ -105,10 +103,10 @@ func (s *Service) ResendVerificationEmail(ctx context.Context, userID uuid.UUID)
|
|
|
105
103
|
func (s *Service) VerifyEmail(ctx context.Context, rawToken string) error {
|
|
106
104
|
_, err := s.recovery.VerifyEmail(ctx, hashToken(rawToken))
|
|
107
105
|
if err != nil {
|
|
108
|
-
if errors.Is(err,
|
|
106
|
+
if errors.Is(err, domain.ErrInvalidToken) {
|
|
109
107
|
return errInvalidToken()
|
|
110
108
|
}
|
|
111
|
-
return
|
|
109
|
+
return fmt.Errorf("verify email: %w", err)
|
|
112
110
|
}
|
|
113
111
|
return nil
|
|
114
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
|