create-forgeon 0.2.2 → 0.2.4
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/bin/create-forgeon.mjs +9 -6
- package/package.json +1 -1
- package/src/cli/add-help.mjs +10 -6
- package/src/cli/help.mjs +13 -9
- package/src/integrations/flow.mjs +118 -0
- package/src/modules/db-prisma.mjs +26 -172
- package/src/modules/executor.test.mjs +150 -10
- package/src/modules/i18n.mjs +29 -150
- package/src/modules/jwt-auth.mjs +37 -297
- package/src/modules/logger.mjs +10 -118
- package/src/modules/shared/patch-utils.mjs +237 -0
- package/src/modules/swagger.mjs +10 -121
- package/src/modules/sync-integrations.mjs +269 -0
- package/src/run-add-module.mjs +8 -42
- package/src/run-scan-integrations.mjs +93 -0
- package/templates/base/docs/AI/ARCHITECTURE.md +17 -0
- package/templates/base/docs/AI/MODULE_SPEC.md +4 -0
- package/templates/base/package.json +0 -3
- package/templates/base/scripts/forgeon-sync-integrations.mjs +44 -241
package/src/modules/i18n.mjs
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureClassMember,
|
|
7
|
+
ensureDependency,
|
|
8
|
+
ensureImportLine,
|
|
9
|
+
ensureLineAfter,
|
|
10
|
+
ensureLineBefore,
|
|
11
|
+
ensureLoadItem,
|
|
12
|
+
ensureScript,
|
|
13
|
+
ensureValidatorSchema,
|
|
14
|
+
upsertEnvLines,
|
|
15
|
+
} from './shared/patch-utils.mjs';
|
|
4
16
|
|
|
5
17
|
function copyFromBase(packageRoot, targetRoot, relativePath) {
|
|
6
18
|
const source = path.join(packageRoot, 'templates', 'base', relativePath);
|
|
@@ -20,140 +32,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
|
20
32
|
copyRecursive(source, destination);
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
function ensureDependency(packageJson, name, version) {
|
|
24
|
-
if (!packageJson.dependencies) {
|
|
25
|
-
packageJson.dependencies = {};
|
|
26
|
-
}
|
|
27
|
-
packageJson.dependencies[name] = version;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function ensureScript(packageJson, name, command) {
|
|
31
|
-
if (!packageJson.scripts) {
|
|
32
|
-
packageJson.scripts = {};
|
|
33
|
-
}
|
|
34
|
-
packageJson.scripts[name] = command;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
|
|
38
|
-
if (!packageJson.scripts) {
|
|
39
|
-
packageJson.scripts = {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const current = packageJson.scripts[scriptName];
|
|
43
|
-
const steps =
|
|
44
|
-
typeof current === 'string' && current.trim().length > 0
|
|
45
|
-
? current
|
|
46
|
-
.split('&&')
|
|
47
|
-
.map((item) => item.trim())
|
|
48
|
-
.filter(Boolean)
|
|
49
|
-
: [];
|
|
50
|
-
|
|
51
|
-
for (const command of requiredCommands) {
|
|
52
|
-
if (!steps.includes(command)) {
|
|
53
|
-
steps.push(command);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (steps.length > 0) {
|
|
58
|
-
packageJson.scripts[scriptName] = steps.join(' && ');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function upsertEnvLines(filePath, lines) {
|
|
63
|
-
let content = '';
|
|
64
|
-
if (fs.existsSync(filePath)) {
|
|
65
|
-
content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const keys = new Set(
|
|
69
|
-
content
|
|
70
|
-
.split('\n')
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
.map((line) => line.split('=')[0]),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const append = [];
|
|
76
|
-
for (const line of lines) {
|
|
77
|
-
const key = line.split('=')[0];
|
|
78
|
-
if (!keys.has(key)) {
|
|
79
|
-
append.push(line);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const next =
|
|
84
|
-
append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
|
|
85
|
-
fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
89
|
-
if (content.includes(lineToInsert)) {
|
|
90
|
-
return content;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const index = content.indexOf(anchorLine);
|
|
94
|
-
if (index < 0) {
|
|
95
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const insertAt = index + anchorLine.length;
|
|
99
|
-
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function ensureLineBefore(content, anchorLine, lineToInsert) {
|
|
103
|
-
if (content.includes(lineToInsert)) {
|
|
104
|
-
return content;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const index = content.indexOf(anchorLine);
|
|
108
|
-
if (index < 0) {
|
|
109
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function ensureLoadItem(content, itemName) {
|
|
116
|
-
const pattern = /load:\s*\[([^\]]*)\]/m;
|
|
117
|
-
const match = content.match(pattern);
|
|
118
|
-
if (!match) {
|
|
119
|
-
return content;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const rawList = match[1];
|
|
123
|
-
const items = rawList
|
|
124
|
-
.split(',')
|
|
125
|
-
.map((item) => item.trim())
|
|
126
|
-
.filter(Boolean);
|
|
127
|
-
|
|
128
|
-
if (!items.includes(itemName)) {
|
|
129
|
-
items.push(itemName);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const next = `load: [${items.join(', ')}]`;
|
|
133
|
-
return content.replace(pattern, next);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function ensureValidatorSchema(content, schemaName) {
|
|
137
|
-
const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
|
|
138
|
-
const match = content.match(pattern);
|
|
139
|
-
if (!match) {
|
|
140
|
-
return content;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const rawList = match[1];
|
|
144
|
-
const items = rawList
|
|
145
|
-
.split(',')
|
|
146
|
-
.map((item) => item.trim())
|
|
147
|
-
.filter(Boolean);
|
|
148
|
-
|
|
149
|
-
if (!items.includes(schemaName)) {
|
|
150
|
-
items.push(schemaName);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const next = `validate: createEnvValidator([${items.join(', ')}])`;
|
|
154
|
-
return content.replace(pattern, next);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
35
|
function patchApiDockerfile(targetRoot) {
|
|
158
36
|
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
159
37
|
if (!fs.existsSync(dockerfilePath)) {
|
|
@@ -412,19 +290,7 @@ function patchHealthController(targetRoot) {
|
|
|
412
290
|
|
|
413
291
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
414
292
|
if (!content.includes("from 'nestjs-i18n';")) {
|
|
415
|
-
|
|
416
|
-
content = ensureLineAfter(
|
|
417
|
-
content,
|
|
418
|
-
"import { PrismaService } from '@forgeon/db-prisma';",
|
|
419
|
-
"import { I18nService } from 'nestjs-i18n';",
|
|
420
|
-
);
|
|
421
|
-
} else {
|
|
422
|
-
content = ensureLineAfter(
|
|
423
|
-
content,
|
|
424
|
-
"import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';",
|
|
425
|
-
"import { I18nService } from 'nestjs-i18n';",
|
|
426
|
-
);
|
|
427
|
-
}
|
|
293
|
+
content = ensureImportLine(content, "import { I18nService } from 'nestjs-i18n';");
|
|
428
294
|
}
|
|
429
295
|
|
|
430
296
|
if (!content.includes('private readonly i18n: I18nService')) {
|
|
@@ -432,11 +298,22 @@ function patchHealthController(targetRoot) {
|
|
|
432
298
|
if (constructorMatch) {
|
|
433
299
|
const original = constructorMatch[0];
|
|
434
300
|
const inner = constructorMatch[1].trimEnd();
|
|
435
|
-
const
|
|
436
|
-
const
|
|
301
|
+
const normalizedInner = inner.replace(/,\s*$/, '');
|
|
302
|
+
const separator = normalizedInner.length > 0 ? ',' : '';
|
|
303
|
+
const next = `constructor(${normalizedInner}${separator}
|
|
437
304
|
private readonly i18n: I18nService,
|
|
438
305
|
) {`;
|
|
439
306
|
content = content.replace(original, next);
|
|
307
|
+
} else {
|
|
308
|
+
const classAnchor = 'export class HealthController {';
|
|
309
|
+
if (content.includes(classAnchor)) {
|
|
310
|
+
content = content.replace(
|
|
311
|
+
classAnchor,
|
|
312
|
+
`${classAnchor}
|
|
313
|
+
constructor(private readonly i18n: I18nService) {}
|
|
314
|
+
`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
440
317
|
}
|
|
441
318
|
}
|
|
442
319
|
|
|
@@ -447,9 +324,10 @@ function patchHealthController(targetRoot) {
|
|
|
447
324
|
return typeof value === 'string' ? value : key;
|
|
448
325
|
}
|
|
449
326
|
`;
|
|
450
|
-
content =
|
|
327
|
+
content = ensureClassMember(content, 'HealthController', translateMethod);
|
|
451
328
|
}
|
|
452
329
|
|
|
330
|
+
content = content.replace(/getHealth\(\)/g, "getHealth(@Query('lang') lang?: string)");
|
|
453
331
|
content = content.replace(
|
|
454
332
|
/getHealth\(@Query\('lang'\)\s*_lang\?:\s*string\)/g,
|
|
455
333
|
"getHealth(@Query('lang') lang?: string)",
|
|
@@ -463,6 +341,7 @@ function patchHealthController(targetRoot) {
|
|
|
463
341
|
"getValidationProbe(@Query('value') value?: string, @Query('lang') lang?: string)",
|
|
464
342
|
);
|
|
465
343
|
content = content.replace(/message:\s*'OK',/g, "message: this.translate('common.actions.ok', lang),");
|
|
344
|
+
content = content.replace(/i18n:\s*'disabled',/g, "i18n: 'en',");
|
|
466
345
|
content = content.replace(/i18n:\s*'English',/g, "i18n: 'en',");
|
|
467
346
|
|
|
468
347
|
content = content.replace(
|
package/src/modules/jwt-auth.mjs
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureClassMember,
|
|
7
|
+
ensureDependency,
|
|
8
|
+
ensureImportLine,
|
|
9
|
+
ensureLineAfter,
|
|
10
|
+
ensureLineBefore,
|
|
11
|
+
ensureLoadItem,
|
|
12
|
+
ensureValidatorSchema,
|
|
13
|
+
upsertEnvLines,
|
|
14
|
+
} from './shared/patch-utils.mjs';
|
|
4
15
|
|
|
5
16
|
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
6
17
|
const source = path.join(packageRoot, 'templates', 'module-presets', 'jwt-auth', relativePath);
|
|
@@ -12,177 +23,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
|
12
23
|
copyRecursive(source, destination);
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
function ensureDependency(packageJson, name, version) {
|
|
16
|
-
if (!packageJson.dependencies) {
|
|
17
|
-
packageJson.dependencies = {};
|
|
18
|
-
}
|
|
19
|
-
packageJson.dependencies[name] = version;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
|
|
23
|
-
if (!packageJson.scripts) {
|
|
24
|
-
packageJson.scripts = {};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const current = packageJson.scripts[scriptName];
|
|
28
|
-
const steps =
|
|
29
|
-
typeof current === 'string' && current.trim().length > 0
|
|
30
|
-
? current
|
|
31
|
-
.split('&&')
|
|
32
|
-
.map((item) => item.trim())
|
|
33
|
-
.filter(Boolean)
|
|
34
|
-
: [];
|
|
35
|
-
|
|
36
|
-
for (const command of requiredCommands) {
|
|
37
|
-
if (!steps.includes(command)) {
|
|
38
|
-
steps.push(command);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (steps.length > 0) {
|
|
43
|
-
packageJson.scripts[scriptName] = steps.join(' && ');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
48
|
-
if (content.includes(lineToInsert)) {
|
|
49
|
-
return content;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const index = content.indexOf(anchorLine);
|
|
53
|
-
if (index < 0) {
|
|
54
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const insertAt = index + anchorLine.length;
|
|
58
|
-
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function ensureLineBefore(content, anchorLine, lineToInsert) {
|
|
62
|
-
if (content.includes(lineToInsert)) {
|
|
63
|
-
return content;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const index = content.indexOf(anchorLine);
|
|
67
|
-
if (index < 0) {
|
|
68
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function ensureLoadItem(content, itemName) {
|
|
75
|
-
const pattern = /load:\s*\[([^\]]*)\]/m;
|
|
76
|
-
const match = content.match(pattern);
|
|
77
|
-
if (!match) {
|
|
78
|
-
return content;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const rawList = match[1];
|
|
82
|
-
const items = rawList
|
|
83
|
-
.split(',')
|
|
84
|
-
.map((item) => item.trim())
|
|
85
|
-
.filter(Boolean);
|
|
86
|
-
|
|
87
|
-
if (!items.includes(itemName)) {
|
|
88
|
-
items.push(itemName);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const next = `load: [${items.join(', ')}]`;
|
|
92
|
-
return content.replace(pattern, next);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function ensureValidatorSchema(content, schemaName) {
|
|
96
|
-
const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
|
|
97
|
-
const match = content.match(pattern);
|
|
98
|
-
if (!match) {
|
|
99
|
-
return content;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const rawList = match[1];
|
|
103
|
-
const items = rawList
|
|
104
|
-
.split(',')
|
|
105
|
-
.map((item) => item.trim())
|
|
106
|
-
.filter(Boolean);
|
|
107
|
-
|
|
108
|
-
if (!items.includes(schemaName)) {
|
|
109
|
-
items.push(schemaName);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const next = `validate: createEnvValidator([${items.join(', ')}])`;
|
|
113
|
-
return content.replace(pattern, next);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function upsertEnvLines(filePath, lines) {
|
|
117
|
-
let content = '';
|
|
118
|
-
if (fs.existsSync(filePath)) {
|
|
119
|
-
content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const keys = new Set(
|
|
123
|
-
content
|
|
124
|
-
.split('\n')
|
|
125
|
-
.filter(Boolean)
|
|
126
|
-
.map((line) => line.split('=')[0]),
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const append = [];
|
|
130
|
-
for (const line of lines) {
|
|
131
|
-
const key = line.split('=')[0];
|
|
132
|
-
if (!keys.has(key)) {
|
|
133
|
-
append.push(line);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const next =
|
|
138
|
-
append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
|
|
139
|
-
fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function detectDbAdapter(targetRoot) {
|
|
143
|
-
const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
144
|
-
let deps = {};
|
|
145
|
-
if (fs.existsSync(apiPackagePath)) {
|
|
146
|
-
const packageJson = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
|
|
147
|
-
deps = {
|
|
148
|
-
...(packageJson.dependencies ?? {}),
|
|
149
|
-
...(packageJson.devDependencies ?? {}),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
deps['@forgeon/db-prisma'] ||
|
|
155
|
-
fs.existsSync(path.join(targetRoot, 'packages', 'db-prisma', 'package.json'))
|
|
156
|
-
) {
|
|
157
|
-
return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const dbDeps = Object.keys(deps).filter((name) => name.startsWith('@forgeon/db-'));
|
|
161
|
-
if (dbDeps.length > 0) {
|
|
162
|
-
return { id: dbDeps[0], supported: false, tokenStore: 'none' };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const packagesPath = path.join(targetRoot, 'packages');
|
|
166
|
-
if (fs.existsSync(packagesPath)) {
|
|
167
|
-
const localDbPackages = fs
|
|
168
|
-
.readdirSync(packagesPath, { withFileTypes: true })
|
|
169
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith('db-'))
|
|
170
|
-
.map((entry) => entry.name);
|
|
171
|
-
if (localDbPackages.includes('db-prisma')) {
|
|
172
|
-
return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
|
|
173
|
-
}
|
|
174
|
-
if (localDbPackages.length > 0) {
|
|
175
|
-
return { id: `@forgeon/${localDbPackages[0]}`, supported: false, tokenStore: 'none' };
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function printDbWarning(message) {
|
|
183
|
-
console.error(`\x1b[31m[create-forgeon add jwt-auth] ${message}\x1b[0m`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
26
|
function patchApiPackage(targetRoot) {
|
|
187
27
|
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
188
28
|
if (!fs.existsSync(packagePath)) {
|
|
@@ -201,21 +41,19 @@ function patchApiPackage(targetRoot) {
|
|
|
201
41
|
writeJson(packagePath, packageJson);
|
|
202
42
|
}
|
|
203
43
|
|
|
204
|
-
function patchAppModule(targetRoot
|
|
44
|
+
function patchAppModule(targetRoot) {
|
|
205
45
|
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
206
46
|
if (!fs.existsSync(filePath)) {
|
|
207
47
|
return;
|
|
208
48
|
}
|
|
209
49
|
|
|
210
|
-
const withPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
|
|
211
|
-
|
|
212
50
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
213
51
|
if (!content.includes("from '@forgeon/auth-api';")) {
|
|
214
52
|
if (content.includes("import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';")) {
|
|
215
53
|
content = ensureLineAfter(
|
|
216
54
|
content,
|
|
217
55
|
"import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
|
|
218
|
-
"import {
|
|
56
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
219
57
|
);
|
|
220
58
|
} else if (
|
|
221
59
|
content.includes("import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';")
|
|
@@ -223,7 +61,7 @@ function patchAppModule(targetRoot, dbAdapter) {
|
|
|
223
61
|
content = ensureLineAfter(
|
|
224
62
|
content,
|
|
225
63
|
"import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
|
|
226
|
-
"import {
|
|
64
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
227
65
|
);
|
|
228
66
|
} else if (
|
|
229
67
|
content.includes("import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';")
|
|
@@ -231,7 +69,7 @@ function patchAppModule(targetRoot, dbAdapter) {
|
|
|
231
69
|
content = ensureLineAfter(
|
|
232
70
|
content,
|
|
233
71
|
"import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
|
|
234
|
-
"import {
|
|
72
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
235
73
|
);
|
|
236
74
|
} else if (
|
|
237
75
|
content.includes("import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';")
|
|
@@ -239,38 +77,22 @@ function patchAppModule(targetRoot, dbAdapter) {
|
|
|
239
77
|
content = ensureLineAfter(
|
|
240
78
|
content,
|
|
241
79
|
"import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
|
|
242
|
-
"import {
|
|
80
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
243
81
|
);
|
|
244
82
|
} else {
|
|
245
83
|
content = ensureLineAfter(
|
|
246
84
|
content,
|
|
247
85
|
"import { ConfigModule } from '@nestjs/config';",
|
|
248
|
-
"import {
|
|
86
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
249
87
|
);
|
|
250
88
|
}
|
|
251
89
|
}
|
|
252
90
|
|
|
253
|
-
if (withPrismaStore && !content.includes("./auth/prisma-auth-refresh-token.store")) {
|
|
254
|
-
content = ensureLineBefore(
|
|
255
|
-
content,
|
|
256
|
-
"import { HealthController } from './health/health.controller';",
|
|
257
|
-
"import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';",
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
91
|
content = ensureLoadItem(content, 'authConfig');
|
|
262
92
|
content = ensureValidatorSchema(content, 'authEnvSchema');
|
|
263
93
|
|
|
264
94
|
if (!content.includes('ForgeonAuthModule.register(')) {
|
|
265
|
-
const moduleBlock =
|
|
266
|
-
? ` ForgeonAuthModule.register({
|
|
267
|
-
imports: [DbPrismaModule],
|
|
268
|
-
refreshTokenStoreProvider: {
|
|
269
|
-
provide: AUTH_REFRESH_TOKEN_STORE,
|
|
270
|
-
useClass: PrismaAuthRefreshTokenStore,
|
|
271
|
-
},
|
|
272
|
-
}),`
|
|
273
|
-
: ` ForgeonAuthModule.register(),`;
|
|
95
|
+
const moduleBlock = ' ForgeonAuthModule.register(),';
|
|
274
96
|
|
|
275
97
|
if (content.includes(' ForgeonI18nModule.register({')) {
|
|
276
98
|
content = ensureLineBefore(content, ' ForgeonI18nModule.register({', moduleBlock);
|
|
@@ -297,19 +119,7 @@ function patchHealthController(targetRoot) {
|
|
|
297
119
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
298
120
|
|
|
299
121
|
if (!content.includes("from '@forgeon/auth-api';")) {
|
|
300
|
-
|
|
301
|
-
content = ensureLineAfter(
|
|
302
|
-
content,
|
|
303
|
-
"import { PrismaService } from '@forgeon/db-prisma';",
|
|
304
|
-
"import { AuthService } from '@forgeon/auth-api';",
|
|
305
|
-
);
|
|
306
|
-
} else {
|
|
307
|
-
content = ensureLineAfter(
|
|
308
|
-
content,
|
|
309
|
-
"import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';",
|
|
310
|
-
"import { AuthService } from '@forgeon/auth-api';",
|
|
311
|
-
);
|
|
312
|
-
}
|
|
122
|
+
content = ensureImportLine(content, "import { AuthService } from '@forgeon/auth-api';");
|
|
313
123
|
}
|
|
314
124
|
|
|
315
125
|
if (!content.includes('private readonly authService: AuthService')) {
|
|
@@ -323,6 +133,16 @@ function patchHealthController(targetRoot) {
|
|
|
323
133
|
private readonly authService: AuthService,
|
|
324
134
|
) {`;
|
|
325
135
|
content = content.replace(original, next);
|
|
136
|
+
} else {
|
|
137
|
+
const classAnchor = 'export class HealthController {';
|
|
138
|
+
if (content.includes(classAnchor)) {
|
|
139
|
+
content = content.replace(
|
|
140
|
+
classAnchor,
|
|
141
|
+
`${classAnchor}
|
|
142
|
+
constructor(private readonly authService: AuthService) {}
|
|
143
|
+
`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
326
146
|
}
|
|
327
147
|
}
|
|
328
148
|
|
|
@@ -333,14 +153,8 @@ function patchHealthController(targetRoot) {
|
|
|
333
153
|
return this.authService.getProbeStatus();
|
|
334
154
|
}
|
|
335
155
|
`;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
} else if (content.includes('private translate(')) {
|
|
339
|
-
const index = content.indexOf('private translate(');
|
|
340
|
-
content = `${content.slice(0, index).trimEnd()}\n\n${method}\n${content.slice(index)}`;
|
|
341
|
-
} else {
|
|
342
|
-
content = `${content.trimEnd()}\n${method}\n`;
|
|
343
|
-
}
|
|
156
|
+
const beforeNeedle = content.includes("@Post('db')") ? "@Post('db')" : 'private translate(';
|
|
157
|
+
content = ensureClassMember(content, 'HealthController', method, { beforeNeedle });
|
|
344
158
|
}
|
|
345
159
|
|
|
346
160
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -488,22 +302,17 @@ function patchCompose(targetRoot) {
|
|
|
488
302
|
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
489
303
|
}
|
|
490
304
|
|
|
491
|
-
function patchReadme(targetRoot
|
|
305
|
+
function patchReadme(targetRoot) {
|
|
492
306
|
const readmePath = path.join(targetRoot, 'README.md');
|
|
493
307
|
if (!fs.existsSync(readmePath)) {
|
|
494
308
|
return;
|
|
495
309
|
}
|
|
496
310
|
|
|
497
311
|
const persistenceSummary =
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
: '- refresh token persistence: disabled (no supported DB adapter found)';
|
|
501
|
-
const dbFollowUp =
|
|
502
|
-
dbAdapter?.supported && dbAdapter.id === 'db-prisma'
|
|
503
|
-
? '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`'
|
|
504
|
-
: `- to enable persistence later:
|
|
312
|
+
'- refresh token persistence: disabled by default (stateless mode)';
|
|
313
|
+
const dbFollowUp = `- to enable persistence later:
|
|
505
314
|
1. install a DB module first (for now: \`create-forgeon add db-prisma --project .\`);
|
|
506
|
-
2. run \`
|
|
315
|
+
2. run \`pnpm forgeon:sync-integrations\` to auto-wire pair integrations.`;
|
|
507
316
|
|
|
508
317
|
const section = `## JWT Auth Module
|
|
509
318
|
|
|
@@ -547,86 +356,17 @@ Default routes:
|
|
|
547
356
|
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
548
357
|
}
|
|
549
358
|
|
|
550
|
-
function patchPrismaSchema(targetRoot) {
|
|
551
|
-
const schemaPath = path.join(targetRoot, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
552
|
-
if (!fs.existsSync(schemaPath)) {
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
let content = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
|
|
557
|
-
if (!content.includes('refreshTokenHash')) {
|
|
558
|
-
content = content.replace(
|
|
559
|
-
/email\s+String\s+@unique/g,
|
|
560
|
-
'email String @unique\n refreshTokenHash String?',
|
|
561
|
-
);
|
|
562
|
-
fs.writeFileSync(schemaPath, `${content.trimEnd()}\n`, 'utf8');
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function patchPrismaMigration(packageRoot, targetRoot) {
|
|
567
|
-
const migrationSource = path.join(
|
|
568
|
-
packageRoot,
|
|
569
|
-
'templates',
|
|
570
|
-
'module-presets',
|
|
571
|
-
'jwt-auth',
|
|
572
|
-
'apps',
|
|
573
|
-
'api',
|
|
574
|
-
'prisma',
|
|
575
|
-
'migrations',
|
|
576
|
-
'0002_auth_refresh_token_hash',
|
|
577
|
-
);
|
|
578
|
-
const migrationTarget = path.join(
|
|
579
|
-
targetRoot,
|
|
580
|
-
'apps',
|
|
581
|
-
'api',
|
|
582
|
-
'prisma',
|
|
583
|
-
'migrations',
|
|
584
|
-
'0002_auth_refresh_token_hash',
|
|
585
|
-
);
|
|
586
|
-
|
|
587
|
-
if (!fs.existsSync(migrationTarget) && fs.existsSync(migrationSource)) {
|
|
588
|
-
copyRecursive(migrationSource, migrationTarget);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
359
|
export function applyJwtAuthModule({ packageRoot, targetRoot }) {
|
|
593
|
-
const dbAdapter = detectDbAdapter(targetRoot);
|
|
594
|
-
const supportsPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
|
|
595
|
-
|
|
596
360
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-contracts'));
|
|
597
361
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-api'));
|
|
598
362
|
|
|
599
|
-
const swaggerPackagePath = path.join(targetRoot, 'packages', 'swagger', 'package.json');
|
|
600
|
-
const authApiPackagePath = path.join(targetRoot, 'packages', 'auth-api', 'package.json');
|
|
601
|
-
if (fs.existsSync(swaggerPackagePath) && fs.existsSync(authApiPackagePath)) {
|
|
602
|
-
const authApiPackage = JSON.parse(fs.readFileSync(authApiPackagePath, 'utf8'));
|
|
603
|
-
ensureDependency(authApiPackage, '@nestjs/swagger', '^11.2.0');
|
|
604
|
-
writeJson(authApiPackagePath, authApiPackage);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (supportsPrismaStore) {
|
|
608
|
-
copyFromPreset(
|
|
609
|
-
packageRoot,
|
|
610
|
-
targetRoot,
|
|
611
|
-
path.join('apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts'),
|
|
612
|
-
);
|
|
613
|
-
patchPrismaSchema(targetRoot);
|
|
614
|
-
patchPrismaMigration(packageRoot, targetRoot);
|
|
615
|
-
} else {
|
|
616
|
-
const detected = dbAdapter?.id ? `detected: ${dbAdapter.id}` : 'no DB adapter detected';
|
|
617
|
-
printDbWarning(
|
|
618
|
-
`jwt-auth installed without persistent refresh token store (${detected}). ` +
|
|
619
|
-
'Login/refresh works in stateless mode. Re-run add after supported DB module is installed.',
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
363
|
patchApiPackage(targetRoot);
|
|
624
|
-
patchAppModule(targetRoot
|
|
364
|
+
patchAppModule(targetRoot);
|
|
625
365
|
patchHealthController(targetRoot);
|
|
626
366
|
patchWebApp(targetRoot);
|
|
627
367
|
patchApiDockerfile(targetRoot);
|
|
628
368
|
patchCompose(targetRoot);
|
|
629
|
-
patchReadme(targetRoot
|
|
369
|
+
patchReadme(targetRoot);
|
|
630
370
|
|
|
631
371
|
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
632
372
|
'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
|