create-forgeon 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +49 -172
- package/src/modules/executor.test.mjs +97 -10
- package/src/modules/i18n.mjs +10 -134
- package/src/modules/jwt-auth.mjs +94 -294
- package/src/modules/logger.mjs +10 -118
- package/src/modules/shared/patch-utils.mjs +162 -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_CHECKS.md +3 -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 -225
package/src/modules/i18n.mjs
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
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
|
+
ensureDependency,
|
|
7
|
+
ensureLineAfter,
|
|
8
|
+
ensureLineBefore,
|
|
9
|
+
ensureLoadItem,
|
|
10
|
+
ensureScript,
|
|
11
|
+
ensureValidatorSchema,
|
|
12
|
+
upsertEnvLines,
|
|
13
|
+
} from './shared/patch-utils.mjs';
|
|
4
14
|
|
|
5
15
|
function copyFromBase(packageRoot, targetRoot, relativePath) {
|
|
6
16
|
const source = path.join(packageRoot, 'templates', 'base', relativePath);
|
|
@@ -20,140 +30,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
|
20
30
|
copyRecursive(source, destination);
|
|
21
31
|
}
|
|
22
32
|
|
|
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
33
|
function patchApiDockerfile(targetRoot) {
|
|
158
34
|
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
159
35
|
if (!fs.existsSync(dockerfilePath)) {
|
package/src/modules/jwt-auth.mjs
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureDependency,
|
|
7
|
+
ensureLineAfter,
|
|
8
|
+
ensureLineBefore,
|
|
9
|
+
ensureLoadItem,
|
|
10
|
+
ensureValidatorSchema,
|
|
11
|
+
upsertEnvLines,
|
|
12
|
+
} from './shared/patch-utils.mjs';
|
|
7
13
|
|
|
8
14
|
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
9
15
|
const source = path.join(packageRoot, 'templates', 'module-presets', 'jwt-auth', relativePath);
|
|
@@ -15,177 +21,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
|
15
21
|
copyRecursive(source, destination);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
function ensureDependency(packageJson, name, version) {
|
|
19
|
-
if (!packageJson.dependencies) {
|
|
20
|
-
packageJson.dependencies = {};
|
|
21
|
-
}
|
|
22
|
-
packageJson.dependencies[name] = version;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
|
|
26
|
-
if (!packageJson.scripts) {
|
|
27
|
-
packageJson.scripts = {};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const current = packageJson.scripts[scriptName];
|
|
31
|
-
const steps =
|
|
32
|
-
typeof current === 'string' && current.trim().length > 0
|
|
33
|
-
? current
|
|
34
|
-
.split('&&')
|
|
35
|
-
.map((item) => item.trim())
|
|
36
|
-
.filter(Boolean)
|
|
37
|
-
: [];
|
|
38
|
-
|
|
39
|
-
for (const command of requiredCommands) {
|
|
40
|
-
if (!steps.includes(command)) {
|
|
41
|
-
steps.push(command);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (steps.length > 0) {
|
|
46
|
-
packageJson.scripts[scriptName] = steps.join(' && ');
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
51
|
-
if (content.includes(lineToInsert)) {
|
|
52
|
-
return content;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const index = content.indexOf(anchorLine);
|
|
56
|
-
if (index < 0) {
|
|
57
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const insertAt = index + anchorLine.length;
|
|
61
|
-
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function ensureLineBefore(content, anchorLine, lineToInsert) {
|
|
65
|
-
if (content.includes(lineToInsert)) {
|
|
66
|
-
return content;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const index = content.indexOf(anchorLine);
|
|
70
|
-
if (index < 0) {
|
|
71
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function ensureLoadItem(content, itemName) {
|
|
78
|
-
const pattern = /load:\s*\[([^\]]*)\]/m;
|
|
79
|
-
const match = content.match(pattern);
|
|
80
|
-
if (!match) {
|
|
81
|
-
return content;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const rawList = match[1];
|
|
85
|
-
const items = rawList
|
|
86
|
-
.split(',')
|
|
87
|
-
.map((item) => item.trim())
|
|
88
|
-
.filter(Boolean);
|
|
89
|
-
|
|
90
|
-
if (!items.includes(itemName)) {
|
|
91
|
-
items.push(itemName);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const next = `load: [${items.join(', ')}]`;
|
|
95
|
-
return content.replace(pattern, next);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function ensureValidatorSchema(content, schemaName) {
|
|
99
|
-
const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
|
|
100
|
-
const match = content.match(pattern);
|
|
101
|
-
if (!match) {
|
|
102
|
-
return content;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const rawList = match[1];
|
|
106
|
-
const items = rawList
|
|
107
|
-
.split(',')
|
|
108
|
-
.map((item) => item.trim())
|
|
109
|
-
.filter(Boolean);
|
|
110
|
-
|
|
111
|
-
if (!items.includes(schemaName)) {
|
|
112
|
-
items.push(schemaName);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const next = `validate: createEnvValidator([${items.join(', ')}])`;
|
|
116
|
-
return content.replace(pattern, next);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function upsertEnvLines(filePath, lines) {
|
|
120
|
-
let content = '';
|
|
121
|
-
if (fs.existsSync(filePath)) {
|
|
122
|
-
content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const keys = new Set(
|
|
126
|
-
content
|
|
127
|
-
.split('\n')
|
|
128
|
-
.filter(Boolean)
|
|
129
|
-
.map((line) => line.split('=')[0]),
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const append = [];
|
|
133
|
-
for (const line of lines) {
|
|
134
|
-
const key = line.split('=')[0];
|
|
135
|
-
if (!keys.has(key)) {
|
|
136
|
-
append.push(line);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const next =
|
|
141
|
-
append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
|
|
142
|
-
fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function detectDbAdapter(targetRoot) {
|
|
146
|
-
const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
147
|
-
let deps = {};
|
|
148
|
-
if (fs.existsSync(apiPackagePath)) {
|
|
149
|
-
const packageJson = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
|
|
150
|
-
deps = {
|
|
151
|
-
...(packageJson.dependencies ?? {}),
|
|
152
|
-
...(packageJson.devDependencies ?? {}),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
deps['@forgeon/db-prisma'] ||
|
|
158
|
-
fs.existsSync(path.join(targetRoot, 'packages', 'db-prisma', 'package.json'))
|
|
159
|
-
) {
|
|
160
|
-
return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const dbDeps = Object.keys(deps).filter((name) => name.startsWith('@forgeon/db-'));
|
|
164
|
-
if (dbDeps.length > 0) {
|
|
165
|
-
return { id: dbDeps[0], supported: false, tokenStore: 'none' };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const packagesPath = path.join(targetRoot, 'packages');
|
|
169
|
-
if (fs.existsSync(packagesPath)) {
|
|
170
|
-
const localDbPackages = fs
|
|
171
|
-
.readdirSync(packagesPath, { withFileTypes: true })
|
|
172
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith('db-'))
|
|
173
|
-
.map((entry) => entry.name);
|
|
174
|
-
if (localDbPackages.includes('db-prisma')) {
|
|
175
|
-
return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
|
|
176
|
-
}
|
|
177
|
-
if (localDbPackages.length > 0) {
|
|
178
|
-
return { id: `@forgeon/${localDbPackages[0]}`, supported: false, tokenStore: 'none' };
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function printDbWarning(message) {
|
|
186
|
-
console.error(`\x1b[31m[create-forgeon add jwt-auth] ${message}\x1b[0m`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
24
|
function patchApiPackage(targetRoot) {
|
|
190
25
|
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
191
26
|
if (!fs.existsSync(packagePath)) {
|
|
@@ -204,21 +39,19 @@ function patchApiPackage(targetRoot) {
|
|
|
204
39
|
writeJson(packagePath, packageJson);
|
|
205
40
|
}
|
|
206
41
|
|
|
207
|
-
function patchAppModule(targetRoot
|
|
42
|
+
function patchAppModule(targetRoot) {
|
|
208
43
|
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
209
44
|
if (!fs.existsSync(filePath)) {
|
|
210
45
|
return;
|
|
211
46
|
}
|
|
212
47
|
|
|
213
|
-
const withPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
|
|
214
|
-
|
|
215
48
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
216
49
|
if (!content.includes("from '@forgeon/auth-api';")) {
|
|
217
50
|
if (content.includes("import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';")) {
|
|
218
51
|
content = ensureLineAfter(
|
|
219
52
|
content,
|
|
220
53
|
"import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
|
|
221
|
-
"import {
|
|
54
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
222
55
|
);
|
|
223
56
|
} else if (
|
|
224
57
|
content.includes("import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';")
|
|
@@ -226,7 +59,7 @@ function patchAppModule(targetRoot, dbAdapter) {
|
|
|
226
59
|
content = ensureLineAfter(
|
|
227
60
|
content,
|
|
228
61
|
"import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
|
|
229
|
-
"import {
|
|
62
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
230
63
|
);
|
|
231
64
|
} else if (
|
|
232
65
|
content.includes("import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';")
|
|
@@ -234,7 +67,7 @@ function patchAppModule(targetRoot, dbAdapter) {
|
|
|
234
67
|
content = ensureLineAfter(
|
|
235
68
|
content,
|
|
236
69
|
"import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
|
|
237
|
-
"import {
|
|
70
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
238
71
|
);
|
|
239
72
|
} else if (
|
|
240
73
|
content.includes("import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';")
|
|
@@ -242,38 +75,22 @@ function patchAppModule(targetRoot, dbAdapter) {
|
|
|
242
75
|
content = ensureLineAfter(
|
|
243
76
|
content,
|
|
244
77
|
"import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
|
|
245
|
-
"import {
|
|
78
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
246
79
|
);
|
|
247
80
|
} else {
|
|
248
81
|
content = ensureLineAfter(
|
|
249
82
|
content,
|
|
250
83
|
"import { ConfigModule } from '@nestjs/config';",
|
|
251
|
-
"import {
|
|
84
|
+
"import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
252
85
|
);
|
|
253
86
|
}
|
|
254
87
|
}
|
|
255
88
|
|
|
256
|
-
if (withPrismaStore && !content.includes("./auth/prisma-auth-refresh-token.store")) {
|
|
257
|
-
content = ensureLineBefore(
|
|
258
|
-
content,
|
|
259
|
-
"import { HealthController } from './health/health.controller';",
|
|
260
|
-
"import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';",
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
89
|
content = ensureLoadItem(content, 'authConfig');
|
|
265
90
|
content = ensureValidatorSchema(content, 'authEnvSchema');
|
|
266
91
|
|
|
267
92
|
if (!content.includes('ForgeonAuthModule.register(')) {
|
|
268
|
-
const moduleBlock =
|
|
269
|
-
? ` ForgeonAuthModule.register({
|
|
270
|
-
imports: [DbPrismaModule],
|
|
271
|
-
refreshTokenStoreProvider: {
|
|
272
|
-
provide: AUTH_REFRESH_TOKEN_STORE,
|
|
273
|
-
useClass: PrismaAuthRefreshTokenStore,
|
|
274
|
-
},
|
|
275
|
-
}),`
|
|
276
|
-
: ` ForgeonAuthModule.register(),`;
|
|
93
|
+
const moduleBlock = ' ForgeonAuthModule.register(),';
|
|
277
94
|
|
|
278
95
|
if (content.includes(' ForgeonI18nModule.register({')) {
|
|
279
96
|
content = ensureLineBefore(content, ' ForgeonI18nModule.register({', moduleBlock);
|
|
@@ -300,6 +117,7 @@ function patchHealthController(targetRoot) {
|
|
|
300
117
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
301
118
|
|
|
302
119
|
if (!content.includes("from '@forgeon/auth-api';")) {
|
|
120
|
+
const nestCommonImport = content.match(/import\s*\{[^}]*\}\s*from '@nestjs\/common';/m)?.[0];
|
|
303
121
|
if (content.includes("import { PrismaService } from '@forgeon/db-prisma';")) {
|
|
304
122
|
content = ensureLineAfter(
|
|
305
123
|
content,
|
|
@@ -309,7 +127,7 @@ function patchHealthController(targetRoot) {
|
|
|
309
127
|
} else {
|
|
310
128
|
content = ensureLineAfter(
|
|
311
129
|
content,
|
|
312
|
-
"import {
|
|
130
|
+
nestCommonImport ?? "import { Controller, Get } from '@nestjs/common';",
|
|
313
131
|
"import { AuthService } from '@forgeon/auth-api';",
|
|
314
132
|
);
|
|
315
133
|
}
|
|
@@ -326,6 +144,16 @@ function patchHealthController(targetRoot) {
|
|
|
326
144
|
private readonly authService: AuthService,
|
|
327
145
|
) {`;
|
|
328
146
|
content = content.replace(original, next);
|
|
147
|
+
} else {
|
|
148
|
+
const classAnchor = 'export class HealthController {';
|
|
149
|
+
if (content.includes(classAnchor)) {
|
|
150
|
+
content = content.replace(
|
|
151
|
+
classAnchor,
|
|
152
|
+
`${classAnchor}
|
|
153
|
+
constructor(private readonly authService: AuthService) {}
|
|
154
|
+
`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
329
157
|
}
|
|
330
158
|
}
|
|
331
159
|
|
|
@@ -342,7 +170,12 @@ function patchHealthController(targetRoot) {
|
|
|
342
170
|
const index = content.indexOf('private translate(');
|
|
343
171
|
content = `${content.slice(0, index).trimEnd()}\n\n${method}\n${content.slice(index)}`;
|
|
344
172
|
} else {
|
|
345
|
-
|
|
173
|
+
const classEnd = content.lastIndexOf('\n}');
|
|
174
|
+
if (classEnd >= 0) {
|
|
175
|
+
content = `${content.slice(0, classEnd).trimEnd()}\n\n${method}\n${content.slice(classEnd)}`;
|
|
176
|
+
} else {
|
|
177
|
+
content = `${content.trimEnd()}\n${method}\n`;
|
|
178
|
+
}
|
|
346
179
|
}
|
|
347
180
|
}
|
|
348
181
|
|
|
@@ -356,30 +189,58 @@ function patchWebApp(targetRoot) {
|
|
|
356
189
|
}
|
|
357
190
|
|
|
358
191
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
192
|
+
content = content
|
|
193
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
|
|
194
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
|
|
195
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
|
|
196
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
|
|
197
|
+
|
|
359
198
|
if (!content.includes('authProbeResult')) {
|
|
360
|
-
content =
|
|
361
|
-
|
|
362
|
-
|
|
199
|
+
if (content.includes(' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);')) {
|
|
200
|
+
content = content.replace(
|
|
201
|
+
' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
|
|
202
|
+
` const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
|
|
363
203
|
const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);`,
|
|
364
|
-
|
|
204
|
+
);
|
|
205
|
+
} else if (content.includes(' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);')) {
|
|
206
|
+
content = content.replace(
|
|
207
|
+
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
208
|
+
` const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
|
|
209
|
+
const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
365
212
|
}
|
|
366
213
|
|
|
367
214
|
if (!content.includes('Check JWT auth probe')) {
|
|
368
215
|
const path = content.includes("runProbe(setHealthResult, '/health')") ? '/health/auth' : '/api/health/auth';
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
216
|
+
const authButton = ` <button onClick={() => runProbe(setAuthProbeResult, '${path}')}>Check JWT auth probe</button>`;
|
|
217
|
+
const actionsStart = content.indexOf('<div className="actions">');
|
|
218
|
+
if (actionsStart >= 0) {
|
|
219
|
+
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
220
|
+
if (actionsEnd >= 0) {
|
|
221
|
+
content = `${content.slice(0, actionsEnd)}\n${authButton}${content.slice(actionsEnd)}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
375
224
|
}
|
|
376
225
|
|
|
377
226
|
if (!content.includes("renderResult('Auth probe response', authProbeResult)")) {
|
|
378
|
-
|
|
379
|
-
"{
|
|
380
|
-
|
|
227
|
+
const authResultLine = " {renderResult('Auth probe response', authProbeResult)}";
|
|
228
|
+
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
229
|
+
if (content.includes(networkLine)) {
|
|
230
|
+
content = content.replace(networkLine, `${authResultLine}\n${networkLine}`);
|
|
231
|
+
} else if (content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
|
|
232
|
+
content = content.replace(
|
|
233
|
+
"{renderResult('DB probe response', dbProbeResult)}",
|
|
234
|
+
`{renderResult('DB probe response', dbProbeResult)}
|
|
381
235
|
{renderResult('Auth probe response', authProbeResult)}`,
|
|
382
|
-
|
|
236
|
+
);
|
|
237
|
+
} else if (content.includes("{renderResult('Validation probe response', validationProbeResult)}")) {
|
|
238
|
+
content = content.replace(
|
|
239
|
+
"{renderResult('Validation probe response', validationProbeResult)}",
|
|
240
|
+
`{renderResult('Validation probe response', validationProbeResult)}
|
|
241
|
+
{renderResult('Auth probe response', authProbeResult)}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
383
244
|
}
|
|
384
245
|
|
|
385
246
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -463,25 +324,19 @@ function patchCompose(targetRoot) {
|
|
|
463
324
|
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
464
325
|
}
|
|
465
326
|
|
|
466
|
-
function patchReadme(targetRoot
|
|
327
|
+
function patchReadme(targetRoot) {
|
|
467
328
|
const readmePath = path.join(targetRoot, 'README.md');
|
|
468
329
|
if (!fs.existsSync(readmePath)) {
|
|
469
330
|
return;
|
|
470
331
|
}
|
|
471
332
|
|
|
472
333
|
const persistenceSummary =
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
: '- refresh token persistence: disabled (no supported DB adapter found)';
|
|
476
|
-
const dbFollowUp =
|
|
477
|
-
dbAdapter?.supported && dbAdapter.id === 'db-prisma'
|
|
478
|
-
? '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`'
|
|
479
|
-
: `- to enable persistence later:
|
|
334
|
+
'- refresh token persistence: disabled by default (stateless mode)';
|
|
335
|
+
const dbFollowUp = `- to enable persistence later:
|
|
480
336
|
1. install a DB module first (for now: \`create-forgeon add db-prisma --project .\`);
|
|
481
|
-
2. run \`
|
|
337
|
+
2. run \`pnpm forgeon:sync-integrations\` to auto-wire pair integrations.`;
|
|
482
338
|
|
|
483
|
-
const section =
|
|
484
|
-
## JWT Auth Module
|
|
339
|
+
const section = `## JWT Auth Module
|
|
485
340
|
|
|
486
341
|
The jwt-auth add-module provides:
|
|
487
342
|
- \`@forgeon/auth-contracts\` shared auth routes/types/error codes
|
|
@@ -501,13 +356,19 @@ Default routes:
|
|
|
501
356
|
- \`POST /api/auth/login\`
|
|
502
357
|
- \`POST /api/auth/refresh\`
|
|
503
358
|
- \`POST /api/auth/logout\`
|
|
504
|
-
- \`GET /api/auth/me
|
|
505
|
-
${JWT_README_END}`;
|
|
359
|
+
- \`GET /api/auth/me\``;
|
|
506
360
|
|
|
507
361
|
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
|
|
362
|
+
const sectionHeading = '## JWT Auth Module';
|
|
363
|
+
if (content.includes(sectionHeading)) {
|
|
364
|
+
const start = content.indexOf(sectionHeading);
|
|
365
|
+
const tail = content.slice(start + sectionHeading.length);
|
|
366
|
+
const nextHeadingMatch = tail.match(/\n##\s+/);
|
|
367
|
+
const end =
|
|
368
|
+
nextHeadingMatch && nextHeadingMatch.index !== undefined
|
|
369
|
+
? start + sectionHeading.length + nextHeadingMatch.index + 1
|
|
370
|
+
: content.length;
|
|
371
|
+
content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
|
|
511
372
|
} else if (content.includes('## Prisma In Docker Start')) {
|
|
512
373
|
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
513
374
|
} else {
|
|
@@ -517,78 +378,17 @@ ${JWT_README_END}`;
|
|
|
517
378
|
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
518
379
|
}
|
|
519
380
|
|
|
520
|
-
function patchPrismaSchema(targetRoot) {
|
|
521
|
-
const schemaPath = path.join(targetRoot, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
522
|
-
if (!fs.existsSync(schemaPath)) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
let content = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
|
|
527
|
-
if (!content.includes('refreshTokenHash')) {
|
|
528
|
-
content = content.replace(
|
|
529
|
-
/email\s+String\s+@unique/g,
|
|
530
|
-
'email String @unique\n refreshTokenHash String?',
|
|
531
|
-
);
|
|
532
|
-
fs.writeFileSync(schemaPath, `${content.trimEnd()}\n`, 'utf8');
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function patchPrismaMigration(packageRoot, targetRoot) {
|
|
537
|
-
const migrationSource = path.join(
|
|
538
|
-
packageRoot,
|
|
539
|
-
'templates',
|
|
540
|
-
'module-presets',
|
|
541
|
-
'jwt-auth',
|
|
542
|
-
'apps',
|
|
543
|
-
'api',
|
|
544
|
-
'prisma',
|
|
545
|
-
'migrations',
|
|
546
|
-
'0002_auth_refresh_token_hash',
|
|
547
|
-
);
|
|
548
|
-
const migrationTarget = path.join(
|
|
549
|
-
targetRoot,
|
|
550
|
-
'apps',
|
|
551
|
-
'api',
|
|
552
|
-
'prisma',
|
|
553
|
-
'migrations',
|
|
554
|
-
'0002_auth_refresh_token_hash',
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
if (!fs.existsSync(migrationTarget) && fs.existsSync(migrationSource)) {
|
|
558
|
-
copyRecursive(migrationSource, migrationTarget);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
381
|
export function applyJwtAuthModule({ packageRoot, targetRoot }) {
|
|
563
|
-
const dbAdapter = detectDbAdapter(targetRoot);
|
|
564
|
-
const supportsPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
|
|
565
|
-
|
|
566
382
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-contracts'));
|
|
567
383
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-api'));
|
|
568
384
|
|
|
569
|
-
if (supportsPrismaStore) {
|
|
570
|
-
copyFromPreset(
|
|
571
|
-
packageRoot,
|
|
572
|
-
targetRoot,
|
|
573
|
-
path.join('apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts'),
|
|
574
|
-
);
|
|
575
|
-
patchPrismaSchema(targetRoot);
|
|
576
|
-
patchPrismaMigration(packageRoot, targetRoot);
|
|
577
|
-
} else {
|
|
578
|
-
const detected = dbAdapter?.id ? `detected: ${dbAdapter.id}` : 'no DB adapter detected';
|
|
579
|
-
printDbWarning(
|
|
580
|
-
`jwt-auth installed without persistent refresh token store (${detected}). ` +
|
|
581
|
-
'Login/refresh works in stateless mode. Re-run add after supported DB module is installed.',
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
385
|
patchApiPackage(targetRoot);
|
|
586
|
-
patchAppModule(targetRoot
|
|
386
|
+
patchAppModule(targetRoot);
|
|
587
387
|
patchHealthController(targetRoot);
|
|
588
388
|
patchWebApp(targetRoot);
|
|
589
389
|
patchApiDockerfile(targetRoot);
|
|
590
390
|
patchCompose(targetRoot);
|
|
591
|
-
patchReadme(targetRoot
|
|
391
|
+
patchReadme(targetRoot);
|
|
592
392
|
|
|
593
393
|
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
594
394
|
'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
|