create-forgeon 0.3.4 → 0.3.6
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/cli/add-help.mjs +1 -0
- package/src/cli/add-options.mjs +6 -0
- package/src/cli/add-options.test.mjs +2 -0
- package/src/modules/dependencies.mjs +31 -0
- package/src/modules/dependencies.test.mjs +207 -5
- package/src/modules/executor.mjs +14 -0
- package/src/modules/executor.test.mjs +729 -18
- package/src/modules/files-access.mjs +437 -0
- package/src/modules/files-image.mjs +531 -0
- package/src/modules/files-local.mjs +221 -0
- package/src/modules/files-quotas.mjs +381 -0
- package/src/modules/files-s3.mjs +266 -0
- package/src/modules/files.mjs +527 -0
- package/src/modules/logger.mjs +12 -3
- package/src/modules/queue.mjs +410 -0
- package/src/modules/registry.mjs +93 -3
- package/src/run-add-module.mjs +89 -2
- package/templates/module-fragments/files/00_title.md +1 -0
- package/templates/module-fragments/files/10_overview.md +17 -0
- package/templates/module-fragments/files/20_scope.md +13 -0
- package/templates/module-fragments/files/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-access/00_title.md +1 -0
- package/templates/module-fragments/files-access/10_overview.md +9 -0
- package/templates/module-fragments/files-access/20_scope.md +20 -0
- package/templates/module-fragments/files-access/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-image/00_title.md +1 -0
- package/templates/module-fragments/files-image/10_overview.md +10 -0
- package/templates/module-fragments/files-image/20_scope.md +20 -0
- package/templates/module-fragments/files-image/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-local/00_title.md +1 -0
- package/templates/module-fragments/files-local/10_overview.md +9 -0
- package/templates/module-fragments/files-local/20_scope.md +10 -0
- package/templates/module-fragments/files-local/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-quotas/00_title.md +1 -0
- package/templates/module-fragments/files-quotas/10_overview.md +9 -0
- package/templates/module-fragments/files-quotas/20_scope.md +20 -0
- package/templates/module-fragments/files-quotas/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-s3/00_title.md +1 -0
- package/templates/module-fragments/files-s3/10_overview.md +17 -0
- package/templates/module-fragments/files-s3/20_scope.md +11 -0
- package/templates/module-fragments/files-s3/90_status_implemented.md +5 -0
- package/templates/module-fragments/queue/20_scope.md +8 -7
- package/templates/module-fragments/queue/90_status_implemented.md +3 -0
- package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_record/migration.sql +30 -0
- package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_variant/migration.sql +55 -0
- package/templates/module-presets/files/packages/files/package.json +24 -0
- package/templates/module-presets/files/packages/files/src/dto/create-file.dto.ts +30 -0
- package/templates/module-presets/files/packages/files/src/files-config.loader.ts +21 -0
- package/templates/module-presets/files/packages/files/src/files-config.module.ts +12 -0
- package/templates/module-presets/files/packages/files/src/files-config.service.ts +32 -0
- package/templates/module-presets/files/packages/files/src/files-env.schema.ts +30 -0
- package/templates/module-presets/files/packages/files/src/files.controller.ts +89 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +744 -0
- package/templates/module-presets/files/packages/files/src/files.types.ts +35 -0
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +12 -0
- package/templates/module-presets/files/packages/files/src/index.ts +9 -0
- package/templates/module-presets/files/packages/files/tsconfig.json +9 -0
- package/templates/module-presets/files-access/packages/files-access/package.json +17 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.service.ts +59 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.subject.ts +45 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.types.ts +14 -0
- package/templates/module-presets/files-access/packages/files-access/src/forgeon-files-access.module.ts +8 -0
- package/templates/module-presets/files-access/packages/files-access/src/index.ts +4 -0
- package/templates/module-presets/files-access/packages/files-access/tsconfig.json +9 -0
- package/templates/module-presets/files-image/packages/files-image/package.json +21 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.loader.ts +32 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.module.ts +11 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.service.ts +55 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-env.schema.ts +28 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image.service.ts +420 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image.types.ts +18 -0
- package/templates/module-presets/files-image/packages/files-image/src/forgeon-files-image.module.ts +10 -0
- package/templates/module-presets/files-image/packages/files-image/src/index.ts +7 -0
- package/templates/module-presets/files-image/packages/files-image/tsconfig.json +9 -0
- package/templates/module-presets/files-local/packages/files-local/package.json +19 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.loader.ts +13 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.module.ts +12 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.service.ts +11 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-env.schema.ts +13 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +4 -0
- package/templates/module-presets/files-local/packages/files-local/tsconfig.json +9 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.loader.ts +22 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.module.ts +11 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.service.ts +27 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-env.schema.ts +15 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.types.ts +22 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +11 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/index.ts +7 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/tsconfig.json +9 -0
- package/templates/module-presets/files-s3/packages/files-s3/package.json +20 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.loader.ts +57 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.module.ts +12 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.service.ts +44 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-env.schema.ts +51 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +4 -0
- package/templates/module-presets/files-s3/packages/files-s3/tsconfig.json +9 -0
- package/templates/module-presets/logger/packages/logger/src/forgeon-logger.module.ts +4 -5
- package/templates/module-presets/logger/packages/logger/src/http-logging.middleware.ts +74 -0
- package/templates/module-presets/logger/packages/logger/src/index.ts +1 -1
- package/templates/module-presets/queue/packages/queue/package.json +21 -0
- package/templates/module-presets/queue/packages/queue/src/forgeon-queue.module.ts +10 -0
- package/templates/module-presets/queue/packages/queue/src/index.ts +6 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.loader.ts +24 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.module.ts +10 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.service.ts +69 -0
- package/templates/module-presets/queue/packages/queue/src/queue-env.schema.ts +17 -0
- package/templates/module-presets/queue/packages/queue/src/queue.service.ts +88 -0
- package/templates/module-presets/queue/packages/queue/tsconfig.json +9 -0
- package/templates/module-fragments/queue/90_status_planned.md +0 -3
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureClassMember,
|
|
7
|
+
ensureDependency,
|
|
8
|
+
ensureImportLine,
|
|
9
|
+
ensureLineAfter,
|
|
10
|
+
ensureLineBefore,
|
|
11
|
+
ensureNestCommonImport,
|
|
12
|
+
} from './shared/patch-utils.mjs';
|
|
13
|
+
|
|
14
|
+
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
15
|
+
const source = path.join(packageRoot, 'templates', 'module-presets', 'files-access', relativePath);
|
|
16
|
+
if (!fs.existsSync(source)) {
|
|
17
|
+
throw new Error(`Missing files-access preset template: ${source}`);
|
|
18
|
+
}
|
|
19
|
+
const destination = path.join(targetRoot, relativePath);
|
|
20
|
+
copyRecursive(source, destination);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function patchApiPackage(targetRoot) {
|
|
24
|
+
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
25
|
+
if (!fs.existsSync(packagePath)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
30
|
+
ensureDependency(packageJson, '@forgeon/files-access', 'workspace:*');
|
|
31
|
+
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-access build']);
|
|
32
|
+
writeJson(packagePath, packageJson);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function patchFilesPackage(targetRoot) {
|
|
36
|
+
const packagePath = path.join(targetRoot, 'packages', 'files', 'package.json');
|
|
37
|
+
if (!fs.existsSync(packagePath)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
42
|
+
ensureDependency(packageJson, '@forgeon/files-access', 'workspace:*');
|
|
43
|
+
writeJson(packagePath, packageJson);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function patchAppModule(targetRoot) {
|
|
47
|
+
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
48
|
+
if (!fs.existsSync(filePath)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
53
|
+
content = ensureImportLine(content, "import { ForgeonFilesAccessModule } from '@forgeon/files-access';");
|
|
54
|
+
|
|
55
|
+
if (!content.includes(' ForgeonFilesAccessModule,')) {
|
|
56
|
+
if (content.includes(' ForgeonFilesModule,')) {
|
|
57
|
+
content = ensureLineAfter(content, ' ForgeonFilesModule,', ' ForgeonFilesAccessModule,');
|
|
58
|
+
} else if (content.includes(' ForgeonI18nModule.register({')) {
|
|
59
|
+
content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonFilesAccessModule,');
|
|
60
|
+
} else if (content.includes(' ForgeonAuthModule.register({')) {
|
|
61
|
+
content = ensureLineBefore(content, ' ForgeonAuthModule.register({', ' ForgeonFilesAccessModule,');
|
|
62
|
+
} else if (content.includes(' ForgeonAuthModule.register(),')) {
|
|
63
|
+
content = ensureLineBefore(content, ' ForgeonAuthModule.register(),', ' ForgeonFilesAccessModule,');
|
|
64
|
+
} else if (content.includes(' DbPrismaModule,')) {
|
|
65
|
+
content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonFilesAccessModule,');
|
|
66
|
+
} else if (content.includes(' ForgeonLoggerModule,')) {
|
|
67
|
+
content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' ForgeonFilesAccessModule,');
|
|
68
|
+
} else if (content.includes(' ForgeonSwaggerModule,')) {
|
|
69
|
+
content = ensureLineAfter(content, ' ForgeonSwaggerModule,', ' ForgeonFilesAccessModule,');
|
|
70
|
+
} else {
|
|
71
|
+
content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonFilesAccessModule,');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function patchFilesController(targetRoot) {
|
|
79
|
+
const filePath = path.join(targetRoot, 'packages', 'files', 'src', 'files.controller.ts');
|
|
80
|
+
if (!fs.existsSync(filePath)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
85
|
+
content = ensureNestCommonImport(content, 'Req');
|
|
86
|
+
content = ensureImportLine(
|
|
87
|
+
content,
|
|
88
|
+
"import { extractFilesAccessSubject, FilesAccessService } from '@forgeon/files-access';",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!content.includes('private readonly filesAccessService: FilesAccessService')) {
|
|
92
|
+
const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
|
|
93
|
+
if (constructorMatch) {
|
|
94
|
+
const original = constructorMatch[0];
|
|
95
|
+
const inner = constructorMatch[1].trimEnd();
|
|
96
|
+
const normalizedInner = inner.replace(/,\s*$/, '');
|
|
97
|
+
const separator = normalizedInner.length > 0 ? ',' : '';
|
|
98
|
+
const next = `constructor(${normalizedInner}${separator}
|
|
99
|
+
private readonly filesAccessService: FilesAccessService,
|
|
100
|
+
) {`;
|
|
101
|
+
content = content.replace(original, next);
|
|
102
|
+
} else {
|
|
103
|
+
const classAnchor = 'export class FilesController {';
|
|
104
|
+
if (content.includes(classAnchor)) {
|
|
105
|
+
content = content.replace(
|
|
106
|
+
classAnchor,
|
|
107
|
+
`${classAnchor}
|
|
108
|
+
constructor(
|
|
109
|
+
private readonly filesService: FilesService,
|
|
110
|
+
private readonly filesAccessService: FilesAccessService,
|
|
111
|
+
) {}
|
|
112
|
+
`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!content.includes('filesAccessService.assertCanRead')) {
|
|
119
|
+
content = content.replace(
|
|
120
|
+
` async getMetadata(@Param('publicId') publicId: string) {
|
|
121
|
+
return this.filesService.getByPublicId(publicId);
|
|
122
|
+
}`,
|
|
123
|
+
` async getMetadata(@Param('publicId') publicId: string, @Req() req: any) {
|
|
124
|
+
const file = await this.filesService.getByPublicId(publicId);
|
|
125
|
+
this.filesAccessService.assertCanRead(file, extractFilesAccessSubject(req));
|
|
126
|
+
return file;
|
|
127
|
+
}`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
content = content.replace(
|
|
131
|
+
` async download(@Param('publicId') publicId: string, @Query('variant') variantQuery?: string) {
|
|
132
|
+
const variant = this.parseVariant(variantQuery);
|
|
133
|
+
const payload = await this.filesService.openDownload(publicId, variant);
|
|
134
|
+
return new StreamableFile(payload.stream, {
|
|
135
|
+
disposition: \`inline; filename="\${payload.fileName}"\`,
|
|
136
|
+
type: payload.mimeType,
|
|
137
|
+
});
|
|
138
|
+
}`,
|
|
139
|
+
` async download(
|
|
140
|
+
@Param('publicId') publicId: string,
|
|
141
|
+
@Query('variant') variantQuery?: string,
|
|
142
|
+
@Req() req: any,
|
|
143
|
+
) {
|
|
144
|
+
const variant = this.parseVariant(variantQuery);
|
|
145
|
+
const file = await this.filesService.getByPublicId(publicId);
|
|
146
|
+
this.filesAccessService.assertCanRead(file, extractFilesAccessSubject(req));
|
|
147
|
+
const payload = await this.filesService.openDownload(publicId, variant);
|
|
148
|
+
return new StreamableFile(payload.stream, {
|
|
149
|
+
disposition: \`inline; filename="\${payload.fileName}"\`,
|
|
150
|
+
type: payload.mimeType,
|
|
151
|
+
});
|
|
152
|
+
}`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
content = content.replace(
|
|
156
|
+
` async download(@Param('publicId') publicId: string) {
|
|
157
|
+
const payload = await this.filesService.openDownload(publicId);
|
|
158
|
+
return new StreamableFile(payload.stream, {
|
|
159
|
+
disposition: \`inline; filename="\${payload.fileName}"\`,
|
|
160
|
+
type: payload.mimeType,
|
|
161
|
+
});
|
|
162
|
+
}`,
|
|
163
|
+
` async download(@Param('publicId') publicId: string, @Req() req: any) {
|
|
164
|
+
const file = await this.filesService.getByPublicId(publicId);
|
|
165
|
+
this.filesAccessService.assertCanRead(file, extractFilesAccessSubject(req));
|
|
166
|
+
const payload = await this.filesService.openDownload(publicId);
|
|
167
|
+
return new StreamableFile(payload.stream, {
|
|
168
|
+
disposition: \`inline; filename="\${payload.fileName}"\`,
|
|
169
|
+
type: payload.mimeType,
|
|
170
|
+
});
|
|
171
|
+
}`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
content = content.replace(
|
|
175
|
+
` async remove(@Param('publicId') publicId: string) {
|
|
176
|
+
return this.filesService.deleteByPublicId(publicId);
|
|
177
|
+
}`,
|
|
178
|
+
` async remove(@Param('publicId') publicId: string, @Req() req: any) {
|
|
179
|
+
const file = await this.filesService.getByPublicId(publicId);
|
|
180
|
+
this.filesAccessService.assertCanDelete(file, extractFilesAccessSubject(req));
|
|
181
|
+
return this.filesService.deleteByPublicId(publicId);
|
|
182
|
+
}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function patchHealthController(targetRoot) {
|
|
190
|
+
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
|
|
191
|
+
if (!fs.existsSync(filePath)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
196
|
+
content = ensureNestCommonImport(content, 'Req');
|
|
197
|
+
content = ensureImportLine(
|
|
198
|
+
content,
|
|
199
|
+
"import { extractFilesAccessSubject, FilesAccessService } from '@forgeon/files-access';",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (!content.includes('private readonly filesAccessService: FilesAccessService')) {
|
|
203
|
+
const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
|
|
204
|
+
if (constructorMatch) {
|
|
205
|
+
const original = constructorMatch[0];
|
|
206
|
+
const inner = constructorMatch[1].trimEnd();
|
|
207
|
+
const normalizedInner = inner.replace(/,\s*$/, '');
|
|
208
|
+
const separator = normalizedInner.length > 0 ? ',' : '';
|
|
209
|
+
const next = `constructor(${normalizedInner}${separator}
|
|
210
|
+
private readonly filesAccessService: FilesAccessService,
|
|
211
|
+
) {`;
|
|
212
|
+
content = content.replace(original, next);
|
|
213
|
+
} else {
|
|
214
|
+
const classAnchor = 'export class HealthController {';
|
|
215
|
+
if (content.includes(classAnchor)) {
|
|
216
|
+
content = content.replace(
|
|
217
|
+
classAnchor,
|
|
218
|
+
`${classAnchor}
|
|
219
|
+
constructor(private readonly filesAccessService: FilesAccessService) {}
|
|
220
|
+
`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!content.includes("@Get('files-access')")) {
|
|
227
|
+
const method = `
|
|
228
|
+
@Get('files-access')
|
|
229
|
+
getFilesAccessProbe(@Req() req: any) {
|
|
230
|
+
const subject = extractFilesAccessSubject(req);
|
|
231
|
+
const sample = {
|
|
232
|
+
ownerType: 'user',
|
|
233
|
+
ownerId: 'probe-owner',
|
|
234
|
+
visibility: 'private',
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
status: 'ok',
|
|
239
|
+
feature: 'files-access',
|
|
240
|
+
canRead: this.filesAccessService.canRead(sample, subject),
|
|
241
|
+
canDelete: this.filesAccessService.canDelete(sample, subject),
|
|
242
|
+
hint: 'Send x-forgeon-user-id: probe-owner or x-forgeon-permissions: files.manage',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
content = ensureClassMember(content, 'HealthController', method, { beforeNeedle: 'private translate(' });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function patchWebApp(targetRoot) {
|
|
253
|
+
const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
|
|
254
|
+
if (!fs.existsSync(filePath)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
259
|
+
content = content
|
|
260
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
|
|
261
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
|
|
262
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
|
|
263
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
|
|
264
|
+
|
|
265
|
+
if (!content.includes('filesAccessProbeResult')) {
|
|
266
|
+
const stateAnchors = [
|
|
267
|
+
' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
|
|
268
|
+
' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
|
|
269
|
+
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
270
|
+
' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
|
|
271
|
+
' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
|
|
272
|
+
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
273
|
+
];
|
|
274
|
+
const stateAnchor = stateAnchors.find((line) => content.includes(line));
|
|
275
|
+
if (stateAnchor) {
|
|
276
|
+
content = ensureLineAfter(
|
|
277
|
+
content,
|
|
278
|
+
stateAnchor,
|
|
279
|
+
' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!content.includes('Check files access')) {
|
|
285
|
+
const probePath = content.includes("runProbe(setHealthResult, '/health')")
|
|
286
|
+
? '/health/files-access'
|
|
287
|
+
: '/api/health/files-access';
|
|
288
|
+
const button = ` <button
|
|
289
|
+
onClick={() =>
|
|
290
|
+
runProbe(setFilesAccessProbeResult, '${probePath}', {
|
|
291
|
+
headers: { 'x-forgeon-user-id': 'probe-owner' },
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
Check files access
|
|
296
|
+
</button>`;
|
|
297
|
+
|
|
298
|
+
const actionsStart = content.indexOf('<div className="actions">');
|
|
299
|
+
if (actionsStart >= 0) {
|
|
300
|
+
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
301
|
+
if (actionsEnd >= 0) {
|
|
302
|
+
content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!content.includes("{renderResult('Files access probe response', filesAccessProbeResult)}")) {
|
|
308
|
+
const resultLine = " {renderResult('Files access probe response', filesAccessProbeResult)}";
|
|
309
|
+
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
310
|
+
if (content.includes(networkLine)) {
|
|
311
|
+
content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
|
|
312
|
+
} else {
|
|
313
|
+
const anchors = [
|
|
314
|
+
" {renderResult('Files probe response', filesProbeResult)}",
|
|
315
|
+
" {renderResult('RBAC probe response', rbacProbeResult)}",
|
|
316
|
+
" {renderResult('Rate limit probe response', rateLimitProbeResult)}",
|
|
317
|
+
" {renderResult('Auth probe response', authProbeResult)}",
|
|
318
|
+
" {renderResult('DB probe response', dbProbeResult)}",
|
|
319
|
+
" {renderResult('Validation probe response', validationProbeResult)}",
|
|
320
|
+
];
|
|
321
|
+
const anchor = anchors.find((line) => content.includes(line));
|
|
322
|
+
if (anchor) {
|
|
323
|
+
content = ensureLineAfter(content, anchor, resultLine);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function patchApiDockerfile(targetRoot) {
|
|
332
|
+
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
333
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
338
|
+
|
|
339
|
+
const packageAnchors = [
|
|
340
|
+
'COPY packages/files/package.json packages/files/package.json',
|
|
341
|
+
'COPY packages/files-local/package.json packages/files-local/package.json',
|
|
342
|
+
'COPY packages/files-s3/package.json packages/files-s3/package.json',
|
|
343
|
+
'COPY packages/auth-api/package.json packages/auth-api/package.json',
|
|
344
|
+
'COPY packages/rbac/package.json packages/rbac/package.json',
|
|
345
|
+
'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
|
|
346
|
+
'COPY packages/logger/package.json packages/logger/package.json',
|
|
347
|
+
'COPY packages/swagger/package.json packages/swagger/package.json',
|
|
348
|
+
'COPY packages/i18n/package.json packages/i18n/package.json',
|
|
349
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
350
|
+
'COPY packages/core/package.json packages/core/package.json',
|
|
351
|
+
];
|
|
352
|
+
const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
|
|
353
|
+
content = ensureLineAfter(
|
|
354
|
+
content,
|
|
355
|
+
packageAnchor,
|
|
356
|
+
'COPY packages/files-access/package.json packages/files-access/package.json',
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const sourceAnchors = [
|
|
360
|
+
'COPY packages/files packages/files',
|
|
361
|
+
'COPY packages/files-local packages/files-local',
|
|
362
|
+
'COPY packages/files-s3 packages/files-s3',
|
|
363
|
+
'COPY packages/auth-api packages/auth-api',
|
|
364
|
+
'COPY packages/rbac packages/rbac',
|
|
365
|
+
'COPY packages/rate-limit packages/rate-limit',
|
|
366
|
+
'COPY packages/logger packages/logger',
|
|
367
|
+
'COPY packages/swagger packages/swagger',
|
|
368
|
+
'COPY packages/i18n packages/i18n',
|
|
369
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
370
|
+
'COPY packages/core packages/core',
|
|
371
|
+
];
|
|
372
|
+
const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
|
|
373
|
+
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-access packages/files-access');
|
|
374
|
+
|
|
375
|
+
content = content.replace(/^RUN pnpm --filter @forgeon\/files-access build\r?\n?/gm, '');
|
|
376
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
377
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
378
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
379
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-access build');
|
|
380
|
+
|
|
381
|
+
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function patchReadme(targetRoot) {
|
|
385
|
+
const readmePath = path.join(targetRoot, 'README.md');
|
|
386
|
+
if (!fs.existsSync(readmePath)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const marker = '## Files Access Module';
|
|
391
|
+
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
392
|
+
if (content.includes(marker)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const section = `## Files Access Module
|
|
397
|
+
|
|
398
|
+
The files-access module adds resource-level authorization checks for file metadata, download, and delete operations.
|
|
399
|
+
|
|
400
|
+
What it adds:
|
|
401
|
+
- \`@forgeon/files-access\` package
|
|
402
|
+
- policy service with owner/public/permission checks
|
|
403
|
+
- files controller enforcement for:
|
|
404
|
+
- \`GET /api/files/:publicId\`
|
|
405
|
+
- \`GET /api/files/:publicId/download\`
|
|
406
|
+
- \`DELETE /api/files/:publicId\`
|
|
407
|
+
- probe endpoint: \`GET /api/health/files-access\`
|
|
408
|
+
|
|
409
|
+
Current policy rules:
|
|
410
|
+
- allow if permission \`files.manage\` is present
|
|
411
|
+
- allow owner (when \`ownerType=user\` and \`ownerId\` matches actor)
|
|
412
|
+
- allow read for \`visibility=public\`
|
|
413
|
+
|
|
414
|
+
Actor context for probe/testing:
|
|
415
|
+
- \`x-forgeon-user-id\`
|
|
416
|
+
- \`x-forgeon-permissions\` (comma-separated)`;
|
|
417
|
+
|
|
418
|
+
if (content.includes('## Prisma In Docker Start')) {
|
|
419
|
+
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
420
|
+
} else {
|
|
421
|
+
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function applyFilesAccessModule({ packageRoot, targetRoot }) {
|
|
428
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-access'));
|
|
429
|
+
patchApiPackage(targetRoot);
|
|
430
|
+
patchFilesPackage(targetRoot);
|
|
431
|
+
patchAppModule(targetRoot);
|
|
432
|
+
patchFilesController(targetRoot);
|
|
433
|
+
patchHealthController(targetRoot);
|
|
434
|
+
patchWebApp(targetRoot);
|
|
435
|
+
patchApiDockerfile(targetRoot);
|
|
436
|
+
patchReadme(targetRoot);
|
|
437
|
+
}
|