@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.
- package/README.md +288 -50
- package/dist/commands/auth.js +53 -22
- package/dist/commands/config.js +50 -0
- package/dist/commands/create.js +32 -2
- package/dist/commands/generate.js +25 -2
- package/dist/commands/method.js +22 -7
- package/dist/commands/migration.js +2 -2
- package/dist/commands/observability.js +3 -3
- package/dist/commands/rbac.js +3 -3
- package/dist/commands/undo.js +5 -0
- package/dist/commands/worker.js +1 -1
- package/dist/index.js +186 -59
- package/dist/prompts/auth-wizard.js +40 -6
- package/dist/prompts/create-wizard.js +43 -2
- package/dist/prompts/generate-wizard.js +89 -9
- package/dist/templates/auth-manifest.js +31 -1
- package/dist/templates/create-manifest.js +4 -0
- package/dist/templates/module-manifest.js +37 -1
- package/dist/templates/rbac-manifest.js +1 -0
- package/dist/types.js +6 -0
- package/dist/utils/auth-patcher.js +115 -24
- package/dist/utils/config.js +147 -3
- package/dist/utils/main-patcher.js +29 -27
- package/dist/utils/marker-patch.js +7 -1
- package/dist/utils/method-patcher.js +261 -81
- package/dist/utils/module-profile.js +32 -0
- package/dist/utils/observability-patcher.js +2 -2
- package/dist/utils/platform-patcher.js +29 -7
- package/dist/utils/rbac-patcher.js +97 -75
- package/package.json +7 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +13 -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/application/oauth.go.hbs +132 -0
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +113 -0
- package/templates/add/auth/internal/app/user/browser_policy.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/composition.go.hbs +165 -0
- package/templates/add/auth/internal/app/user/contracts.go.hbs +88 -0
- package/templates/add/auth/internal/app/user/dto.go.hbs +57 -0
- package/templates/add/auth/internal/app/user/errors.go.hbs +25 -0
- package/templates/add/auth/internal/app/user/external_login.go.hbs +208 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +60 -203
- package/templates/add/auth/internal/app/user/handler_local.go.hbs +75 -0
- package/templates/add/auth/internal/app/user/handler_mfa.go.hbs +83 -0
- package/templates/add/auth/internal/app/user/handler_oauth.go.hbs +70 -0
- package/templates/add/auth/internal/app/user/handler_recovery.go.hbs +49 -0
- package/templates/add/auth/internal/app/user/handler_test.go.hbs +290 -0
- package/templates/add/auth/internal/app/user/handler_user.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/jwt.go.hbs +6 -59
- package/templates/add/auth/internal/app/user/local_auth.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/mfa_service.go.hbs +450 -0
- package/templates/add/auth/internal/app/user/mfa_service_test.go.hbs +199 -0
- package/templates/add/auth/internal/app/user/mfa_store.go.hbs +127 -0
- package/templates/add/auth/internal/app/user/mfa_store_test.go.hbs +174 -0
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +8 -2
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +4 -3
- package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +17 -0
- package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +20 -0
- package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +17 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +3 -2
- package/templates/add/auth/internal/app/user/provider_test.go.hbs +286 -0
- package/templates/add/auth/internal/app/user/recovery_service.go.hbs +114 -0
- package/templates/add/auth/internal/app/user/repository.go.hbs +2 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +82 -478
- package/templates/add/auth/internal/app/user/service_test.go.hbs +601 -45
- package/templates/add/auth/internal/app/user/session_cookie.go.hbs +33 -0
- package/templates/add/auth/internal/app/user/sessions.go.hbs +99 -0
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +42 -14
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +105 -40
- package/templates/add/auth/internal/app/user/tokenstore_pg_test.go.hbs +96 -0
- package/templates/add/auth/internal/app/user/tokenstore_recovery.go.hbs +58 -0
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +144 -70
- package/templates/add/auth/internal/app/user/tokenstore_redis_test.go.hbs +185 -0
- package/templates/add/auth/internal/app/user/user_query.go.hbs +65 -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 +9 -4
- package/templates/add/auth/migrations/create_identities.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 +2 -2
- package/templates/add/rbac/internal/app/role/composition.go.hbs +35 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +12 -12
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +340 -121
- package/templates/create/base/.env.example.hbs +0 -1
- package/templates/create/base/AGENTS.md.hbs +255 -67
- package/templates/create/base/Makefile.hbs +2 -1
- package/templates/create/base/README.md.hbs +45 -17
- package/templates/create/base/cmd/api/wiring.go.hbs +18 -25
- 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 +38 -16
- package/templates/create/features/docs/patterns.md.hbs +40 -21
- package/templates/create/features/docs/techstack.md.hbs +3 -3
- package/templates/generate/module/commands.go.hbs +95 -0
- package/templates/generate/module/composition.go.hbs +23 -0
- package/templates/generate/module/cqrs_test.go.hbs +7 -0
- package/templates/generate/module/handler.go.hbs +50 -5
- package/templates/generate/module/minimal/commands.go.hbs +34 -0
- package/templates/generate/module/minimal/handler.go.hbs +34 -0
- package/templates/generate/module/minimal/queries.go.hbs +45 -0
- package/templates/generate/module/minimal/service.go.hbs +27 -1
- package/templates/generate/module/queries.go.hbs +62 -0
- package/templates/generate/module/service.go.hbs +61 -5
- package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
- package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
--
|
|
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
|
|
13
|
-
--
|
|
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
|
|
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,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";
|
|
16
|
-
--
|
|
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:
|
|
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 (
|
|
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
|
}
|