@nakedev/go-scaffold 0.3.3 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +288 -50
  2. package/dist/commands/auth.js +53 -22
  3. package/dist/commands/config.js +50 -0
  4. package/dist/commands/create.js +32 -2
  5. package/dist/commands/generate.js +25 -2
  6. package/dist/commands/method.js +22 -7
  7. package/dist/commands/migration.js +2 -2
  8. package/dist/commands/observability.js +3 -3
  9. package/dist/commands/rbac.js +3 -3
  10. package/dist/commands/undo.js +5 -0
  11. package/dist/commands/worker.js +1 -1
  12. package/dist/index.js +186 -59
  13. package/dist/prompts/auth-wizard.js +40 -6
  14. package/dist/prompts/create-wizard.js +43 -2
  15. package/dist/prompts/generate-wizard.js +89 -9
  16. package/dist/templates/auth-manifest.js +31 -1
  17. package/dist/templates/create-manifest.js +4 -0
  18. package/dist/templates/module-manifest.js +37 -1
  19. package/dist/templates/rbac-manifest.js +1 -0
  20. package/dist/types.js +6 -0
  21. package/dist/utils/auth-patcher.js +115 -24
  22. package/dist/utils/config.js +147 -3
  23. package/dist/utils/main-patcher.js +29 -27
  24. package/dist/utils/marker-patch.js +7 -1
  25. package/dist/utils/method-patcher.js +261 -81
  26. package/dist/utils/module-profile.js +32 -0
  27. package/dist/utils/observability-patcher.js +2 -2
  28. package/dist/utils/platform-patcher.js +29 -7
  29. package/dist/utils/rbac-patcher.js +97 -75
  30. package/package.json +7 -2
  31. package/templates/add/auth/cmd/seed/main.go.hbs +13 -3
  32. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  33. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  34. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  35. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  36. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  37. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  38. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  39. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  40. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  41. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  42. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  43. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  44. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  45. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +113 -0
  46. package/templates/add/auth/internal/app/user/browser_policy.go.hbs +98 -0
  47. package/templates/add/auth/internal/app/user/composition.go.hbs +165 -0
  48. package/templates/add/auth/internal/app/user/contracts.go.hbs +88 -0
  49. package/templates/add/auth/internal/app/user/dto.go.hbs +57 -0
  50. package/templates/add/auth/internal/app/user/errors.go.hbs +25 -0
  51. package/templates/add/auth/internal/app/user/external_login.go.hbs +208 -0
  52. package/templates/add/auth/internal/app/user/handler.go.hbs +60 -203
  53. package/templates/add/auth/internal/app/user/handler_local.go.hbs +75 -0
  54. package/templates/add/auth/internal/app/user/handler_mfa.go.hbs +83 -0
  55. package/templates/add/auth/internal/app/user/handler_oauth.go.hbs +70 -0
  56. package/templates/add/auth/internal/app/user/handler_recovery.go.hbs +49 -0
  57. package/templates/add/auth/internal/app/user/handler_test.go.hbs +290 -0
  58. package/templates/add/auth/internal/app/user/handler_user.go.hbs +41 -0
  59. package/templates/add/auth/internal/app/user/jwt.go.hbs +6 -59
  60. package/templates/add/auth/internal/app/user/local_auth.go.hbs +98 -0
  61. package/templates/add/auth/internal/app/user/mfa_service.go.hbs +450 -0
  62. package/templates/add/auth/internal/app/user/mfa_service_test.go.hbs +199 -0
  63. package/templates/add/auth/internal/app/user/mfa_store.go.hbs +127 -0
  64. package/templates/add/auth/internal/app/user/mfa_store_test.go.hbs +174 -0
  65. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +8 -2
  66. package/templates/add/auth/internal/app/user/model/identity.go.hbs +4 -3
  67. package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +17 -0
  68. package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +20 -0
  69. package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +17 -0
  70. package/templates/add/auth/internal/app/user/model/user.go.hbs +3 -2
  71. package/templates/add/auth/internal/app/user/provider_test.go.hbs +286 -0
  72. package/templates/add/auth/internal/app/user/recovery_service.go.hbs +114 -0
  73. package/templates/add/auth/internal/app/user/repository.go.hbs +2 -0
  74. package/templates/add/auth/internal/app/user/service.go.hbs +82 -478
  75. package/templates/add/auth/internal/app/user/service_test.go.hbs +601 -45
  76. package/templates/add/auth/internal/app/user/session_cookie.go.hbs +33 -0
  77. package/templates/add/auth/internal/app/user/sessions.go.hbs +99 -0
  78. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +42 -14
  79. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +105 -40
  80. package/templates/add/auth/internal/app/user/tokenstore_pg_test.go.hbs +96 -0
  81. package/templates/add/auth/internal/app/user/tokenstore_recovery.go.hbs +58 -0
  82. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +144 -70
  83. package/templates/add/auth/internal/app/user/tokenstore_redis_test.go.hbs +185 -0
  84. package/templates/add/auth/internal/app/user/user_query.go.hbs +65 -0
  85. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  86. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  87. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +9 -4
  88. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  89. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  90. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  91. package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -2
  92. package/templates/add/rbac/internal/app/role/composition.go.hbs +35 -0
  93. package/templates/add/rbac/internal/app/role/service.go.hbs +12 -12
  94. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +340 -121
  95. package/templates/create/base/.env.example.hbs +0 -1
  96. package/templates/create/base/AGENTS.md.hbs +255 -67
  97. package/templates/create/base/Makefile.hbs +2 -1
  98. package/templates/create/base/README.md.hbs +45 -17
  99. package/templates/create/base/cmd/api/wiring.go.hbs +18 -25
  100. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  101. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  102. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  103. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  104. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  105. package/templates/create/features/docs/architecture.md.hbs +38 -16
  106. package/templates/create/features/docs/patterns.md.hbs +40 -21
  107. package/templates/create/features/docs/techstack.md.hbs +3 -3
  108. package/templates/generate/module/commands.go.hbs +95 -0
  109. package/templates/generate/module/composition.go.hbs +23 -0
  110. package/templates/generate/module/cqrs_test.go.hbs +7 -0
  111. package/templates/generate/module/handler.go.hbs +50 -5
  112. package/templates/generate/module/minimal/commands.go.hbs +34 -0
  113. package/templates/generate/module/minimal/handler.go.hbs +34 -0
  114. package/templates/generate/module/minimal/queries.go.hbs +45 -0
  115. package/templates/generate/module/minimal/service.go.hbs +27 -1
  116. package/templates/generate/module/queries.go.hbs +62 -0
  117. package/templates/generate/module/service.go.hbs +61 -5
  118. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  119. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
@@ -1,4 +1,5 @@
1
- -- Short-lived auth tokens, for projects scaffolded with `--store postgres`.
1
+ -- Durable recovery tokens for both refresh-store choices. Refresh rotation may
2
+ -- use Redis, but password reset and email verification share this DB contract.
2
3
  -- Only the SHA-256 hash is stored; see model/authtoken.go for what `kind`
3
4
  -- means and why the rotation tombstone is a separate row.
4
5
  CREATE TABLE user_svc.auth_tokens (
@@ -6,11 +7,15 @@ CREATE TABLE user_svc.auth_tokens (
6
7
  user_id UUID NOT NULL,
7
8
  kind VARCHAR(20) NOT NULL,
8
9
  expires_at TIMESTAMPTZ NOT NULL,
10
+ absolute_expires_at TIMESTAMPTZ,
11
+ provider VARCHAR(20) NOT NULL DEFAULT '',
12
+ code_challenge TEXT NOT NULL DEFAULT '',
13
+ nonce TEXT NOT NULL DEFAULT '',
9
14
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
10
15
  );
11
16
 
12
- -- Named to match model.AuthToken's own `index:` tags exactly GORM's
13
- -- AutoMigrate (dev) reconciles against these names, and an index the
14
- -- migration named differently is what stops the app booting.
17
+ -- Named to match model.AuthToken's own `index:` tags exactly, so the
18
+ -- development bootstrap and production migration describe the same indexes.
15
19
  CREATE INDEX idx_auth_tokens_user_kind ON user_svc.auth_tokens (user_id, kind);
16
20
  CREATE INDEX idx_auth_tokens_expires_at ON user_svc.auth_tokens (expires_at);
21
+ CREATE INDEX idx_auth_tokens_absolute_expires_at ON user_svc.auth_tokens (absolute_expires_at);
@@ -10,6 +10,6 @@ CREATE TABLE user_svc.identities (
10
10
 
11
11
  -- Named unique indexes matching model.Identity's own
12
12
  -- `uniqueIndex:idx_identities_*` tags — see create_users.up.sql for why an
13
- -- anonymous UNIQUE constraint breaks AutoMigrate.
13
+ -- anonymous UNIQUE constraint breaks schema parity.
14
14
  CREATE UNIQUE INDEX idx_identities_user_provider ON user_svc.identities (user_id, provider);
15
15
  CREATE UNIQUE INDEX idx_identities_provider_uid ON user_svc.identities (provider, provider_uid);
@@ -0,0 +1,3 @@
1
+ DROP TABLE IF EXISTS user_svc.mfa_recovery_codes;
2
+ DROP TABLE IF EXISTS user_svc.mfa_challenges;
3
+ DROP TABLE IF EXISTS user_svc.mfa_enrollments;
@@ -0,0 +1,29 @@
1
+ -- MFA state is durable in Postgres for both refresh-token store choices. TOTP
2
+ -- secrets are encrypted by the application; challenges and recovery codes are
3
+ -- stored only as hashes and consumed atomically.
4
+ CREATE TABLE user_svc.mfa_enrollments (
5
+ user_id UUID PRIMARY KEY REFERENCES user_svc.users(id) ON DELETE CASCADE,
6
+ encrypted_secret TEXT NOT NULL,
7
+ enabled BOOLEAN NOT NULL DEFAULT false,
8
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
9
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
10
+ );
11
+
12
+ CREATE TABLE user_svc.mfa_challenges (
13
+ challenge_hash TEXT PRIMARY KEY,
14
+ user_id UUID NOT NULL REFERENCES user_svc.users(id) ON DELETE CASCADE,
15
+ expires_at TIMESTAMPTZ NOT NULL,
16
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
17
+ );
18
+
19
+ CREATE INDEX idx_mfa_challenges_user ON user_svc.mfa_challenges (user_id);
20
+ CREATE INDEX idx_mfa_challenges_expires_at ON user_svc.mfa_challenges (expires_at);
21
+
22
+ CREATE TABLE user_svc.mfa_recovery_codes (
23
+ user_id UUID NOT NULL REFERENCES user_svc.users(id) ON DELETE CASCADE,
24
+ code_hash TEXT NOT NULL,
25
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
26
+ PRIMARY KEY (user_id, code_hash)
27
+ );
28
+
29
+ CREATE INDEX idx_mfa_recovery_codes_user ON user_svc.mfa_recovery_codes (user_id);
@@ -12,8 +12,8 @@ CREATE TABLE user_svc.users (
12
12
 
13
13
  -- A unique INDEX, not an inline UNIQUE constraint, and named to match the
14
14
  -- `uniqueIndex:idx_users_email` tag on model.User exactly. Postgres names an
15
- -- inline UNIQUE "users_email_key"; GORM's AutoMigrate (dev, AUTO_MIGRATE=true)
16
- -- then sees uniqueness it didn't create and tries to drop it under its own
15
+ -- inline UNIQUE "users_email_key"; the development bootstrap then sees
16
+ -- uniqueness it didn't create and tries to drop it under its own
17
17
  -- name, which fails the boot with
18
18
  -- constraint "uni_users_email" of relation "users" does not exist
19
19
  -- for anyone who ran `make migrate-up` first — which `add rbac` tells you to.
@@ -0,0 +1,35 @@
1
+ package role
2
+
3
+ import (
4
+ "time"
5
+
6
+ "{{goModule}}/internal/shared/middleware"
7
+
8
+ "gorm.io/gorm"
9
+ )
10
+
11
+ // Composition groups role's service, authorization adapter, and HTTP handler
12
+ // so cmd/api can register the feature without constructing its internals.
13
+ type Composition struct {
14
+ Service *Service
15
+ Authz *middleware.Authz
16
+ Handler *Handler
17
+ }
18
+
19
+ // NewServiceFromDB is the role feature's persistence composition boundary.
20
+ func NewServiceFromDB(db *gorm.DB) *Service {
21
+ return NewService(NewRepository(db))
22
+ }
23
+
24
+ // NewCompositionFromDB builds role and its authorization middleware locally.
25
+ // The API binary supplies the shared DB/config values and only registers the
26
+ // returned handlers.
27
+ func NewCompositionFromDB(db *gorm.DB, jwtSecret string, cacheTTL time.Duration) *Composition {
28
+ service := NewServiceFromDB(db)
29
+ authz := middleware.NewAuthz(service.PermissionsOf, cacheTTL)
30
+ return &Composition{
31
+ Service: service,
32
+ Authz: authz,
33
+ Handler: NewHandler(service, jwtSecret, authz),
34
+ }
35
+ }
@@ -52,7 +52,7 @@ func (s *Service) PermissionsOf(ctx context.Context, roleCode string) (map[strin
52
52
  }
53
53
 
54
54
  // CodeExists reports whether code names a real role — used by user.Service's
55
- // SetRole to validate a role assignment: AutoMigrate-only test schemas never
55
+ // SetRole to validate a role assignment: development-only test schemas never
56
56
  // create the users_role_fkey FK, so that alone can't be relied on to reject
57
57
  // an unknown code.
58
58
  func (s *Service) CodeExists(ctx context.Context, code string) (bool, error) {
@@ -75,7 +75,7 @@ type roleListItem struct {
75
75
  func (s *Service) List(ctx context.Context, limit, offset int) ([]roleListItem, error) {
76
76
  roles, err := s.repo.FindAll(ctx, limit, offset)
77
77
  if err != nil {
78
- return nil, apperror.NewInternal()
78
+ return nil, apperror.NewInternal(err)
79
79
  }
80
80
  // ponytail: one query per role, not a join. A system has a handful of
81
81
  // roles, so this is a handful of round trips on an admin screen nobody
@@ -88,7 +88,7 @@ func (s *Service) List(ctx context.Context, limit, offset int) ([]roleListItem,
88
88
  for i, r := range roles {
89
89
  perms, err := s.repo.PermissionCodes(ctx, r.Code)
90
90
  if err != nil {
91
- return nil, apperror.NewInternal()
91
+ return nil, apperror.NewInternal(err)
92
92
  }
93
93
  out[i] = roleListItem{Role: r, Permissions: perms}
94
94
  }
@@ -108,7 +108,7 @@ func (s *Service) Create(ctx context.Context, in CreateInput) (*roleListItem, er
108
108
  if dberr.IsDuplicate(err) {
109
109
  return nil, errConflict()
110
110
  }
111
- return nil, apperror.NewInternal()
111
+ return nil, apperror.NewInternal(err)
112
112
  }
113
113
  return &roleListItem{Role: *m, Permissions: []string{}}, nil
114
114
  }
@@ -127,7 +127,7 @@ func (s *Service) SetPermissions(ctx context.Context, code string, permissionCod
127
127
  if !slices.Contains(permissionCodes, PermRoleManage) {
128
128
  isLast, err := s.isLastRoleManager(ctx, code)
129
129
  if err != nil {
130
- return nil, apperror.NewInternal()
130
+ return nil, apperror.NewInternal(err)
131
131
  }
132
132
  if isLast {
133
133
  return nil, errLastRoleManager()
@@ -139,18 +139,18 @@ func (s *Service) SetPermissions(ctx context.Context, code string, permissionCod
139
139
  if dberr.IsForeignKey(err) {
140
140
  return nil, errUnknownPermission()
141
141
  }
142
- return nil, apperror.NewInternal()
142
+ return nil, apperror.NewInternal(err)
143
143
  }
144
144
  return &roleListItem{Role: *m, Permissions: permissionCodes}, nil
145
145
  }
146
146
 
147
147
  // validatePermissionCodes checks every code against the actual permission
148
- // catalog — the DB's FK alone isn't enough (AutoMigrate, used in dev/tests,
148
+ // catalog — the DB's FK alone isn't enough (the development bootstrap, used in dev/tests,
149
149
  // never creates it, only the raw migration SQL does).
150
150
  func (s *Service) validatePermissionCodes(ctx context.Context, codes []string) error {
151
151
  all, err := s.repo.AllPermissionCodes(ctx)
152
152
  if err != nil {
153
- return apperror.NewInternal()
153
+ return apperror.NewInternal(err)
154
154
  }
155
155
  valid := make(map[string]struct{}, len(all))
156
156
  for _, c := range all {
@@ -174,7 +174,7 @@ func (s *Service) Delete(ctx context.Context, code string) error {
174
174
  }
175
175
  isLast, err := s.isLastRoleManager(ctx, code)
176
176
  if err != nil {
177
- return apperror.NewInternal()
177
+ return apperror.NewInternal(err)
178
178
  }
179
179
  if isLast {
180
180
  return errLastRoleManager()
@@ -183,7 +183,7 @@ func (s *Service) Delete(ctx context.Context, code string) error {
183
183
  if dberr.IsForeignKey(err) {
184
184
  return errHasReferences()
185
185
  }
186
- return apperror.NewInternal()
186
+ return apperror.NewInternal(err)
187
187
  }
188
188
  return nil
189
189
  }
@@ -204,7 +204,7 @@ func (s *Service) isLastRoleManager(ctx context.Context, code string) (bool, err
204
204
  func (s *Service) ListPermissions(ctx context.Context, limit, offset int) ([]model.Permission, error) {
205
205
  items, err := s.repo.FindAllPermissions(ctx, limit, offset)
206
206
  if err != nil {
207
- return nil, apperror.NewInternal()
207
+ return nil, apperror.NewInternal(err)
208
208
  }
209
209
  return items, nil
210
210
  }
@@ -213,5 +213,5 @@ func wrapFindErr(err error) error {
213
213
  if errors.Is(err, gorm.ErrRecordNotFound) {
214
214
  return errNotFound()
215
215
  }
216
- return apperror.NewInternal()
216
+ return apperror.NewInternal(err)
217
217
  }