create-forgeon 0.3.16 → 0.3.18
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-options.test.mjs +5 -2
- package/src/cli/options.test.mjs +1 -0
- package/src/cli/prompt-select.test.mjs +1 -0
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/core/validate.test.mjs +1 -0
- package/src/modules/accounts.mjs +416 -0
- package/src/modules/db-prisma.mjs +5 -9
- package/src/modules/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +521 -477
- package/src/modules/files-access.mjs +9 -7
- package/src/modules/files-image.mjs +9 -7
- package/src/modules/files-local.mjs +15 -6
- package/src/modules/files-quotas.mjs +8 -6
- package/src/modules/files-s3.mjs +17 -6
- package/src/modules/files.mjs +21 -21
- package/src/modules/idempotency.test.mjs +13 -7
- package/src/modules/probes.test.mjs +4 -2
- package/src/modules/queue.mjs +9 -6
- package/src/modules/rate-limit.mjs +14 -10
- package/src/modules/rbac.mjs +12 -11
- package/src/modules/registry.mjs +22 -35
- package/src/modules/scheduler.mjs +9 -6
- package/src/modules/shared/files-runtime-wiring.mjs +81 -0
- package/src/modules/shared/patch-utils.mjs +29 -1
- package/src/modules/sync-integrations.mjs +102 -422
- package/src/modules/sync-integrations.test.mjs +32 -111
- package/src/run-add-module.test.mjs +1 -0
- package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -325
- package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
- package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
- package/templates/module-fragments/accounts/20_scope.md +29 -0
- package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
- package/templates/module-fragments/accounts/90_status_planned.md +7 -0
- package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
- package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
- package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
- package/templates/module-fragments/swagger/20_scope.md +2 -1
- package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
- package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
- package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
- package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
- package/templates/module-presets/db-prisma/apps/api/prisma/seed.ts +40 -19
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
- package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
- package/templates/module-presets/files/packages/files/package.json +1 -2
- package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
- package/templates/module-presets/files/packages/files/src/index.ts +2 -1
- package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
- package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
- package/src/modules/jwt-auth.mjs +0 -271
- package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
- package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
- package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
- package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
- package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
- package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
- package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
- package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
- /package/templates/module-presets/{jwt-auth/packages/auth-api/src/jwt-auth.guard.ts → accounts/packages/accounts-api/src/access-token.guard.ts} +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
package/package.json
CHANGED
|
@@ -4,8 +4,8 @@ import { parseAddCliArgs } from './add-options.mjs';
|
|
|
4
4
|
|
|
5
5
|
describe('parseAddCliArgs', () => {
|
|
6
6
|
it('parses module id and explicit project', () => {
|
|
7
|
-
const options = parseAddCliArgs(['
|
|
8
|
-
assert.equal(options.moduleId, '
|
|
7
|
+
const options = parseAddCliArgs(['accounts', '--project', './demo']);
|
|
8
|
+
assert.equal(options.moduleId, 'accounts');
|
|
9
9
|
assert.equal(options.project, './demo');
|
|
10
10
|
assert.equal(options.list, false);
|
|
11
11
|
});
|
|
@@ -40,3 +40,6 @@ describe('parseAddCliArgs', () => {
|
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
package/src/cli/options.test.mjs
CHANGED
package/src/core/docs.test.mjs
CHANGED
|
@@ -0,0 +1,416 @@
|
|
|
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
|
+
ensureDependency,
|
|
7
|
+
ensureImportLine,
|
|
8
|
+
ensureLineAfter,
|
|
9
|
+
ensureLineBefore,
|
|
10
|
+
ensureLoadItem,
|
|
11
|
+
ensureValidatorSchema,
|
|
12
|
+
upsertEnvLines,
|
|
13
|
+
} from './shared/patch-utils.mjs';
|
|
14
|
+
import { patchHealthControllerServiceProbe } from './shared/nest-runtime-wiring.mjs';
|
|
15
|
+
import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
|
|
16
|
+
|
|
17
|
+
const ACCOUNTS_RBAC_MARKERS = {
|
|
18
|
+
start: '<!-- forgeon:accounts:rbac:start -->',
|
|
19
|
+
end: '<!-- forgeon:accounts:rbac:end -->',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ACCOUNTS_DEFAULT_RBAC_BLOCK =
|
|
23
|
+
'- RBAC compatibility sync: not enabled by default (add `rbac` and run `pnpm forgeon:sync-integrations` to prepare claims compatibility without changing the base accounts schema).';
|
|
24
|
+
|
|
25
|
+
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
26
|
+
const source = path.join(packageRoot, 'templates', 'module-presets', 'accounts', relativePath);
|
|
27
|
+
if (!fs.existsSync(source)) {
|
|
28
|
+
throw new Error(`Missing accounts preset template: ${source}`);
|
|
29
|
+
}
|
|
30
|
+
const destination = path.join(targetRoot, relativePath);
|
|
31
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
32
|
+
copyRecursive(source, destination);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function patchApiPackage(targetRoot) {
|
|
36
|
+
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
37
|
+
if (!fs.existsSync(packagePath)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
42
|
+
ensureDependency(packageJson, '@forgeon/accounts-api', 'workspace:*');
|
|
43
|
+
ensureDependency(packageJson, '@forgeon/accounts-contracts', 'workspace:*');
|
|
44
|
+
|
|
45
|
+
ensureBuildSteps(packageJson, 'predev', [
|
|
46
|
+
'pnpm --filter @forgeon/accounts-contracts build',
|
|
47
|
+
'pnpm --filter @forgeon/accounts-api build',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
writeJson(packagePath, packageJson);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function patchPrismaSchema(targetRoot) {
|
|
54
|
+
const schemaPath = path.join(targetRoot, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
55
|
+
if (!fs.existsSync(schemaPath)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let content = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
|
|
60
|
+
const userModel = `model User {
|
|
61
|
+
id String @id @default(cuid())
|
|
62
|
+
status String @default("active")
|
|
63
|
+
data Json?
|
|
64
|
+
createdAt DateTime @default(now())
|
|
65
|
+
updatedAt DateTime @updatedAt
|
|
66
|
+
deletedAt DateTime?
|
|
67
|
+
profile UserProfile?
|
|
68
|
+
settings UserSettings?
|
|
69
|
+
authIdentities AuthIdentity[]
|
|
70
|
+
authCredential AuthCredential?
|
|
71
|
+
authRefreshTokens AuthRefreshToken[]
|
|
72
|
+
}`;
|
|
73
|
+
|
|
74
|
+
if (/model User \{[\s\S]*?\n\}/m.test(content)) {
|
|
75
|
+
content = content.replace(/model User \{[\s\S]*?\n\}/m, userModel);
|
|
76
|
+
} else {
|
|
77
|
+
content = `${content.trimEnd()}\n\n${userModel}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const extraModels = [
|
|
81
|
+
`model UserProfile {
|
|
82
|
+
userId String @id
|
|
83
|
+
name String?
|
|
84
|
+
avatar String?
|
|
85
|
+
data Json?
|
|
86
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
87
|
+
}`,
|
|
88
|
+
`model UserSettings {
|
|
89
|
+
userId String @id
|
|
90
|
+
theme String?
|
|
91
|
+
locale String?
|
|
92
|
+
data Json?
|
|
93
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
94
|
+
}`,
|
|
95
|
+
`model AuthIdentity {
|
|
96
|
+
id String @id @default(cuid())
|
|
97
|
+
userId String
|
|
98
|
+
provider String
|
|
99
|
+
providerId String
|
|
100
|
+
createdAt DateTime @default(now())
|
|
101
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
102
|
+
|
|
103
|
+
@@unique([provider, providerId])
|
|
104
|
+
}`,
|
|
105
|
+
`model AuthCredential {
|
|
106
|
+
id String @id @default(cuid())
|
|
107
|
+
userId String @unique
|
|
108
|
+
passwordHash String
|
|
109
|
+
createdAt DateTime @default(now())
|
|
110
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
111
|
+
}`,
|
|
112
|
+
`model AuthRefreshToken {
|
|
113
|
+
id String @id @default(cuid())
|
|
114
|
+
userId String
|
|
115
|
+
tokenHash String
|
|
116
|
+
expiresAt DateTime
|
|
117
|
+
revokedAt DateTime?
|
|
118
|
+
createdAt DateTime @default(now())
|
|
119
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
120
|
+
|
|
121
|
+
@@index([userId, createdAt])
|
|
122
|
+
}`,
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
for (const model of extraModels) {
|
|
126
|
+
const name = model.match(/^model\s+(\w+)/m)?.[1];
|
|
127
|
+
if (!name || content.includes(`model ${name} {`)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
content = `${content.trimEnd()}\n\n${model}\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fs.writeFileSync(schemaPath, `${content.trimEnd()}\n`, 'utf8');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function copyMigrationFolder(packageRoot, targetRoot, migrationName) {
|
|
137
|
+
const migrationDir = path.join(targetRoot, 'apps', 'api', 'prisma', 'migrations', migrationName);
|
|
138
|
+
const migrationFile = path.join(migrationDir, 'migration.sql');
|
|
139
|
+
if (fs.existsSync(migrationFile)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sourceDir = path.join(
|
|
144
|
+
packageRoot,
|
|
145
|
+
'templates',
|
|
146
|
+
'module-presets',
|
|
147
|
+
'accounts',
|
|
148
|
+
'apps',
|
|
149
|
+
'api',
|
|
150
|
+
'prisma',
|
|
151
|
+
'migrations',
|
|
152
|
+
migrationName,
|
|
153
|
+
);
|
|
154
|
+
if (!fs.existsSync(sourceDir)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.mkdirSync(path.dirname(migrationDir), { recursive: true });
|
|
159
|
+
copyRecursive(sourceDir, migrationDir);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function patchPrismaMigration(packageRoot, targetRoot) {
|
|
163
|
+
copyMigrationFolder(packageRoot, targetRoot, '0002_accounts_core');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function patchAppModule(targetRoot) {
|
|
167
|
+
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
168
|
+
if (!fs.existsSync(filePath)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
173
|
+
content = content.replace(
|
|
174
|
+
"import { authConfig, authEnvSchema, ForgeonAccountsModule } from '@forgeon/accounts-api';",
|
|
175
|
+
"import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
176
|
+
);
|
|
177
|
+
content = ensureImportLine(
|
|
178
|
+
content,
|
|
179
|
+
"import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
180
|
+
);
|
|
181
|
+
content = ensureImportLine(
|
|
182
|
+
content,
|
|
183
|
+
"import { ForgeonAccountsDbPrismaModule } from './accounts/forgeon-accounts-db-prisma.module';",
|
|
184
|
+
);
|
|
185
|
+
content = ensureLoadItem(content, 'authConfig');
|
|
186
|
+
content = ensureValidatorSchema(content, 'authEnvSchema');
|
|
187
|
+
|
|
188
|
+
if (!content.includes(' ForgeonAccountsDbPrismaModule,')) {
|
|
189
|
+
if (content.includes(' DbPrismaModule,')) {
|
|
190
|
+
content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonAccountsDbPrismaModule,');
|
|
191
|
+
} else {
|
|
192
|
+
content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonAccountsDbPrismaModule,');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const accountsModuleLine = ` ForgeonAccountsModule.register({
|
|
197
|
+
users: UsersModule.register({}),
|
|
198
|
+
}),`;
|
|
199
|
+
if (!content.includes('ForgeonAccountsModule.register({')) {
|
|
200
|
+
if (content.includes(' ForgeonI18nModule.register({')) {
|
|
201
|
+
content = ensureLineBefore(content, ' ForgeonI18nModule.register({', accountsModuleLine);
|
|
202
|
+
} else if (content.includes(' ForgeonAccountsDbPrismaModule,')) {
|
|
203
|
+
content = ensureLineAfter(content, ' ForgeonAccountsDbPrismaModule,', accountsModuleLine);
|
|
204
|
+
} else {
|
|
205
|
+
content = ensureLineAfter(content, ' CoreErrorsModule,', accountsModuleLine);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function patchHealthController(targetRoot, probeTargets) {
|
|
213
|
+
patchHealthControllerServiceProbe(targetRoot, probeTargets, {
|
|
214
|
+
importLine: "import { AuthService } from '@forgeon/accounts-api';",
|
|
215
|
+
constructorMember: 'private readonly authService: AuthService',
|
|
216
|
+
routePath: 'auth',
|
|
217
|
+
methodName: 'getAuthProbe',
|
|
218
|
+
serviceCall: 'this.authService.getProbeStatus()',
|
|
219
|
+
beforeNeedles: ["@Post('db')"],
|
|
220
|
+
beforeNeedle: 'private translate(',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function registerWebProbe(targetRoot, probeTargets) {
|
|
225
|
+
ensureWebProbeDefinition({
|
|
226
|
+
targetRoot,
|
|
227
|
+
probeTargets,
|
|
228
|
+
definition: {
|
|
229
|
+
id: 'auth',
|
|
230
|
+
title: 'Accounts',
|
|
231
|
+
buttonLabel: 'Check accounts probe',
|
|
232
|
+
resultTitle: 'Accounts probe response',
|
|
233
|
+
path: '/health/auth',
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function patchApiDockerfile(targetRoot) {
|
|
239
|
+
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
240
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
245
|
+
const packageAnchors = [
|
|
246
|
+
'COPY packages/swagger/package.json packages/swagger/package.json',
|
|
247
|
+
'COPY packages/logger/package.json packages/logger/package.json',
|
|
248
|
+
'COPY packages/i18n/package.json packages/i18n/package.json',
|
|
249
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
250
|
+
'COPY packages/core/package.json packages/core/package.json',
|
|
251
|
+
];
|
|
252
|
+
const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
|
|
253
|
+
content = ensureLineAfter(
|
|
254
|
+
content,
|
|
255
|
+
packageAnchor,
|
|
256
|
+
'COPY packages/accounts-contracts/package.json packages/accounts-contracts/package.json',
|
|
257
|
+
);
|
|
258
|
+
content = ensureLineAfter(
|
|
259
|
+
content,
|
|
260
|
+
'COPY packages/accounts-contracts/package.json packages/accounts-contracts/package.json',
|
|
261
|
+
'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const sourceAnchors = [
|
|
265
|
+
'COPY packages/swagger packages/swagger',
|
|
266
|
+
'COPY packages/logger packages/logger',
|
|
267
|
+
'COPY packages/i18n packages/i18n',
|
|
268
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
269
|
+
'COPY packages/core packages/core',
|
|
270
|
+
];
|
|
271
|
+
const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
|
|
272
|
+
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/accounts-contracts packages/accounts-contracts');
|
|
273
|
+
content = ensureLineAfter(
|
|
274
|
+
content,
|
|
275
|
+
'COPY packages/accounts-contracts packages/accounts-contracts',
|
|
276
|
+
'COPY packages/accounts-api packages/accounts-api',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
content = content
|
|
280
|
+
.replace(/^RUN pnpm --filter @forgeon\/auth-contracts build\r?\n?/gm, '')
|
|
281
|
+
.replace(/^RUN pnpm --filter @forgeon\/auth-api build\r?\n?/gm, '');
|
|
282
|
+
|
|
283
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
284
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
285
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
286
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/accounts-contracts build');
|
|
287
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/accounts-api build');
|
|
288
|
+
|
|
289
|
+
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function patchCompose(targetRoot) {
|
|
293
|
+
const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
|
|
294
|
+
if (!fs.existsSync(composePath)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
|
|
299
|
+
if (!content.includes('JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET}')) {
|
|
300
|
+
content = content.replace(
|
|
301
|
+
/^(\s+API_PREFIX:.*)$/m,
|
|
302
|
+
`$1
|
|
303
|
+
JWT_ACCESS_SECRET: \${JWT_ACCESS_SECRET}
|
|
304
|
+
JWT_ACCESS_EXPIRES_IN: \${JWT_ACCESS_EXPIRES_IN}
|
|
305
|
+
JWT_REFRESH_SECRET: \${JWT_REFRESH_SECRET}
|
|
306
|
+
JWT_REFRESH_EXPIRES_IN: \${JWT_REFRESH_EXPIRES_IN}
|
|
307
|
+
AUTH_ARGON2_MEMORY_COST: \${AUTH_ARGON2_MEMORY_COST}
|
|
308
|
+
AUTH_ARGON2_TIME_COST: \${AUTH_ARGON2_TIME_COST}
|
|
309
|
+
AUTH_ARGON2_PARALLELISM: \${AUTH_ARGON2_PARALLELISM}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function patchReadme(targetRoot) {
|
|
317
|
+
const readmePath = path.join(targetRoot, 'README.md');
|
|
318
|
+
if (!fs.existsSync(readmePath)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const section = [
|
|
323
|
+
'## Accounts Module',
|
|
324
|
+
'',
|
|
325
|
+
'The accounts add-module provides a DB-backed accounts/authentication surface with owner-scoped user routes.',
|
|
326
|
+
'',
|
|
327
|
+
'What it adds:',
|
|
328
|
+
'- `@forgeon/accounts-contracts` shared contracts for auth and self-service users routes',
|
|
329
|
+
'- `@forgeon/accounts-api` Nest accounts runtime with JWT auth, argon2 passwords, and hashed refresh-token rotation',
|
|
330
|
+
'- owner-scoped routes under `/api/users/:id`, `/api/users/:id/profile`, and `/api/users/:id/settings` (`/users/me` resolves through the same surface)',
|
|
331
|
+
'- auth probe endpoint: `GET /api/health/auth`',
|
|
332
|
+
'',
|
|
333
|
+
'Current boundaries:',
|
|
334
|
+
'- `UsersModule.register({ user, profile, settings })` controls runtime defaults for JSON-backed extension fields',
|
|
335
|
+
'- email verification and password reset use an internal email stub through `AccountsEmailPort`',
|
|
336
|
+
'- base accounts schema does not include RBAC storage',
|
|
337
|
+
ACCOUNTS_RBAC_MARKERS.start,
|
|
338
|
+
ACCOUNTS_DEFAULT_RBAC_BLOCK,
|
|
339
|
+
ACCOUNTS_RBAC_MARKERS.end,
|
|
340
|
+
'',
|
|
341
|
+
'Default routes:',
|
|
342
|
+
'- `POST /api/auth/register`',
|
|
343
|
+
'- `POST /api/auth/login`',
|
|
344
|
+
'- `POST /api/auth/refresh`',
|
|
345
|
+
'- `POST /api/auth/logout`',
|
|
346
|
+
'- `GET /api/auth/me`',
|
|
347
|
+
'- `POST /api/auth/change-password`',
|
|
348
|
+
'- `POST /api/auth/verify-email` (stub)',
|
|
349
|
+
'- `POST /api/auth/password-reset/request` (stub)',
|
|
350
|
+
'- `POST /api/auth/password-reset/confirm` (stub)',
|
|
351
|
+
].join('\n');
|
|
352
|
+
|
|
353
|
+
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
354
|
+
const sectionHeading = '## Accounts Module';
|
|
355
|
+
if (content.includes(sectionHeading)) {
|
|
356
|
+
const start = content.indexOf(sectionHeading);
|
|
357
|
+
const tail = content.slice(start + sectionHeading.length);
|
|
358
|
+
const nextHeadingMatch = tail.match(/\n##\s+/);
|
|
359
|
+
const end =
|
|
360
|
+
nextHeadingMatch && nextHeadingMatch.index !== undefined
|
|
361
|
+
? start + sectionHeading.length + nextHeadingMatch.index + 1
|
|
362
|
+
: content.length;
|
|
363
|
+
content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
|
|
364
|
+
} else if (content.includes('## Prisma In Docker Start')) {
|
|
365
|
+
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
366
|
+
} else {
|
|
367
|
+
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function applyAccountsModule({ packageRoot, targetRoot }) {
|
|
374
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-contracts'));
|
|
375
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-api'));
|
|
376
|
+
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'accounts'));
|
|
377
|
+
copyFromPreset(
|
|
378
|
+
packageRoot,
|
|
379
|
+
targetRoot,
|
|
380
|
+
path.join('apps', 'api', 'prisma', 'migrations', '0002_accounts_core'),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'accounts' });
|
|
384
|
+
|
|
385
|
+
patchApiPackage(targetRoot);
|
|
386
|
+
patchPrismaSchema(targetRoot);
|
|
387
|
+
patchPrismaMigration(packageRoot, targetRoot);
|
|
388
|
+
patchAppModule(targetRoot);
|
|
389
|
+
patchHealthController(targetRoot, probeTargets);
|
|
390
|
+
registerWebProbe(targetRoot, probeTargets);
|
|
391
|
+
patchApiDockerfile(targetRoot);
|
|
392
|
+
patchCompose(targetRoot);
|
|
393
|
+
patchReadme(targetRoot);
|
|
394
|
+
|
|
395
|
+
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
396
|
+
'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
|
|
397
|
+
'JWT_ACCESS_EXPIRES_IN=15m',
|
|
398
|
+
'JWT_REFRESH_SECRET=forgeon-refresh-secret-change-me',
|
|
399
|
+
'JWT_REFRESH_EXPIRES_IN=7d',
|
|
400
|
+
'AUTH_ARGON2_MEMORY_COST=19456',
|
|
401
|
+
'AUTH_ARGON2_TIME_COST=2',
|
|
402
|
+
'AUTH_ARGON2_PARALLELISM=1',
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
|
|
406
|
+
'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
|
|
407
|
+
'JWT_ACCESS_EXPIRES_IN=15m',
|
|
408
|
+
'JWT_REFRESH_SECRET=forgeon-refresh-secret-change-me',
|
|
409
|
+
'JWT_REFRESH_EXPIRES_IN=7d',
|
|
410
|
+
'AUTH_ARGON2_MEMORY_COST=19456',
|
|
411
|
+
'AUTH_ARGON2_TIME_COST=2',
|
|
412
|
+
'AUTH_ARGON2_PARALLELISM=1',
|
|
413
|
+
]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
@@ -156,17 +156,12 @@ function patchHealthController(targetRoot, probeTargets) {
|
|
|
156
156
|
const dbMethod = `
|
|
157
157
|
@Post('db')
|
|
158
158
|
async getDbProbe() {
|
|
159
|
-
const
|
|
160
|
-
const email = \`health-probe-\${token}@example.local\`;
|
|
161
|
-
const user = await this.prisma.user.create({
|
|
162
|
-
data: { email },
|
|
163
|
-
select: { id: true, email: true, createdAt: true },
|
|
164
|
-
});
|
|
159
|
+
const queryResult = await this.prisma.$queryRaw\`SELECT 1 AS ok\`;
|
|
165
160
|
|
|
166
161
|
return {
|
|
167
162
|
status: 'ok',
|
|
168
163
|
feature: 'db-prisma',
|
|
169
|
-
|
|
164
|
+
queryResult,
|
|
170
165
|
};
|
|
171
166
|
}
|
|
172
167
|
`;
|
|
@@ -183,7 +178,7 @@ function registerWebProbe(targetRoot, probeTargets) {
|
|
|
183
178
|
definition: {
|
|
184
179
|
id: 'db',
|
|
185
180
|
title: 'Database',
|
|
186
|
-
buttonLabel: 'Check database
|
|
181
|
+
buttonLabel: 'Check database query',
|
|
187
182
|
resultTitle: 'DB probe response',
|
|
188
183
|
path: '/health/db',
|
|
189
184
|
request: { method: 'POST' },
|
|
@@ -336,3 +331,4 @@ export function applyDbPrismaModule({ packageRoot, targetRoot }) {
|
|
|
336
331
|
'DATABASE_URL=postgresql://postgres:postgres@db:5432/app?schema=public',
|
|
337
332
|
]);
|
|
338
333
|
}
|
|
334
|
+
|
|
@@ -25,6 +25,48 @@ const TEST_PRESETS = [
|
|
|
25
25
|
requires: [],
|
|
26
26
|
optionalIntegrations: [],
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
id: 'accounts',
|
|
30
|
+
label: 'Accounts',
|
|
31
|
+
implemented: true,
|
|
32
|
+
detectionPaths: ['packages/accounts-api/package.json'],
|
|
33
|
+
provides: ['accounts-runtime'],
|
|
34
|
+
requires: [{ type: 'capability', id: 'db-adapter' }],
|
|
35
|
+
optionalIntegrations: [
|
|
36
|
+
{
|
|
37
|
+
id: 'accounts-rbac',
|
|
38
|
+
title: 'Accounts RBAC Compatibility Sync',
|
|
39
|
+
modules: ['accounts', 'rbac'],
|
|
40
|
+
requires: [{ type: 'module', id: 'rbac' }],
|
|
41
|
+
description: ['Prepare accounts auth claims compatibility'],
|
|
42
|
+
followUpCommands: [
|
|
43
|
+
'npx create-forgeon@latest add rbac',
|
|
44
|
+
'pnpm forgeon:sync-integrations',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'rbac',
|
|
51
|
+
label: 'RBAC',
|
|
52
|
+
implemented: true,
|
|
53
|
+
detectionPaths: ['packages/rbac/package.json'],
|
|
54
|
+
provides: ['rbac-runtime'],
|
|
55
|
+
requires: [],
|
|
56
|
+
optionalIntegrations: [
|
|
57
|
+
{
|
|
58
|
+
id: 'accounts-rbac',
|
|
59
|
+
title: 'Accounts RBAC Compatibility Sync',
|
|
60
|
+
modules: ['accounts', 'rbac'],
|
|
61
|
+
requires: [{ type: 'module', id: 'accounts' }],
|
|
62
|
+
description: ['Prepare accounts auth claims compatibility'],
|
|
63
|
+
followUpCommands: [
|
|
64
|
+
'npx create-forgeon@latest add accounts',
|
|
65
|
+
'pnpm forgeon:sync-integrations',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
28
70
|
{
|
|
29
71
|
id: 'files',
|
|
30
72
|
label: 'Files',
|
|
@@ -88,27 +130,6 @@ const TEST_PRESETS = [
|
|
|
88
130
|
requires: [{ type: 'capability', id: 'files-runtime' }],
|
|
89
131
|
optionalIntegrations: [],
|
|
90
132
|
},
|
|
91
|
-
{
|
|
92
|
-
id: 'jwt-auth',
|
|
93
|
-
label: 'JWT Auth',
|
|
94
|
-
implemented: true,
|
|
95
|
-
detectionPaths: ['packages/auth-api/package.json'],
|
|
96
|
-
provides: ['auth-runtime'],
|
|
97
|
-
requires: [],
|
|
98
|
-
optionalIntegrations: [
|
|
99
|
-
{
|
|
100
|
-
id: 'auth-persistence',
|
|
101
|
-
title: 'Auth Persistence Integration',
|
|
102
|
-
modules: ['jwt-auth', 'db-adapter'],
|
|
103
|
-
requires: [{ type: 'capability', id: 'db-adapter' }],
|
|
104
|
-
description: ['Persist refresh-token state'],
|
|
105
|
-
followUpCommands: [
|
|
106
|
-
'npx create-forgeon@latest add db-prisma',
|
|
107
|
-
'pnpm forgeon:sync-integrations',
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
133
|
{
|
|
113
134
|
id: 'queue',
|
|
114
135
|
label: 'Queue',
|
|
@@ -169,6 +190,28 @@ describe('module dependency helpers', () => {
|
|
|
169
190
|
}
|
|
170
191
|
});
|
|
171
192
|
|
|
193
|
+
it('builds a concrete install plan for accounts with required db-adapter', async () => {
|
|
194
|
+
const targetRoot = mkTmp('forgeon-deps-accounts-');
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const result = await resolveModuleInstallPlan({
|
|
198
|
+
moduleId: 'accounts',
|
|
199
|
+
targetRoot,
|
|
200
|
+
presets: TEST_PRESETS,
|
|
201
|
+
withRequired: true,
|
|
202
|
+
isInteractive: false,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
assert.equal(result.cancelled, false);
|
|
206
|
+
assert.deepEqual(result.moduleSequence, ['db-prisma', 'accounts']);
|
|
207
|
+
assert.deepEqual(result.selectedProviders, {
|
|
208
|
+
'db-adapter': 'db-prisma',
|
|
209
|
+
});
|
|
210
|
+
} finally {
|
|
211
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
172
215
|
it('fails in non-interactive mode with --with-required when capability provider mapping is ambiguous', async () => {
|
|
173
216
|
const targetRoot = mkTmp('forgeon-deps-provider-required-');
|
|
174
217
|
|
|
@@ -318,21 +361,21 @@ describe('module dependency helpers', () => {
|
|
|
318
361
|
}
|
|
319
362
|
});
|
|
320
363
|
|
|
321
|
-
it('reports
|
|
364
|
+
it('reports pending optional integrations for accounts when rbac is missing', () => {
|
|
322
365
|
const targetRoot = mkTmp('forgeon-deps-optional-');
|
|
323
366
|
try {
|
|
324
|
-
fs.mkdirSync(path.join(targetRoot, 'packages', '
|
|
325
|
-
fs.writeFileSync(path.join(targetRoot, 'packages', '
|
|
367
|
+
fs.mkdirSync(path.join(targetRoot, 'packages', 'accounts-api'), { recursive: true });
|
|
368
|
+
fs.writeFileSync(path.join(targetRoot, 'packages', 'accounts-api', 'package.json'), '{}\n', 'utf8');
|
|
326
369
|
|
|
327
370
|
const pending = getPendingOptionalIntegrations({
|
|
328
|
-
moduleId: '
|
|
371
|
+
moduleId: 'accounts',
|
|
329
372
|
targetRoot,
|
|
330
373
|
presets: TEST_PRESETS,
|
|
331
374
|
});
|
|
332
375
|
|
|
333
376
|
assert.equal(pending.length, 1);
|
|
334
|
-
assert.equal(pending[0].id, '
|
|
335
|
-
assert.equal(pending[0].missing[0].id, '
|
|
377
|
+
assert.equal(pending[0].id, 'accounts-rbac');
|
|
378
|
+
assert.equal(pending[0].missing[0].id, 'rbac');
|
|
336
379
|
} finally {
|
|
337
380
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
338
381
|
}
|
|
@@ -397,5 +440,4 @@ describe('module dependency helpers', () => {
|
|
|
397
440
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
398
441
|
}
|
|
399
442
|
});
|
|
400
|
-
});
|
|
401
|
-
|
|
443
|
+
});
|
package/src/modules/executor.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { applyFilesQuotasModule } from './files-quotas.mjs';
|
|
|
10
10
|
import { applyFilesLocalModule } from './files-local.mjs';
|
|
11
11
|
import { applyFilesS3Module } from './files-s3.mjs';
|
|
12
12
|
import { applyI18nModule } from './i18n.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { applyAccountsModule } from './accounts.mjs';
|
|
14
14
|
import { applyLoggerModule } from './logger.mjs';
|
|
15
15
|
import { applyRateLimitModule } from './rate-limit.mjs';
|
|
16
16
|
import { applyRbacModule } from './rbac.mjs';
|
|
@@ -43,7 +43,7 @@ const MODULE_APPLIERS = {
|
|
|
43
43
|
'files-local': applyFilesLocalModule,
|
|
44
44
|
'files-s3': applyFilesS3Module,
|
|
45
45
|
i18n: applyI18nModule,
|
|
46
|
-
|
|
46
|
+
accounts: applyAccountsModule,
|
|
47
47
|
logger: applyLoggerModule,
|
|
48
48
|
queue: applyQueueModule,
|
|
49
49
|
'rate-limit': applyRateLimitModule,
|
|
@@ -85,3 +85,4 @@ export function addModule({ moduleId, targetRoot, packageRoot, writeDocs = true
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
|