@nakedev/go-scaffold 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +598 -306
- package/dist/commands/auth.js +65 -23
- package/dist/commands/check.js +281 -0
- package/dist/commands/config.js +50 -0
- package/dist/commands/create.js +33 -2
- package/dist/commands/generate.js +29 -3
- package/dist/commands/method.js +74 -63
- package/dist/commands/migration.js +2 -2
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +21 -10
- package/dist/commands/undo.js +11 -3
- package/dist/commands/worker.js +15 -5
- package/dist/index.js +198 -59
- package/dist/prompts/auth-wizard.js +40 -6
- package/dist/prompts/create-wizard.js +42 -1
- package/dist/prompts/generate-wizard.js +89 -9
- package/dist/templates/auth-manifest.js +50 -19
- package/dist/templates/create-manifest.js +8 -0
- package/dist/templates/module-manifest.js +84 -26
- package/dist/templates/rbac-manifest.js +16 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +8 -0
- package/dist/utils/auth-patcher.js +124 -33
- package/dist/utils/config.js +167 -4
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +32 -30
- package/dist/utils/marker-patch.js +7 -1
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/module-profile.js +32 -0
- package/dist/utils/platform-patcher.js +56 -7
- package/dist/utils/rbac-patcher.js +89 -210
- package/package.json +7 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
- package/templates/add/auth/docs/login.yaml.hbs +11 -1
- package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
- package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
- package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
- package/templates/add/auth/docs/register.yaml.hbs +7 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
- package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
- package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
- package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
- package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
- package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
- 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/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
- 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/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
- 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/application/external_login.go.hbs +198 -0
- package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
- package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
- package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
- package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
- package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
- package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
- package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
- package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
- 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/internal/platform/authprovider/google/google.go.hbs +389 -0
- package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
- package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
- 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 +48 -0
- 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 +358 -121
- package/templates/create/base/.env.example.hbs +0 -1
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +279 -67
- package/templates/create/base/Makefile.hbs +2 -1
- package/templates/create/base/README.md.hbs +115 -32
- package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
- package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
- package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
- package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
- package/templates/create/features/docs/architecture.md.hbs +92 -32
- package/templates/create/features/docs/patterns.md.hbs +137 -91
- package/templates/create/features/docs/techstack.md.hbs +18 -3
- 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 -357
- package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
- package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
- package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
- package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
- package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
- 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/dto.go.hbs +0 -36
- package/templates/generate/module/errors.go.hbs +0 -33
- package/templates/generate/module/handler.go.hbs +0 -134
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -48
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/service.go.hbs +0 -45
- 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/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -108
- package/templates/generate/module/service_test.go.hbs +0 -161
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"log/slog"
|
|
8
|
+
|
|
9
|
+
"{{goModule}}/internal/app/user/domain"
|
|
10
|
+
|
|
11
|
+
"github.com/google/uuid"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// ForgotPassword always returns success from the caller's perspective. Only a
|
|
15
|
+
// local password identity gets a token, so this endpoint cannot enumerate
|
|
16
|
+
// accounts or issue a reset for an external-only account.
|
|
17
|
+
func (s *Service) ForgotPassword(ctx context.Context, email string) error {
|
|
18
|
+
key := throttleKey("pwreset", email)
|
|
19
|
+
if s.throttled(ctx, key) {
|
|
20
|
+
return nil
|
|
21
|
+
}
|
|
22
|
+
if err := s.repo.RecordLoginFailure(ctx, key, loginFreeAttempts, loginMaxLock); err != nil {
|
|
23
|
+
slog.Error("record password reset attempt", "error", err)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
u, err := s.repo.FindByEmail(ctx, normalizeEmail(email))
|
|
27
|
+
if err != nil {
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
ident, err := s.repo.FindIdentity(ctx, u.ID, domain.ProviderLocal)
|
|
31
|
+
if err != nil || ident.PasswordHash == nil {
|
|
32
|
+
return nil
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
raw, err := randomToken()
|
|
36
|
+
if err != nil {
|
|
37
|
+
return fmt.Errorf("generate password reset token: %w", err)
|
|
38
|
+
}
|
|
39
|
+
if err := s.recoveryTokens.SetPasswordResetToken(ctx, hashToken(raw), u.ID, s.config.PasswordResetTTL); err != nil {
|
|
40
|
+
return fmt.Errorf("store password reset token: %w", err)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
link := s.config.PasswordResetURL + "?token=" + raw
|
|
44
|
+
if err := s.mailer.Send(ctx, u.Email, "Reset your password", "Reset your password: "+link); err != nil {
|
|
45
|
+
// The token is stored already. Log the delivery failure so it is visible
|
|
46
|
+
// operationally while preserving the anti-enumeration response.
|
|
47
|
+
slog.Error("send password reset email", "error", err)
|
|
48
|
+
}
|
|
49
|
+
return nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ResetPassword consumes a one-time token and updates the local identity in
|
|
53
|
+
// one transaction, then revokes all sessions. If the cross-store revoke fails,
|
|
54
|
+
// restoring the token lets the caller retry the complete operation.
|
|
55
|
+
func (s *Service) ResetPassword(ctx context.Context, rawToken, newPassword string) error {
|
|
56
|
+
tokenHash := hashToken(rawToken)
|
|
57
|
+
userID, err := s.recovery.ResetPassword(ctx, tokenHash, newPassword)
|
|
58
|
+
if err != nil {
|
|
59
|
+
if errors.Is(err, domain.ErrInvalidToken) {
|
|
60
|
+
return errInvalidToken()
|
|
61
|
+
}
|
|
62
|
+
return fmt.Errorf("reset password: %w", err)
|
|
63
|
+
}
|
|
64
|
+
if err := s.refreshTokens.RevokeAllRefreshTokens(ctx, userID); err != nil {
|
|
65
|
+
if restoreErr := s.recoveryTokens.SetPasswordResetToken(ctx, tokenHash, userID, s.config.PasswordResetTTL); restoreErr != nil {
|
|
66
|
+
return fmt.Errorf("revoke sessions after password reset: %w; restore reset token for retry: %v", err, restoreErr)
|
|
67
|
+
}
|
|
68
|
+
return fmt.Errorf("revoke sessions after password reset (reset token restored for retry): %w", err)
|
|
69
|
+
}
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// sendVerificationEmail issues a one-time token and sends its link. It is
|
|
74
|
+
// shared by registration and the authenticated resend flow.
|
|
75
|
+
func (s *Service) sendVerificationEmail(ctx context.Context, u *domain.User) {
|
|
76
|
+
raw, err := randomToken()
|
|
77
|
+
if err != nil {
|
|
78
|
+
slog.Error("generate email verification token", "error", err)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if err := s.recoveryTokens.SetEmailVerifyToken(ctx, hashToken(raw), u.ID, s.config.EmailVerifyTTL); err != nil {
|
|
82
|
+
slog.Error("store email verification token", "error", err)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
link := s.config.EmailVerifyURL + "?token=" + raw
|
|
86
|
+
if err := s.mailer.Send(ctx, u.Email, "Verify your email", "Verify your email: "+link); err != nil {
|
|
87
|
+
slog.Error("send email verification email", "error", err)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func (s *Service) ResendVerificationEmail(ctx context.Context, userID uuid.UUID) error {
|
|
92
|
+
u, err := s.repo.FindByID(ctx, userID)
|
|
93
|
+
if err != nil {
|
|
94
|
+
return wrapFindErr(err)
|
|
95
|
+
}
|
|
96
|
+
if u.EmailVerified {
|
|
97
|
+
return errAlreadyVerified()
|
|
98
|
+
}
|
|
99
|
+
s.sendVerificationEmail(ctx, u)
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func (s *Service) VerifyEmail(ctx context.Context, rawToken string) error {
|
|
104
|
+
_, err := s.recovery.VerifyEmail(ctx, hashToken(rawToken))
|
|
105
|
+
if err != nil {
|
|
106
|
+
if errors.Is(err, domain.ErrInvalidToken) {
|
|
107
|
+
return errInvalidToken()
|
|
108
|
+
}
|
|
109
|
+
return fmt.Errorf("verify email: %w", err)
|
|
110
|
+
}
|
|
111
|
+
return nil
|
|
112
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"{{goModule}}/internal/app/user/domain"
|
|
10
|
+
"{{goModule}}/internal/app/user/ports"
|
|
11
|
+
|
|
12
|
+
"github.com/google/uuid"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// repository is a package-local name used by focused application tests and
|
|
16
|
+
// generated method extensions. The actual contract is owned by ports.
|
|
17
|
+
type repository = ports.UserRepository
|
|
18
|
+
|
|
19
|
+
// Service coordinates user authentication use cases. Transport concerns stay
|
|
20
|
+
// in the inbound adapter and persistence concerns stay in outbound adapters.
|
|
21
|
+
type Service struct {
|
|
22
|
+
repo ports.UserRepository
|
|
23
|
+
passwords ports.PasswordHasher
|
|
24
|
+
refreshTokens ports.RefreshTokenStore
|
|
25
|
+
oauthTransactions ports.OAuthTransactionStore
|
|
26
|
+
recoveryTokens ports.RecoveryTokenStore
|
|
27
|
+
mfa ports.MFAStore
|
|
28
|
+
mailer ports.AuthMailer
|
|
29
|
+
providers ProviderRegistry
|
|
30
|
+
recovery *Recovery
|
|
31
|
+
config AuthConfig
|
|
32
|
+
roles ports.RoleChecker
|
|
33
|
+
now func() time.Time
|
|
34
|
+
// go-scaffold:user-service-fields
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// NewService is the application composition constructor. It accepts explicit
|
|
38
|
+
// ports and policy rather than a framework or catch-all infrastructure object.
|
|
39
|
+
func NewService(deps Dependencies, cfg AuthConfig) *Service {
|
|
40
|
+
if deps.Passwords == nil {
|
|
41
|
+
panic("user application: password hasher is required")
|
|
42
|
+
}
|
|
43
|
+
if cfg.JWTAccessTTL <= 0 {
|
|
44
|
+
cfg.JWTAccessTTL = 15 * time.Minute
|
|
45
|
+
}
|
|
46
|
+
if cfg.JWTRefreshTTL <= 0 {
|
|
47
|
+
cfg.JWTRefreshTTL = 12 * time.Hour
|
|
48
|
+
}
|
|
49
|
+
cfg.JWTRefreshMaxTTL = defaultDuration(cfg.JWTRefreshMaxTTL, cfg.JWTRefreshTTL)
|
|
50
|
+
cfg.OAuthStateTTL = defaultDuration(cfg.OAuthStateTTL, 10*time.Minute)
|
|
51
|
+
if cfg.MFA.RecoveryCodeCount == 0 {
|
|
52
|
+
cfg.MFA.RecoveryCodeCount = defaultMFACodes
|
|
53
|
+
}
|
|
54
|
+
clock := deps.Clock
|
|
55
|
+
if clock == nil {
|
|
56
|
+
clock = time.Now
|
|
57
|
+
}
|
|
58
|
+
return &Service{
|
|
59
|
+
repo: deps.Repository,
|
|
60
|
+
passwords: deps.Passwords,
|
|
61
|
+
refreshTokens: deps.RefreshTokens,
|
|
62
|
+
oauthTransactions: deps.OAuthTransactions,
|
|
63
|
+
recoveryTokens: deps.RecoveryTokens,
|
|
64
|
+
mfa: deps.MFA,
|
|
65
|
+
mailer: deps.Mailer,
|
|
66
|
+
providers: deps.Providers,
|
|
67
|
+
recovery: NewRecovery(deps.Repository, deps.RecoveryTokens, deps.Passwords),
|
|
68
|
+
config: cfg,
|
|
69
|
+
roles: deps.Roles,
|
|
70
|
+
now: clock,
|
|
71
|
+
// go-scaffold:user-service-init
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func defaultDuration(value, fallback time.Duration) time.Duration {
|
|
76
|
+
if value > 0 {
|
|
77
|
+
return value
|
|
78
|
+
}
|
|
79
|
+
return fallback
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (s *Service) clock() time.Time {
|
|
83
|
+
if s.now == nil {
|
|
84
|
+
return time.Now()
|
|
85
|
+
}
|
|
86
|
+
return s.now()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// normalizeEmail is applied at every boundary an address enters the service
|
|
90
|
+
// through. The database stores the canonical lower-case representation.
|
|
91
|
+
func normalizeEmail(email string) string {
|
|
92
|
+
return strings.ToLower(strings.TrimSpace(email))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// SetRole validates the role against the role catalog before persisting it.
|
|
96
|
+
func (s *Service) SetRole(ctx context.Context, userID uuid.UUID, roleCode string) (*domain.User, error) {
|
|
97
|
+
u, err := s.repo.FindByID(ctx, userID)
|
|
98
|
+
if err != nil {
|
|
99
|
+
return nil, wrapFindErr(err)
|
|
100
|
+
}
|
|
101
|
+
if s.roles == nil {
|
|
102
|
+
return nil, fmt.Errorf("role checker is not configured")
|
|
103
|
+
}
|
|
104
|
+
exists, err := s.roles.CodeExists(ctx, roleCode)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return nil, fmt.Errorf("check role code: %w", err)
|
|
107
|
+
}
|
|
108
|
+
if !exists {
|
|
109
|
+
return nil, errUnknownRole()
|
|
110
|
+
}
|
|
111
|
+
u.Role = roleCode
|
|
112
|
+
if err := s.repo.UpdateUser(ctx, u); err != nil {
|
|
113
|
+
return nil, fmt.Errorf("update user role: %w", err)
|
|
114
|
+
}
|
|
115
|
+
return u, nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ServicePort is the inbound application boundary. HTTP, jobs, and future
|
|
119
|
+
// transports depend on this capability set instead of the concrete service.
|
|
120
|
+
type ServicePort interface {
|
|
121
|
+
// go-scaffold:service-interface
|
|
122
|
+
Register(context.Context, RegisterInput) (*AuthResult, error)
|
|
123
|
+
Login(context.Context, LoginInput) (*AuthResult, error)
|
|
124
|
+
Refresh(context.Context, string) (*AuthResponse, error)
|
|
125
|
+
Logout(context.Context, string) error
|
|
126
|
+
LogoutAll(context.Context, uuid.UUID) error
|
|
127
|
+
ForgotPassword(context.Context, string) error
|
|
128
|
+
ResetPassword(context.Context, string, string) error
|
|
129
|
+
VerifyEmail(context.Context, string) error
|
|
130
|
+
ResendVerificationEmail(context.Context, uuid.UUID) error
|
|
131
|
+
BeginLogin(context.Context, string, LoginStartInput) (*Authorization, error)
|
|
132
|
+
ExchangeLogin(context.Context, string, LoginExchangeInput) (*AuthResult, error)
|
|
133
|
+
VerifyMFA(context.Context, string, string) (*AuthResponse, error)
|
|
134
|
+
MFAStatus(context.Context, uuid.UUID) (MFAStatus, error)
|
|
135
|
+
SetupMFA(context.Context, uuid.UUID) (*MFASetup, error)
|
|
136
|
+
ConfirmMFA(context.Context, uuid.UUID, string) ([]string, error)
|
|
137
|
+
DisableMFA(context.Context, uuid.UUID, string) error
|
|
138
|
+
Get(context.Context, uuid.UUID) (*domain.User, error)
|
|
139
|
+
List(context.Context, int, int) ([]domain.User, error)
|
|
140
|
+
SetRole(context.Context, uuid.UUID, string) (*domain.User, error)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var _ ServicePort = (*Service)(nil)
|
|
144
|
+
|
|
145
|
+
// go-scaffold:service-methods
|