@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
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.patchComposeForRedis = patchComposeForRedis;
|
|
7
7
|
exports.patchCiForRedis = patchCiForRedis;
|
|
8
|
+
exports.patchCiForRiver = patchCiForRiver;
|
|
8
9
|
exports.patchConfigForRedis = patchConfigForRedis;
|
|
9
10
|
exports.patchConfigForWorker = patchConfigForWorker;
|
|
10
11
|
exports.patchConfigForSMTP = patchConfigForSMTP;
|
|
@@ -17,7 +18,6 @@ const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
|
|
|
17
18
|
const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
|
|
18
19
|
const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
|
|
19
20
|
const READYZ_MARKER = "// go-scaffold:readyz-checks";
|
|
20
|
-
const SHUTDOWN_MARKER = "// go-scaffold:shutdown";
|
|
21
21
|
// patchComposeForRedis adds a redis service to docker-compose.yml. Whatever
|
|
22
22
|
// pulls Redis in — `add worker --queue redis`, or `add auth`'s refresh-token
|
|
23
23
|
// store on top of a Postgres queue — makes cmd/api call cache.Open and adds a
|
|
@@ -55,8 +55,7 @@ function patchCiForRedis(ciPath) {
|
|
|
55
55
|
if (!fs_extra_1.default.existsSync(ciPath))
|
|
56
56
|
return;
|
|
57
57
|
const content = fs_extra_1.default.readFileSync(ciPath, "utf8");
|
|
58
|
-
|
|
59
|
-
return;
|
|
58
|
+
const hasRedisService = /^\s{6}redis:/m.test(content);
|
|
60
59
|
// `steps:` sits one level under the job, so it's the first line that ends
|
|
61
60
|
// the `services:` block — insert the service just above it.
|
|
62
61
|
//
|
|
@@ -65,7 +64,7 @@ function patchCiForRedis(ciPath) {
|
|
|
65
64
|
// means CI runs without Redis and fails on a connection error that says
|
|
66
65
|
// nothing about this — so say it here instead.
|
|
67
66
|
const stepsLine = content.split("\n").find((l) => l.trimEnd() === " steps:");
|
|
68
|
-
if (!stepsLine) {
|
|
67
|
+
if (!hasRedisService && !stepsLine) {
|
|
69
68
|
console.error(picocolors_1.default.yellow(`skipped adding the Redis service to ${ciPath} — no \`steps:\` line at the expected indentation to anchor it to.\n` +
|
|
70
69
|
`Add a redis service to the workflow's \`services:\` block by hand, or CI will run without one.`));
|
|
71
70
|
return;
|
|
@@ -82,7 +81,51 @@ function patchCiForRedis(ciPath) {
|
|
|
82
81
|
" --health-retries 5",
|
|
83
82
|
"",
|
|
84
83
|
].join("\n");
|
|
85
|
-
|
|
84
|
+
let patched = hasRedisService ? content : content.replace(stepsLine, () => `${service}\n${stepsLine}`);
|
|
85
|
+
// The service alone is not enough: token-store tests deliberately skip when
|
|
86
|
+
// TEST_REDIS_URL is absent, so a generated workflow could report green while
|
|
87
|
+
// never exercising Redis. Add the required test variables alongside the
|
|
88
|
+
// existing PostgreSQL test variables, anchored on the actual go test line so
|
|
89
|
+
// we do not accidentally put them in the Postgres container's env block.
|
|
90
|
+
const testRunLine = " run: go test ./...";
|
|
91
|
+
if (!patched.includes("REQUIRE_TEST_REDIS")) {
|
|
92
|
+
if (!patched.includes(testRunLine)) {
|
|
93
|
+
console.error(picocolors_1.default.yellow(`skipped requiring Redis integration tests in ${ciPath} — no \`run: go test ./...\` line to anchor the test environment to.\n` +
|
|
94
|
+
`Add TEST_REDIS_URL and REQUIRE_TEST_REDIS=true to the workflow's test step by hand.`));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
patched = patched.replace(testRunLine, ' TEST_REDIS_URL: redis://127.0.0.1:6379/0\n' +
|
|
98
|
+
' REQUIRE_TEST_REDIS: "true"\n' +
|
|
99
|
+
testRunLine);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
fs_extra_1.default.writeFileSync(ciPath, patched);
|
|
103
|
+
}
|
|
104
|
+
// patchCiForRiver makes the generated worker round-trip test runnable in CI.
|
|
105
|
+
// River's schema is versioned by River itself, separately from this project's
|
|
106
|
+
// migrations/, so applying only the application migrations leaves the test
|
|
107
|
+
// unable to exercise the worker.
|
|
108
|
+
function patchCiForRiver(ciPath) {
|
|
109
|
+
if (!fs_extra_1.default.existsSync(ciPath))
|
|
110
|
+
return;
|
|
111
|
+
const content = fs_extra_1.default.readFileSync(ciPath, "utf8");
|
|
112
|
+
if (content.includes("river-migrate-test") || content.includes("Apply River migrations"))
|
|
113
|
+
return;
|
|
114
|
+
const testStep = content.split("\n").find((line) => line.trimStart().startsWith("- name: Test (required PostgreSQL integration tests cannot skip)"));
|
|
115
|
+
const dsn = content.match(/^\s+DB_DSN:\s+(.+)$/m)?.[1]?.trim();
|
|
116
|
+
if (!testStep || !dsn) {
|
|
117
|
+
console.error(picocolors_1.default.yellow(`skipped adding River migrations to ${ciPath} — the generated migration DSN or PostgreSQL test step is missing.\n` +
|
|
118
|
+
`Add a River migration step before the required PostgreSQL test step by hand.`));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const step = [
|
|
122
|
+
" - name: Apply River migrations to the test database",
|
|
123
|
+
" env:",
|
|
124
|
+
` TEST_DB_DSN: ${dsn}`,
|
|
125
|
+
' run: go run github.com/riverqueue/river/cmd/river@v0.43.0 migrate-up --line main --database-url "$TEST_DB_DSN"',
|
|
126
|
+
"",
|
|
127
|
+
].join("\n");
|
|
128
|
+
fs_extra_1.default.writeFileSync(ciPath, content.replace(testStep, `${step}${testStep}`));
|
|
86
129
|
}
|
|
87
130
|
// patchConfigForRedis adds RedisURL to Config and its env() load to Load().
|
|
88
131
|
// Split out from the worker patch because Redis is no longer the worker's
|
|
@@ -141,6 +184,14 @@ function patchMainGoForWorker(mainGoPath, goModule) {
|
|
|
141
184
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, cacheImport, cacheImport);
|
|
142
185
|
const initBlock = ["rdb, err := cache.Open(cfg)", "if err != nil {", '\treturn fmt.Errorf("open redis: %w", err)', "}"].join("\n");
|
|
143
186
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, initBlock, "rdb, err := cache.Open(cfg)");
|
|
187
|
+
const cleanupBlock = [
|
|
188
|
+
"defer func() {",
|
|
189
|
+
'\tif err := rdb.Close(); err != nil {',
|
|
190
|
+
'\t\tlogger.Error("close redis", "error", err)',
|
|
191
|
+
"\t}",
|
|
192
|
+
"}()",
|
|
193
|
+
].join("\n");
|
|
194
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, cleanupBlock, "defer func() {\n\tif err := rdb.Close()");
|
|
144
195
|
const readyzBlock = [
|
|
145
196
|
"if err := rdb.Ping(c.Request.Context()).Err(); err != nil {",
|
|
146
197
|
'\tc.JSON(http.StatusServiceUnavailable, gin.H{"status": "unavailable"})',
|
|
@@ -148,7 +199,5 @@ function patchMainGoForWorker(mainGoPath, goModule) {
|
|
|
148
199
|
"}",
|
|
149
200
|
].join("\n");
|
|
150
201
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, READYZ_MARKER, readyzBlock, "if err := rdb.Ping(");
|
|
151
|
-
const shutdownBlock = ["if err := rdb.Close(); err != nil {", '\tlogger.Error("close redis", "error", err)', "}"].join("\n");
|
|
152
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SHUTDOWN_MARKER, shutdownBlock, "if err := rdb.Close()");
|
|
153
202
|
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
154
203
|
}
|
|
@@ -13,7 +13,6 @@ exports.patchUserServiceTestForRbac = patchUserServiceTestForRbac;
|
|
|
13
13
|
exports.patchUserDTOForRbac = patchUserDTOForRbac;
|
|
14
14
|
exports.patchUserHandlerForRbac = patchUserHandlerForRbac;
|
|
15
15
|
exports.patchUserErrorsForRbac = patchUserErrorsForRbac;
|
|
16
|
-
exports.userSvcLineFor = userSvcLineFor;
|
|
17
16
|
exports.assertRbacPatchable = assertRbacPatchable;
|
|
18
17
|
exports.patchMainGoForRbac = patchMainGoForRbac;
|
|
19
18
|
exports.patchCmdSeedForRbac = patchCmdSeedForRbac;
|
|
@@ -23,13 +22,12 @@ const marker_patch_1 = require("./marker-patch");
|
|
|
23
22
|
const IMPORT_MARKER = "// go-scaffold:imports";
|
|
24
23
|
const SCHEMA_MARKER = "// go-scaffold:schemas";
|
|
25
24
|
const MODEL_MARKER = "// go-scaffold:models";
|
|
26
|
-
const ROUTE_MARKER = "// go-scaffold:routes";
|
|
27
25
|
const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
|
|
28
26
|
const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
|
|
29
27
|
// patchAuthDocsForRbac adds `role` to the hand-written MeResponse schema in
|
|
30
28
|
// docs/auth/schemas.yaml — same marker convention as the Go dto.go patch
|
|
31
29
|
// (patchUserDTOForRbac), since GET/PATCH /users(/me) all serialize the same
|
|
32
|
-
//
|
|
30
|
+
// user response whether or not RBAC is installed.
|
|
33
31
|
function patchAuthDocsForRbac(authSchemasYamlPath) {
|
|
34
32
|
if (!fs_extra_1.default.existsSync(authSchemasYamlPath))
|
|
35
33
|
return; // openapi docs feature disabled
|
|
@@ -46,15 +44,16 @@ function patchConfigForRbac(configGoPath) {
|
|
|
46
44
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, 'AuthzCacheTTL: time.Duration(envInt("AUTHZ_CACHE_TTL_MIN", 1)) * time.Minute,', "AuthzCacheTTL: time.Duration");
|
|
47
45
|
fs_extra_1.default.writeFileSync(configGoPath, content);
|
|
48
46
|
}
|
|
49
|
-
// patchUserModelForRbac
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
47
|
+
// patchUserModelForRbac verifies the auth template exposes the optional role
|
|
48
|
+
// field in its outbound persistence model. Auth owns the stable user shape;
|
|
49
|
+
// RBAC adds the role catalog and authorization policy on top of it. Keeping
|
|
50
|
+
// this check idempotent lets the command work across scaffold versions
|
|
51
|
+
// without recreating a second model package.
|
|
53
52
|
function patchUserModelForRbac(userModelPath) {
|
|
54
53
|
let content = fs_extra_1.default.readFileSync(userModelPath, "utf8");
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
if (/\bRole\s+string\b/.test(content))
|
|
55
|
+
return;
|
|
56
|
+
throw new Error(`${userModelPath} does not expose User.Role — the auth module must be regenerated or migrated before adding RBAC`);
|
|
58
57
|
}
|
|
59
58
|
// patchMiddlewareAuthForRbac adds RoleKey + the Role claim to the shared
|
|
60
59
|
// middleware's own accessClaims copy (see auth.go's own comment on why it's
|
|
@@ -78,189 +77,88 @@ function patchUserJWTForRbac(jwtGoPath) {
|
|
|
78
77
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:jwt-claims-values", "Role: role,", "Role: role,");
|
|
79
78
|
fs_extra_1.default.writeFileSync(jwtGoPath, content);
|
|
80
79
|
}
|
|
81
|
-
// patchUserServiceForRbac
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-init", "roles: roles,", "roles: roles,");
|
|
98
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:issue-access-token-args", "u.Role,", "u.Role,");
|
|
99
|
-
const setRoleMethod = [
|
|
100
|
-
"// SetRole assigns userID a new role — validated against the role catalog",
|
|
101
|
-
"// (not just the DB's FK) so a typo'd code fails with a clear 422 here",
|
|
102
|
-
"// instead of surfacing as an opaque FK-violation, and so AutoMigrate-only",
|
|
103
|
-
"// test schemas (which never create the FK) still reject it correctly.",
|
|
104
|
-
"func (s *Service) SetRole(ctx context.Context, userID uuid.UUID, roleCode string) (*model.User, error) {",
|
|
105
|
-
"\tu, err := s.repo.FindByID(ctx, userID)",
|
|
106
|
-
"\tif err != nil {",
|
|
107
|
-
"\t\tif errors.Is(err, gorm.ErrRecordNotFound) {",
|
|
108
|
-
"\t\t\treturn nil, errNotFound()",
|
|
109
|
-
"\t\t}",
|
|
110
|
-
"\t\treturn nil, apperror.NewInternal()",
|
|
111
|
-
"\t}",
|
|
112
|
-
"\texists, err := s.roles.CodeExists(ctx, roleCode)",
|
|
113
|
-
"\tif err != nil {",
|
|
114
|
-
"\t\treturn nil, apperror.NewInternal()",
|
|
115
|
-
"\t}",
|
|
116
|
-
"\tif !exists {",
|
|
117
|
-
"\t\treturn nil, errUnknownRole()",
|
|
118
|
-
"\t}",
|
|
119
|
-
"\tu.Role = roleCode",
|
|
120
|
-
"\tif err := s.repo.UpdateUser(ctx, u); err != nil {",
|
|
121
|
-
"\t\treturn nil, apperror.NewInternal()",
|
|
122
|
-
"\t}",
|
|
123
|
-
"\treturn u, nil",
|
|
124
|
-
"}",
|
|
125
|
-
].join("\n");
|
|
126
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-methods", setRoleMethod, "func (s *Service) SetRole(");
|
|
127
|
-
fs_extra_1.default.writeFileSync(serviceGoPath, content);
|
|
80
|
+
// patchUserServiceForRbac verifies the auth application already exposes the
|
|
81
|
+
// optional role capability. The canonical auth template owns this shared user
|
|
82
|
+
// behavior; `add rbac` only supplies the role catalog and authorizer.
|
|
83
|
+
function patchUserServiceForRbac(serviceGoPath, sessionsGoPath) {
|
|
84
|
+
const content = fs_extra_1.default.readFileSync(serviceGoPath, "utf8");
|
|
85
|
+
for (const required of ["roles ports.RoleChecker", "func (s *Service) SetRole(", "// go-scaffold:service-interface"]) {
|
|
86
|
+
if (!content.includes(required)) {
|
|
87
|
+
throw new Error(`${serviceGoPath} is missing the canonical auth role capability (${required}); regenerate auth before adding RBAC`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (sessionsGoPath && fs_extra_1.default.existsSync(sessionsGoPath)) {
|
|
91
|
+
const sessions = fs_extra_1.default.readFileSync(sessionsGoPath, "utf8");
|
|
92
|
+
if (!sessions.includes("u.Role,")) {
|
|
93
|
+
throw new Error(`${sessionsGoPath} does not include the user's role in access-token issuance; regenerate auth before adding RBAC`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
128
96
|
}
|
|
129
|
-
// patchUserServiceTestForRbac
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// middleware.NewAuthz's call sites, just for this signature instead. The fake
|
|
133
|
-
// itself is added here too, not pre-declared unconditionally in the
|
|
134
|
-
// template: golangci-lint's unused check flags an unreferenced type+method
|
|
135
|
-
// pair in the auth-only (pre-rbac) state, since nothing there yet implements
|
|
136
|
-
// or needs a roleChecker.
|
|
97
|
+
// patchUserServiceTestForRbac verifies the canonical auth test seam already
|
|
98
|
+
// supplies the optional role capability. It is part of the auth contract so
|
|
99
|
+
// adding RBAC does not rewrite every test call site.
|
|
137
100
|
function patchUserServiceTestForRbac(serviceTestGoPath) {
|
|
138
|
-
|
|
139
|
-
const fakeRoles
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
].join("\n");
|
|
145
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-test-types", fakeRoles, "type fakeRoles struct{}");
|
|
146
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-test-args", "fakeRoles{},", "fakeRoles{},");
|
|
147
|
-
fs_extra_1.default.writeFileSync(serviceTestGoPath, content);
|
|
101
|
+
const content = fs_extra_1.default.readFileSync(serviceTestGoPath, "utf8");
|
|
102
|
+
for (const required of ["type fakeRoles struct{}", "Roles: fakeRoles{},", "// go-scaffold:user-service-test-deps"]) {
|
|
103
|
+
if (!content.includes(required)) {
|
|
104
|
+
throw new Error(`${serviceTestGoPath} is missing the canonical auth role test seam (${required}); regenerate auth before adding RBAC`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
148
107
|
}
|
|
149
|
-
// patchUserDTOForRbac
|
|
150
|
-
//
|
|
151
|
-
// model.User has no Role field at all without it.
|
|
108
|
+
// patchUserDTOForRbac verifies the role DTOs owned by the canonical auth
|
|
109
|
+
// inbound adapter. The routes remain inactive until an Authz is composed.
|
|
152
110
|
function patchUserDTOForRbac(dtoGoPath) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
111
|
+
const content = fs_extra_1.default.readFileSync(dtoGoPath, "utf8");
|
|
112
|
+
if (!/\bRole\s+string\s+`json:"role"`/.test(content)) {
|
|
113
|
+
throw new Error(`${dtoGoPath} is missing the canonical auth role DTO; regenerate auth before adding RBAC`);
|
|
114
|
+
}
|
|
115
|
+
if (!/Role:\s+user\.Role,/.test(content)) {
|
|
116
|
+
throw new Error(`${dtoGoPath} is missing the canonical auth role DTO mapping; regenerate auth before adding RBAC`);
|
|
117
|
+
}
|
|
118
|
+
if (!content.includes("type setRoleInput struct")) {
|
|
119
|
+
throw new Error(`${dtoGoPath} is missing the canonical auth role DTO (type setRoleInput struct); regenerate auth before adding RBAC`);
|
|
120
|
+
}
|
|
159
121
|
}
|
|
160
|
-
// patchUserHandlerForRbac
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"func (h *Handler) adminListUsers(c *gin.Context) {",
|
|
179
|
-
"\tp := pagination.Parse(c)",
|
|
180
|
-
"\titems, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)",
|
|
181
|
-
"\tif err != nil {",
|
|
182
|
-
"\t\tc.Error(err)",
|
|
183
|
-
"\t\treturn",
|
|
184
|
-
"\t}",
|
|
185
|
-
"\tout := make([]meResponse, len(items))",
|
|
186
|
-
"\tfor i := range items {",
|
|
187
|
-
"\t\tout[i] = toMeResponse(&items[i])",
|
|
188
|
-
"\t}",
|
|
189
|
-
"\tc.JSON(http.StatusOK, p.Response(out))",
|
|
190
|
-
"}",
|
|
191
|
-
].join("\n");
|
|
192
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", adminListHandler, "func (h *Handler) adminListUsers(");
|
|
193
|
-
const adminGetHandler = [
|
|
194
|
-
"func (h *Handler) adminGetUser(c *gin.Context) {",
|
|
195
|
-
"\tid, ok := httpx.ParseID(c)",
|
|
196
|
-
"\tif !ok {",
|
|
197
|
-
"\t\treturn",
|
|
198
|
-
"\t}",
|
|
199
|
-
"\tu, err := h.svc.Get(c.Request.Context(), id)",
|
|
200
|
-
"\tif err != nil {",
|
|
201
|
-
"\t\tc.Error(err)",
|
|
202
|
-
"\t\treturn",
|
|
203
|
-
"\t}",
|
|
204
|
-
"\tc.JSON(http.StatusOK, toMeResponse(u))",
|
|
205
|
-
"}",
|
|
206
|
-
].join("\n");
|
|
207
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", adminGetHandler, "func (h *Handler) adminGetUser(");
|
|
208
|
-
const setRoleHandler = [
|
|
209
|
-
"func (h *Handler) setRole(c *gin.Context) {",
|
|
210
|
-
"\tid, ok := httpx.ParseID(c)",
|
|
211
|
-
"\tif !ok {",
|
|
212
|
-
"\t\treturn",
|
|
213
|
-
"\t}",
|
|
214
|
-
"\tvar in setRoleInput",
|
|
215
|
-
"\tif err := c.ShouldBindJSON(&in); err != nil {",
|
|
216
|
-
"\t\tc.Error(httpx.BindErr(err))",
|
|
217
|
-
"\t\treturn",
|
|
218
|
-
"\t}",
|
|
219
|
-
"\tu, err := h.svc.SetRole(c.Request.Context(), id, in.Role)",
|
|
220
|
-
"\tif err != nil {",
|
|
221
|
-
"\t\tc.Error(err)",
|
|
222
|
-
"\t\treturn",
|
|
223
|
-
"\t}",
|
|
224
|
-
"\tc.JSON(http.StatusOK, toMeResponse(u))",
|
|
225
|
-
"}",
|
|
226
|
-
].join("\n");
|
|
227
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", setRoleHandler, "func (h *Handler) setRole(");
|
|
228
|
-
fs_extra_1.default.writeFileSync(handlerGoPath, content);
|
|
122
|
+
// patchUserHandlerForRbac verifies that auth owns the role-aware admin route
|
|
123
|
+
// surface. The routes are guarded by the optional authorizer, so adding RBAC
|
|
124
|
+
// composes the guard; it does not copy or rewrite handler code.
|
|
125
|
+
function patchUserHandlerForRbac(handlerGoPath) {
|
|
126
|
+
const content = fs_extra_1.default.readFileSync(handlerGoPath, "utf8");
|
|
127
|
+
for (const required of [
|
|
128
|
+
"authz authorizer",
|
|
129
|
+
"func (h *Handler) adminListUsers(",
|
|
130
|
+
"func (h *Handler) adminGetUser(",
|
|
131
|
+
"func (h *Handler) setRole(",
|
|
132
|
+
'usersGroup.GET("", h.authz.Require(PermUserRead), h.adminListUsers)',
|
|
133
|
+
'usersGroup.GET("/:id", h.authz.Require(PermUserRead), h.adminGetUser)',
|
|
134
|
+
'usersGroup.PATCH("/:id/set-role", h.authz.Require(PermUserManageRole), h.setRole)',
|
|
135
|
+
]) {
|
|
136
|
+
if (!content.includes(required)) {
|
|
137
|
+
throw new Error(`${handlerGoPath} is missing the canonical auth role route (${required}); regenerate auth before adding RBAC`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
229
140
|
}
|
|
230
141
|
function patchUserErrorsForRbac(errorsGoPath) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
// patchMainGoForRbac wires the role domain into cmd/api: its import, its
|
|
237
|
-
// three models in the AutoMigrate call, and — the one place a plain insert
|
|
238
|
-
// isn't enough — REPLACES the `add auth` PR's user.NewHandler(...) call with
|
|
239
|
-
// a version that also builds roleSvc/authz and passes them through, plus
|
|
240
|
-
// registers role's own routes right after it.
|
|
241
|
-
// userSvcLineFor rebuilds the exact line `add auth` wrote, from the same
|
|
242
|
-
// helper it used. Exported so the command can check for it *before* it starts
|
|
243
|
-
// patching: every other file rbac touches is edited first, and until this
|
|
244
|
-
// existed a mismatch here threw after user.NewService had already grown a
|
|
245
|
-
// roleChecker parameter — leaving a project that no longer compiled and an
|
|
246
|
-
// error telling you to restore a line that wouldn't have fixed it.
|
|
247
|
-
function userSvcLineFor(goModule, store, worker) {
|
|
248
|
-
const { tokenStore, mailer } = (0, auth_patcher_1.authWiringLines)({ goModule, queueBackend: "river", store, worker });
|
|
249
|
-
return `userSvc := user.NewService(user.NewRepository(db), ${tokenStore}, ${mailer}, cfg)`;
|
|
142
|
+
const content = fs_extra_1.default.readFileSync(errorsGoPath, "utf8");
|
|
143
|
+
if (!content.includes("func errUnknownRole(")) {
|
|
144
|
+
throw new Error(`${errorsGoPath} is missing the canonical unknown-role error; regenerate auth before adding RBAC`);
|
|
145
|
+
}
|
|
250
146
|
}
|
|
251
147
|
function assertRbacPatchable(mainGoPath, goModule, store, worker) {
|
|
252
|
-
const
|
|
253
|
-
|
|
148
|
+
const wiring = { goModule, queueBackend: "river", store, worker };
|
|
149
|
+
const content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
150
|
+
const expected = (0, auth_patcher_1.authHandlerLineFor)(wiring);
|
|
151
|
+
if (content.includes(expected))
|
|
254
152
|
return;
|
|
255
|
-
throw new Error("cmd/api/wiring.go's
|
|
153
|
+
throw new Error("cmd/api/wiring.go's auth route doesn't match what `add auth` wrote, so `add rbac` can't extend it.\n" +
|
|
256
154
|
`Expected to find:\n ${expected}\n\n` +
|
|
257
|
-
"It was probably hand-edited. Restore that
|
|
155
|
+
"It was probably hand-edited. Restore that route and re-run — nothing has been changed yet.");
|
|
258
156
|
}
|
|
259
157
|
function patchMainGoForRbac(mainGoPath, goModule, store, worker) {
|
|
260
158
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
261
159
|
const importLine = `"${goModule}/internal/app/role"`;
|
|
262
160
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
|
|
263
|
-
const modelImportLine = `
|
|
161
|
+
const modelImportLine = `rolepostgres "${goModule}/internal/app/role/adapters/outbound/postgres"`;
|
|
264
162
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
|
|
265
163
|
const schemaBlock = [
|
|
266
164
|
'if err := db.Exec("CREATE SCHEMA IF NOT EXISTS role_svc").Error; err != nil {',
|
|
@@ -268,41 +166,21 @@ function patchMainGoForRbac(mainGoPath, goModule, store, worker) {
|
|
|
268
166
|
"}",
|
|
269
167
|
].join("\n");
|
|
270
168
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaBlock, "CREATE SCHEMA IF NOT EXISTS role_svc");
|
|
271
|
-
const migrateLines = ["&
|
|
169
|
+
const migrateLines = ["&rolepostgres.Role{},", "&rolepostgres.Permission{},", "&rolepostgres.RolePermission{},"];
|
|
272
170
|
for (const line of migrateLines) {
|
|
273
171
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, line, line);
|
|
274
172
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// are what the unconditional patches below refer to. Skipping quietly still
|
|
284
|
-
// emits `roleSvc`/`authz` references with nothing declaring them, so the
|
|
285
|
-
// command reports success over a main.go that doesn't compile.
|
|
286
|
-
if (!content.includes(userSvcLine)) {
|
|
287
|
-
throw new Error(`cmd/api/wiring.go's userSvc line doesn't match what \`add auth\` wrote, so \`add rbac\` can't extend it.\n` +
|
|
288
|
-
`Expected to find:\n ${userSvcLine}\n\n` +
|
|
289
|
-
`It was probably hand-edited. Restore that line (add rbac will re-extend it), or apply the rbac wiring by hand:\n` +
|
|
290
|
-
` roleSvc := role.NewService(role.NewRepository(db))\n` +
|
|
291
|
-
` authz := middleware.NewAuthz(roleSvc.PermissionsOf, cfg.AuthzCacheTTL)\n` +
|
|
292
|
-
` ...then pass roleSvc as user.NewService's last argument.`);
|
|
173
|
+
const wiring = { goModule, queueBackend: "river", store, worker };
|
|
174
|
+
const authRouteLine = (0, auth_patcher_1.authHandlerLineFor)(wiring);
|
|
175
|
+
const roleCompositionLine = "roleComposition := role.NewCompositionFromDB(db, cfg.JWTSecret, cfg.AuthzCacheTTL)";
|
|
176
|
+
const roleHandlerLine = "roleComposition.Handler.Register(api)";
|
|
177
|
+
if (!content.includes(authRouteLine)) {
|
|
178
|
+
throw new Error(`cmd/api/wiring.go's auth route doesn't match what \`add auth\` wrote, so \`add rbac\` can't extend it.\n` +
|
|
179
|
+
`Expected to find:\n ${authRouteLine}\n\n` +
|
|
180
|
+
"It was probably hand-edited. Restore that route and re-run — nothing has been changed yet.");
|
|
293
181
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"authz := middleware.NewAuthz(roleSvc.PermissionsOf, cfg.AuthzCacheTTL)",
|
|
297
|
-
`${userSvcLine.slice(0, -1)}, roleSvc)`,
|
|
298
|
-
].join("\n"));
|
|
299
|
-
const userRouteLine = `user.NewHandler(userSvc, cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, cfg.CookieSameSite, ${limiter}).Register(api)`;
|
|
300
|
-
// strip the trailing `).Register(api)` — not just `.Register(api)` — so authz
|
|
301
|
-
// lands inside NewHandler's argument list rather than after its closing paren
|
|
302
|
-
const tail = ").Register(api)";
|
|
303
|
-
content = content.replace(userRouteLine, `${userRouteLine.slice(0, -tail.length)}, authz${tail}`);
|
|
304
|
-
const roleRouteLine = "role.NewHandler(roleSvc, cfg.JWTSecret, authz).Register(api)";
|
|
305
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, roleRouteLine, roleRouteLine);
|
|
182
|
+
const authWithRoleLine = (0, auth_patcher_1.authHandlerLineFor)(wiring, ["roleComposition.Service", "roleComposition.Authz"]);
|
|
183
|
+
content = content.replace(authRouteLine, [roleCompositionLine, authWithRoleLine, roleHandlerLine].join("\n"));
|
|
306
184
|
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
307
185
|
}
|
|
308
186
|
// patchCmdSeedForRbac makes the seeded admin actually an admin: wires a
|
|
@@ -313,9 +191,10 @@ function patchCmdSeedForRbac(seedMainGoPath, goModule) {
|
|
|
313
191
|
let content = fs_extra_1.default.readFileSync(seedMainGoPath, "utf8");
|
|
314
192
|
const importLine = `"${goModule}/internal/app/role"`;
|
|
315
193
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:seed-imports", importLine, importLine);
|
|
316
|
-
const roleSvcLine = "roleSvc := role.
|
|
194
|
+
const roleSvcLine = "roleSvc := role.NewServiceFromDB(db)";
|
|
317
195
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:seed-services", roleSvcLine, roleSvcLine);
|
|
318
|
-
|
|
196
|
+
const seedRoleDependency = content.includes("user.Dependencies{") ? "Roles: roleSvc," : "roleSvc,";
|
|
197
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:seed-user-service-args", seedRoleDependency, seedRoleDependency);
|
|
319
198
|
const setRoleCall = [
|
|
320
199
|
'if _, err := svc.SetRole(ctx, u.ID, "admin"); err != nil {',
|
|
321
200
|
'\tlogger.Error("promote seed admin to admin role", "error", err)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nakedev/go-scaffold",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Scaffold Gin + GORM + Postgres Go backend projects with a consistent domain-module standard",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,9 +23,13 @@
|
|
|
23
23
|
"test:unit": "node --test tests/unit/*.test.mjs",
|
|
24
24
|
"test:integration": "node --test tests/integration/*.test.mjs",
|
|
25
25
|
"test:smoke": "node scripts/smoke-test.mjs",
|
|
26
|
+
"test:e2e": "playwright test --config=playwright.config.mjs",
|
|
27
|
+
"test:e2e:install": "playwright install chromium",
|
|
26
28
|
"verify": "pnpm run build && pnpm run test",
|
|
29
|
+
"release:check": "node scripts/check-release.mjs",
|
|
30
|
+
"policy:main": "node scripts/check-main-merge.mjs",
|
|
27
31
|
"prepack": "rm -rf dist && pnpm run build",
|
|
28
|
-
"prepublishOnly": "pnpm run verify"
|
|
32
|
+
"prepublishOnly": "pnpm run release:check && pnpm run verify"
|
|
29
33
|
},
|
|
30
34
|
"keywords": [
|
|
31
35
|
"go",
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"pluralize": "^8.0.0"
|
|
50
54
|
},
|
|
51
55
|
"devDependencies": {
|
|
56
|
+
"@playwright/test": "1.55.0",
|
|
52
57
|
"@types/fs-extra": "^11.0.4",
|
|
53
58
|
"@types/node": "^24.0.0",
|
|
54
59
|
"@types/pluralize": "^0.0.33",
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
"os"
|
|
8
8
|
|
|
9
9
|
"{{goModule}}/internal/app/user"
|
|
10
|
+
userpassword "{{goModule}}/internal/app/user/adapters/outbound/password"
|
|
10
11
|
"{{goModule}}/internal/platform/database"
|
|
11
12
|
"{{goModule}}/internal/shared/config"
|
|
12
13
|
// go-scaffold:seed-imports
|
|
@@ -30,10 +31,21 @@ func main() {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// go-scaffold:seed-services
|
|
33
|
-
svc := user.NewService(
|
|
34
|
-
user.NewRepository(db),
|
|
34
|
+
svc := user.NewService(user.Dependencies{
|
|
35
|
+
Repository: user.NewRepository(db),
|
|
36
|
+
Passwords: userpassword.NewBCryptHasher(),
|
|
35
37
|
// go-scaffold:seed-user-service-args
|
|
36
|
-
|
|
38
|
+
}, user.AuthConfig{
|
|
39
|
+
JWTSecret: cfg.JWTSecret,
|
|
40
|
+
JWTAccessTTL: cfg.JWTAccessTTL,
|
|
41
|
+
JWTRefreshTTL: cfg.JWTRefreshTTL,
|
|
42
|
+
JWTRefreshMaxTTL: cfg.JWTRefreshMaxTTL,
|
|
43
|
+
OAuthStateTTL: cfg.OAuthStateTTL,
|
|
44
|
+
PasswordResetTTL: cfg.PasswordResetTTL,
|
|
45
|
+
PasswordResetURL: cfg.PasswordResetURL,
|
|
46
|
+
EmailVerifyTTL: cfg.EmailVerifyTTL,
|
|
47
|
+
EmailVerifyURL: cfg.EmailVerifyURL,
|
|
48
|
+
})
|
|
37
49
|
ctx := context.Background()
|
|
38
50
|
|
|
39
51
|
if email := os.Getenv("SEED_ADMIN_EMAIL"); email != "" {
|
|
@@ -11,9 +11,19 @@ post:
|
|
|
11
11
|
responses:
|
|
12
12
|
"200":
|
|
13
13
|
description: ok, refresh_token set as an httpOnly cookie
|
|
14
|
+
headers:
|
|
15
|
+
Cache-Control:
|
|
16
|
+
description: token responses are never cacheable
|
|
17
|
+
schema: { type: string, example: no-store }
|
|
18
|
+
Pragma:
|
|
19
|
+
description: legacy cache prevention for token responses
|
|
20
|
+
schema: { type: string, example: no-cache }
|
|
14
21
|
content:
|
|
15
22
|
application/json:
|
|
16
|
-
schema:
|
|
23
|
+
schema:
|
|
24
|
+
oneOf:
|
|
25
|
+
- { $ref: './schemas.yaml#/AuthResponse' }
|
|
26
|
+
- { $ref: './schemas.yaml#/MFAChallengeResponse' }
|
|
17
27
|
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
18
28
|
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
19
29
|
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Complete a login with a TOTP or recovery code
|
|
3
|
+
operationId: verifyMFA
|
|
4
|
+
tags: [auth]
|
|
5
|
+
security: []
|
|
6
|
+
requestBody:
|
|
7
|
+
required: true
|
|
8
|
+
content:
|
|
9
|
+
application/json:
|
|
10
|
+
schema: { $ref: './schemas.yaml#/MFAChallengeInput' }
|
|
11
|
+
responses:
|
|
12
|
+
"200":
|
|
13
|
+
description: authenticated; refresh_token set as an httpOnly cookie
|
|
14
|
+
content:
|
|
15
|
+
application/json:
|
|
16
|
+
schema: { $ref: './schemas.yaml#/AuthResponse' }
|
|
17
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
18
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
19
|
+
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|