create-forgeon 0.2.5 → 0.2.7
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/package.json +1 -1
- package/src/modules/executor.mjs +4 -0
- package/src/modules/executor.test.mjs +214 -0
- package/src/modules/i18n.mjs +113 -0
- package/src/modules/rate-limit.mjs +346 -0
- package/src/modules/rbac.mjs +324 -0
- package/src/modules/registry.mjs +39 -3
- package/src/run-add-module.mjs +83 -6
- package/templates/base/README.md +2 -2
- package/templates/base/docs/AI/MODULE_SPEC.md +9 -4
- package/templates/module-fragments/rate-limit/00_title.md +1 -0
- package/templates/module-fragments/rate-limit/10_overview.md +6 -0
- package/templates/module-fragments/rate-limit/20_idea.md +11 -0
- package/templates/module-fragments/rate-limit/30_what_it_adds.md +10 -0
- package/templates/module-fragments/rate-limit/40_how_it_works.md +13 -0
- package/templates/module-fragments/rate-limit/50_how_to_use.md +21 -0
- package/templates/module-fragments/rate-limit/60_configuration.md +15 -0
- package/templates/module-fragments/rate-limit/70_operational_notes.md +10 -0
- package/templates/module-fragments/rate-limit/90_status_implemented.md +3 -0
- package/templates/module-fragments/rbac/00_title.md +1 -0
- package/templates/module-fragments/rbac/10_overview.md +6 -0
- package/templates/module-fragments/rbac/20_idea.md +9 -0
- package/templates/module-fragments/rbac/30_what_it_adds.md +11 -0
- package/templates/module-fragments/rbac/40_how_it_works.md +20 -0
- package/templates/module-fragments/rbac/50_how_to_use.md +19 -0
- package/templates/module-fragments/rbac/60_configuration.md +9 -0
- package/templates/module-fragments/rbac/70_operational_notes.md +10 -0
- package/templates/module-fragments/rbac/90_status_implemented.md +3 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/package.json +22 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/src/forgeon-rate-limit.module.ts +50 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/src/index.ts +5 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/src/rate-limit-config.loader.ts +25 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/src/rate-limit-config.module.ts +8 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/src/rate-limit-config.service.ts +35 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/src/rate-limit-env.schema.ts +16 -0
- package/templates/module-presets/rate-limit/packages/rate-limit/tsconfig.json +9 -0
- package/templates/module-presets/rbac/packages/rbac/package.json +19 -0
- package/templates/module-presets/rbac/packages/rbac/src/forgeon-rbac.guard.ts +91 -0
- package/templates/module-presets/rbac/packages/rbac/src/forgeon-rbac.module.ts +8 -0
- package/templates/module-presets/rbac/packages/rbac/src/index.ts +6 -0
- package/templates/module-presets/rbac/packages/rbac/src/rbac.constants.ts +4 -0
- package/templates/module-presets/rbac/packages/rbac/src/rbac.decorators.ts +11 -0
- package/templates/module-presets/rbac/packages/rbac/src/rbac.helpers.ts +31 -0
- package/templates/module-presets/rbac/packages/rbac/src/rbac.types.ts +7 -0
- package/templates/module-presets/rbac/packages/rbac/tsconfig.json +9 -0
package/package.json
CHANGED
package/src/modules/executor.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { applyDbPrismaModule } from './db-prisma.mjs';
|
|
|
6
6
|
import { applyI18nModule } from './i18n.mjs';
|
|
7
7
|
import { applyJwtAuthModule } from './jwt-auth.mjs';
|
|
8
8
|
import { applyLoggerModule } from './logger.mjs';
|
|
9
|
+
import { applyRateLimitModule } from './rate-limit.mjs';
|
|
10
|
+
import { applyRbacModule } from './rbac.mjs';
|
|
9
11
|
import { applySwaggerModule } from './swagger.mjs';
|
|
10
12
|
|
|
11
13
|
function ensureForgeonLikeProject(targetRoot) {
|
|
@@ -29,6 +31,8 @@ const MODULE_APPLIERS = {
|
|
|
29
31
|
i18n: applyI18nModule,
|
|
30
32
|
'jwt-auth': applyJwtAuthModule,
|
|
31
33
|
logger: applyLoggerModule,
|
|
34
|
+
'rate-limit': applyRateLimitModule,
|
|
35
|
+
rbac: applyRbacModule,
|
|
32
36
|
swagger: applySwaggerModule,
|
|
33
37
|
};
|
|
34
38
|
|
|
@@ -42,6 +42,76 @@ function assertDbPrismaWiring(projectRoot) {
|
|
|
42
42
|
assert.match(healthController, /PrismaService/);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function assertRateLimitWiring(projectRoot) {
|
|
46
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
47
|
+
assert.match(appModule, /rateLimitConfig/);
|
|
48
|
+
assert.match(appModule, /rateLimitEnvSchema/);
|
|
49
|
+
assert.match(appModule, /ForgeonRateLimitModule/);
|
|
50
|
+
|
|
51
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
52
|
+
assert.match(apiPackage, /@forgeon\/rate-limit/);
|
|
53
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/rate-limit build/);
|
|
54
|
+
|
|
55
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
56
|
+
assert.match(apiDockerfile, /COPY packages\/rate-limit\/package\.json packages\/rate-limit\/package\.json/);
|
|
57
|
+
assert.match(apiDockerfile, /COPY packages\/rate-limit packages\/rate-limit/);
|
|
58
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/rate-limit build/);
|
|
59
|
+
|
|
60
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
61
|
+
assert.match(compose, /THROTTLE_ENABLED: \$\{THROTTLE_ENABLED\}/);
|
|
62
|
+
assert.match(compose, /THROTTLE_LIMIT: \$\{THROTTLE_LIMIT\}/);
|
|
63
|
+
|
|
64
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
65
|
+
assert.match(apiEnv, /THROTTLE_ENABLED=true/);
|
|
66
|
+
assert.match(apiEnv, /THROTTLE_TTL=10/);
|
|
67
|
+
assert.match(apiEnv, /THROTTLE_LIMIT=3/);
|
|
68
|
+
|
|
69
|
+
const healthController = fs.readFileSync(
|
|
70
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
71
|
+
'utf8',
|
|
72
|
+
);
|
|
73
|
+
assert.match(healthController, /@Get\('rate-limit'\)/);
|
|
74
|
+
assert.match(healthController, /TOO_MANY_REQUESTS/);
|
|
75
|
+
|
|
76
|
+
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
77
|
+
assert.match(appTsx, /Check rate limit \(click repeatedly\)/);
|
|
78
|
+
assert.match(appTsx, /Rate limit probe response/);
|
|
79
|
+
|
|
80
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
81
|
+
assert.match(readme, /## Rate Limit Module/);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assertRbacWiring(projectRoot) {
|
|
85
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
86
|
+
assert.match(appModule, /ForgeonRbacModule/);
|
|
87
|
+
|
|
88
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
89
|
+
assert.match(apiPackage, /@forgeon\/rbac/);
|
|
90
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/rbac build/);
|
|
91
|
+
|
|
92
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
93
|
+
assert.match(apiDockerfile, /COPY packages\/rbac\/package\.json packages\/rbac\/package\.json/);
|
|
94
|
+
assert.match(apiDockerfile, /COPY packages\/rbac packages\/rbac/);
|
|
95
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/rbac build/);
|
|
96
|
+
|
|
97
|
+
const healthController = fs.readFileSync(
|
|
98
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
99
|
+
'utf8',
|
|
100
|
+
);
|
|
101
|
+
assert.match(healthController, /UseGuards/);
|
|
102
|
+
assert.match(healthController, /ForgeonRbacGuard/);
|
|
103
|
+
assert.match(healthController, /@Get\('rbac'\)/);
|
|
104
|
+
assert.match(healthController, /@Permissions\('health\.rbac'\)/);
|
|
105
|
+
|
|
106
|
+
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
107
|
+
assert.match(appTsx, /Check RBAC access/);
|
|
108
|
+
assert.match(appTsx, /RBAC probe response/);
|
|
109
|
+
assert.match(appTsx, /x-forgeon-permissions/);
|
|
110
|
+
|
|
111
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
112
|
+
assert.match(readme, /## RBAC \/ Permissions Module/);
|
|
113
|
+
}
|
|
114
|
+
|
|
45
115
|
function assertJwtAuthWiring(projectRoot, withPrismaStore) {
|
|
46
116
|
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
47
117
|
assert.match(apiPackage, /@forgeon\/auth-api/);
|
|
@@ -529,6 +599,76 @@ describe('addModule', () => {
|
|
|
529
599
|
}
|
|
530
600
|
});
|
|
531
601
|
|
|
602
|
+
it('applies rate-limit module on top of scaffold without i18n', () => {
|
|
603
|
+
const targetRoot = mkTmp('forgeon-module-rate-limit-');
|
|
604
|
+
const projectRoot = path.join(targetRoot, 'demo-rate-limit');
|
|
605
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
scaffoldProject({
|
|
609
|
+
templateRoot,
|
|
610
|
+
packageRoot,
|
|
611
|
+
targetRoot: projectRoot,
|
|
612
|
+
projectName: 'demo-rate-limit',
|
|
613
|
+
frontend: 'react',
|
|
614
|
+
db: 'prisma',
|
|
615
|
+
dbPrismaEnabled: false,
|
|
616
|
+
i18nEnabled: false,
|
|
617
|
+
proxy: 'caddy',
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const result = addModule({
|
|
621
|
+
moduleId: 'rate-limit',
|
|
622
|
+
targetRoot: projectRoot,
|
|
623
|
+
packageRoot,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
assert.equal(result.applied, true);
|
|
627
|
+
assertRateLimitWiring(projectRoot);
|
|
628
|
+
|
|
629
|
+
const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
|
|
630
|
+
assert.match(moduleDoc, /## Idea \/ Why/);
|
|
631
|
+
assert.match(moduleDoc, /## Configuration/);
|
|
632
|
+
} finally {
|
|
633
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('applies rbac module on top of scaffold without i18n', () => {
|
|
638
|
+
const targetRoot = mkTmp('forgeon-module-rbac-');
|
|
639
|
+
const projectRoot = path.join(targetRoot, 'demo-rbac');
|
|
640
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
scaffoldProject({
|
|
644
|
+
templateRoot,
|
|
645
|
+
packageRoot,
|
|
646
|
+
targetRoot: projectRoot,
|
|
647
|
+
projectName: 'demo-rbac',
|
|
648
|
+
frontend: 'react',
|
|
649
|
+
db: 'prisma',
|
|
650
|
+
dbPrismaEnabled: false,
|
|
651
|
+
i18nEnabled: false,
|
|
652
|
+
proxy: 'caddy',
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const result = addModule({
|
|
656
|
+
moduleId: 'rbac',
|
|
657
|
+
targetRoot: projectRoot,
|
|
658
|
+
packageRoot,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
assert.equal(result.applied, true);
|
|
662
|
+
assertRbacWiring(projectRoot);
|
|
663
|
+
|
|
664
|
+
const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
|
|
665
|
+
assert.match(moduleDoc, /## Idea \/ Why/);
|
|
666
|
+
assert.match(moduleDoc, /## How It Works/);
|
|
667
|
+
} finally {
|
|
668
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
532
672
|
it('applies swagger module on top of scaffold without i18n', () => {
|
|
533
673
|
const targetRoot = mkTmp('forgeon-module-swagger-');
|
|
534
674
|
const projectRoot = path.join(targetRoot, 'demo-swagger');
|
|
@@ -1144,6 +1284,80 @@ describe('addModule', () => {
|
|
|
1144
1284
|
}
|
|
1145
1285
|
});
|
|
1146
1286
|
|
|
1287
|
+
it('keeps rate-limit wiring valid after mixed module installation order', () => {
|
|
1288
|
+
const targetRoot = mkTmp('forgeon-module-rate-limit-order-');
|
|
1289
|
+
const projectRoot = path.join(targetRoot, 'demo-rate-limit-order');
|
|
1290
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
scaffoldProject({
|
|
1294
|
+
templateRoot,
|
|
1295
|
+
packageRoot,
|
|
1296
|
+
targetRoot: projectRoot,
|
|
1297
|
+
projectName: 'demo-rate-limit-order',
|
|
1298
|
+
frontend: 'react',
|
|
1299
|
+
db: 'prisma',
|
|
1300
|
+
dbPrismaEnabled: false,
|
|
1301
|
+
i18nEnabled: false,
|
|
1302
|
+
proxy: 'caddy',
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
for (const moduleId of ['jwt-auth', 'logger', 'swagger', 'rate-limit', 'i18n', 'db-prisma']) {
|
|
1306
|
+
addModule({ moduleId, targetRoot: projectRoot, packageRoot });
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
assertRateLimitWiring(projectRoot);
|
|
1310
|
+
|
|
1311
|
+
const healthController = fs.readFileSync(
|
|
1312
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
1313
|
+
'utf8',
|
|
1314
|
+
);
|
|
1315
|
+
const classStart = healthController.indexOf('export class HealthController {');
|
|
1316
|
+
const classEnd = healthController.lastIndexOf('\n}');
|
|
1317
|
+
const rateLimitProbe = healthController.indexOf("@Get('rate-limit')");
|
|
1318
|
+
assert.equal(rateLimitProbe > classStart && rateLimitProbe < classEnd, true);
|
|
1319
|
+
} finally {
|
|
1320
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it('keeps rbac wiring valid after mixed module installation order', () => {
|
|
1325
|
+
const targetRoot = mkTmp('forgeon-module-rbac-order-');
|
|
1326
|
+
const projectRoot = path.join(targetRoot, 'demo-rbac-order');
|
|
1327
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1328
|
+
|
|
1329
|
+
try {
|
|
1330
|
+
scaffoldProject({
|
|
1331
|
+
templateRoot,
|
|
1332
|
+
packageRoot,
|
|
1333
|
+
targetRoot: projectRoot,
|
|
1334
|
+
projectName: 'demo-rbac-order',
|
|
1335
|
+
frontend: 'react',
|
|
1336
|
+
db: 'prisma',
|
|
1337
|
+
dbPrismaEnabled: false,
|
|
1338
|
+
i18nEnabled: false,
|
|
1339
|
+
proxy: 'caddy',
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
for (const moduleId of ['jwt-auth', 'logger', 'rate-limit', 'rbac', 'swagger', 'i18n', 'db-prisma']) {
|
|
1343
|
+
addModule({ moduleId, targetRoot: projectRoot, packageRoot });
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
assertRbacWiring(projectRoot);
|
|
1347
|
+
|
|
1348
|
+
const healthController = fs.readFileSync(
|
|
1349
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
1350
|
+
'utf8',
|
|
1351
|
+
);
|
|
1352
|
+
const classStart = healthController.indexOf('export class HealthController {');
|
|
1353
|
+
const classEnd = healthController.lastIndexOf('\n}');
|
|
1354
|
+
const rbacProbe = healthController.indexOf("@Get('rbac')");
|
|
1355
|
+
assert.equal(rbacProbe > classStart && rbacProbe < classEnd, true);
|
|
1356
|
+
} finally {
|
|
1357
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1147
1361
|
it('keeps db-prisma wiring across module installation orders', () => {
|
|
1148
1362
|
const sequences = [
|
|
1149
1363
|
['logger', 'swagger', 'i18n'],
|
package/src/modules/i18n.mjs
CHANGED
|
@@ -405,7 +405,119 @@ function patchRootPackage(targetRoot) {
|
|
|
405
405
|
writeJson(packagePath, packageJson);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
function restoreKnownWebProbes(targetRoot, previousAppContent) {
|
|
409
|
+
if (!previousAppContent) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
|
|
414
|
+
if (!fs.existsSync(filePath)) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
419
|
+
|
|
420
|
+
const ensureProbeState = (stateLine) => {
|
|
421
|
+
if (content.includes(stateLine)) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const anchors = [
|
|
425
|
+
' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
|
|
426
|
+
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
427
|
+
' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
|
|
428
|
+
' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
|
|
429
|
+
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
430
|
+
];
|
|
431
|
+
const anchor = anchors.find((line) => content.includes(line));
|
|
432
|
+
if (anchor) {
|
|
433
|
+
content = ensureLineAfter(content, anchor, stateLine);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const ensureProbeButton = (buttonText, buttonCode) => {
|
|
438
|
+
if (content.includes(buttonText)) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const actionsStart = content.indexOf('<div className="actions">');
|
|
442
|
+
if (actionsStart < 0) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
446
|
+
if (actionsEnd < 0) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
content = `${content.slice(0, actionsEnd)}\n${buttonCode}${content.slice(actionsEnd)}`;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const ensureProbeResult = (resultLine) => {
|
|
453
|
+
if (content.includes(resultLine)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
457
|
+
if (content.includes(networkLine)) {
|
|
458
|
+
content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const anchors = [
|
|
462
|
+
" {renderResult('RBAC probe response', rbacProbeResult)}",
|
|
463
|
+
" {renderResult('Rate limit probe response', rateLimitProbeResult)}",
|
|
464
|
+
" {renderResult('Auth probe response', authProbeResult)}",
|
|
465
|
+
" {renderResult('DB probe response', dbProbeResult)}",
|
|
466
|
+
" {renderResult('Validation probe response', validationProbeResult)}",
|
|
467
|
+
];
|
|
468
|
+
const anchor = anchors.find((line) => content.includes(line));
|
|
469
|
+
if (anchor) {
|
|
470
|
+
content = ensureLineAfter(content, anchor, resultLine);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
if (previousAppContent.includes('Check database (create user)')) {
|
|
475
|
+
ensureProbeState(' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);');
|
|
476
|
+
ensureProbeButton(
|
|
477
|
+
'Check database (create user)',
|
|
478
|
+
" <button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>",
|
|
479
|
+
);
|
|
480
|
+
ensureProbeResult(" {renderResult('DB probe response', dbProbeResult)}");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (previousAppContent.includes('Check JWT auth probe')) {
|
|
484
|
+
ensureProbeState(' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);');
|
|
485
|
+
ensureProbeButton(
|
|
486
|
+
'Check JWT auth probe',
|
|
487
|
+
" <button onClick={() => runProbe(setAuthProbeResult, '/health/auth')}>Check JWT auth probe</button>",
|
|
488
|
+
);
|
|
489
|
+
ensureProbeResult(" {renderResult('Auth probe response', authProbeResult)}");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (previousAppContent.includes('Check rate limit (click repeatedly)')) {
|
|
493
|
+
ensureProbeState(
|
|
494
|
+
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
495
|
+
);
|
|
496
|
+
ensureProbeButton(
|
|
497
|
+
'Check rate limit (click repeatedly)',
|
|
498
|
+
" <button onClick={() => runProbe(setRateLimitProbeResult, '/health/rate-limit')}>\n Check rate limit (click repeatedly)\n </button>",
|
|
499
|
+
);
|
|
500
|
+
ensureProbeResult(" {renderResult('Rate limit probe response', rateLimitProbeResult)}");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (previousAppContent.includes('Check RBAC access')) {
|
|
504
|
+
ensureProbeState(' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);');
|
|
505
|
+
ensureProbeButton(
|
|
506
|
+
'Check RBAC access',
|
|
507
|
+
" <button\n onClick={() =>\n runProbe(setRbacProbeResult, '/health/rbac', {\n headers: { 'x-forgeon-permissions': 'health.rbac' },\n })\n }\n >\n Check RBAC access\n </button>",
|
|
508
|
+
);
|
|
509
|
+
ensureProbeResult(" {renderResult('RBAC probe response', rbacProbeResult)}");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
513
|
+
}
|
|
514
|
+
|
|
408
515
|
export function applyI18nModule({ packageRoot, targetRoot }) {
|
|
516
|
+
const existingWebAppPath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
|
|
517
|
+
const previousAppContent = fs.existsSync(existingWebAppPath)
|
|
518
|
+
? fs.readFileSync(existingWebAppPath, 'utf8')
|
|
519
|
+
: '';
|
|
520
|
+
|
|
409
521
|
copyFromBase(packageRoot, targetRoot, path.join('scripts', 'i18n-add.mjs'));
|
|
410
522
|
copyFromBase(packageRoot, targetRoot, path.join('packages', 'i18n'));
|
|
411
523
|
copyFromBase(packageRoot, targetRoot, path.join('resources', 'i18n'));
|
|
@@ -415,6 +527,7 @@ export function applyI18nModule({ packageRoot, targetRoot }) {
|
|
|
415
527
|
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'App.tsx'));
|
|
416
528
|
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'i18n.ts'));
|
|
417
529
|
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'main.tsx'));
|
|
530
|
+
restoreKnownWebProbes(targetRoot, previousAppContent);
|
|
418
531
|
|
|
419
532
|
patchI18nPackage(targetRoot);
|
|
420
533
|
patchApiPackage(targetRoot);
|