create-forgeon 0.3.16 → 0.3.17
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/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +512 -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/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
|
@@ -2,347 +2,105 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { Injectable } from '@nestjs/common';
|
|
10
|
-
|
|
11
|
-
@Injectable()
|
|
12
|
-
export class PrismaAuthRefreshTokenStore implements AuthRefreshTokenStore {
|
|
13
|
-
readonly kind = 'prisma';
|
|
5
|
+
const ACCOUNTS_RBAC_MARKERS = {
|
|
6
|
+
start: '<!-- forgeon:accounts:rbac:start -->',
|
|
7
|
+
end: '<!-- forgeon:accounts:rbac:end -->',
|
|
8
|
+
};
|
|
14
9
|
|
|
15
|
-
|
|
10
|
+
const ACCOUNTS_RBAC_ENABLED_BLOCK =
|
|
11
|
+
'- RBAC compatibility sync: contracts and JWT payload surfaces are prepared for optional RBAC claims, while the base accounts schema remains free of roles and permissions.';
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
create: { email: subject, refreshTokenHash: hash },
|
|
21
|
-
update: { refreshTokenHash: hash },
|
|
22
|
-
select: { id: true },
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async getRefreshTokenHash(subject: string): Promise<string | null> {
|
|
27
|
-
const user = await this.prisma.user.findUnique({
|
|
28
|
-
where: { email: subject },
|
|
29
|
-
select: { refreshTokenHash: true },
|
|
30
|
-
});
|
|
31
|
-
return user?.refreshTokenHash ?? null;
|
|
32
|
-
}
|
|
13
|
+
function escapeRegExp(value) {
|
|
14
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
}
|
|
33
16
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
17
|
+
function replaceReadmeManagedBlock(content, startMarker, endMarker, nextBody) {
|
|
18
|
+
const pattern = new RegExp(`${escapeRegExp(startMarker)}\\n[\\s\\S]*?\\n${escapeRegExp(endMarker)}`);
|
|
19
|
+
if (!pattern.test(content)) {
|
|
20
|
+
return content;
|
|
39
21
|
}
|
|
22
|
+
return content.replace(pattern, `${startMarker}\n${nextBody}\n${endMarker}`);
|
|
40
23
|
}
|
|
41
|
-
`;
|
|
42
|
-
|
|
43
|
-
const PRISMA_AUTH_MIGRATION_CONTENT = `-- AlterTable
|
|
44
|
-
ALTER TABLE "User"
|
|
45
|
-
ADD COLUMN "refreshTokenHash" TEXT;
|
|
46
|
-
`;
|
|
47
|
-
|
|
48
|
-
const AUTH_PERSISTENCE_STRATEGIES = [
|
|
49
|
-
{
|
|
50
|
-
id: 'db-prisma',
|
|
51
|
-
providerLabel: 'db-prisma',
|
|
52
|
-
isDetected: (detected) => detected.dbPrisma,
|
|
53
|
-
apply: syncJwtDbPrisma,
|
|
54
|
-
},
|
|
55
|
-
];
|
|
56
24
|
|
|
57
25
|
function detectModules(rootDir) {
|
|
58
26
|
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
59
27
|
const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
|
|
60
28
|
|
|
61
29
|
return {
|
|
62
|
-
|
|
63
|
-
fs.existsSync(path.join(rootDir, 'packages', '
|
|
64
|
-
appModuleText.includes("from '@forgeon/
|
|
30
|
+
accounts:
|
|
31
|
+
fs.existsSync(path.join(rootDir, 'packages', 'accounts-api', 'package.json')) ||
|
|
32
|
+
appModuleText.includes("from '@forgeon/accounts-api'"),
|
|
65
33
|
rbac:
|
|
66
34
|
fs.existsSync(path.join(rootDir, 'packages', 'rbac', 'package.json')) ||
|
|
67
35
|
appModuleText.includes("from '@forgeon/rbac'"),
|
|
68
|
-
dbPrisma:
|
|
69
|
-
fs.existsSync(path.join(rootDir, 'packages', 'db-prisma', 'package.json')) ||
|
|
70
|
-
appModuleText.includes("from '@forgeon/db-prisma'"),
|
|
71
36
|
};
|
|
72
37
|
}
|
|
73
38
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
const index = content.indexOf(anchorLine);
|
|
79
|
-
if (index < 0) {
|
|
80
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
81
|
-
}
|
|
82
|
-
const insertAt = index + anchorLine.length;
|
|
83
|
-
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const JWT_AUTH_PERSISTENCE_MARKERS = {
|
|
87
|
-
start: '<!-- forgeon:jwt-auth:persistence:start -->',
|
|
88
|
-
end: '<!-- forgeon:jwt-auth:persistence:end -->',
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const JWT_AUTH_RBAC_MARKERS = {
|
|
92
|
-
start: '<!-- forgeon:jwt-auth:rbac:start -->',
|
|
93
|
-
end: '<!-- forgeon:jwt-auth:rbac:end -->',
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK = [
|
|
97
|
-
'- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
|
|
98
|
-
'- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
|
|
99
|
-
].join('\n');
|
|
100
|
-
|
|
101
|
-
const JWT_AUTH_RBAC_ENABLED_BLOCK = '- RBAC integration: demo auth tokens include `health.rbac` permission';
|
|
102
|
-
|
|
103
|
-
function escapeRegExp(value) {
|
|
104
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function replaceReadmeManagedBlock(content, startMarker, endMarker, nextBody) {
|
|
108
|
-
const pattern = new RegExp(`${escapeRegExp(startMarker)}\\n[\\s\\S]*?\\n${escapeRegExp(endMarker)}`);
|
|
109
|
-
if (!pattern.test(content)) {
|
|
110
|
-
return content;
|
|
111
|
-
}
|
|
112
|
-
return content.replace(pattern, `${startMarker}\n${nextBody}\n${endMarker}`);
|
|
113
|
-
}
|
|
114
|
-
function resolveAuthPersistenceStrategy(detected) {
|
|
115
|
-
const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
|
|
116
|
-
if (matched.length === 0) {
|
|
117
|
-
return { kind: 'none' };
|
|
118
|
-
}
|
|
119
|
-
if (matched.length > 1) {
|
|
120
|
-
return { kind: 'conflict', strategies: matched };
|
|
121
|
-
}
|
|
122
|
-
return { kind: 'single', strategy: matched[0] };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
126
|
-
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
127
|
-
const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
128
|
-
const storePath = path.join(rootDir, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts');
|
|
129
|
-
const migrationPath = path.join(
|
|
130
|
-
rootDir,
|
|
131
|
-
'apps',
|
|
132
|
-
'api',
|
|
133
|
-
'prisma',
|
|
134
|
-
'migrations',
|
|
135
|
-
'0002_auth_refresh_token_hash',
|
|
136
|
-
'migration.sql',
|
|
137
|
-
);
|
|
39
|
+
function syncAccountsRbac(rootDir, changedFiles) {
|
|
40
|
+
const contractsPath = path.join(rootDir, 'packages', 'accounts-contracts', 'src', 'index.ts');
|
|
41
|
+
const authTypesPath = path.join(rootDir, 'packages', 'accounts-api', 'src', 'auth.types.ts');
|
|
138
42
|
const readmePath = path.join(rootDir, 'README.md');
|
|
139
43
|
|
|
140
|
-
if (!fs.existsSync(
|
|
141
|
-
return { applied: false, reason: '
|
|
44
|
+
if (!fs.existsSync(contractsPath) || !fs.existsSync(authTypesPath) || !fs.existsSync(readmePath)) {
|
|
45
|
+
return { applied: false, reason: 'accounts package files are missing' };
|
|
142
46
|
}
|
|
143
47
|
|
|
144
48
|
let touched = false;
|
|
145
49
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
let appModule = fs.readFileSync(appModulePath, 'utf8').replace(/\r\n/g, '\n');
|
|
154
|
-
const originalAppModule = appModule;
|
|
155
|
-
|
|
156
|
-
if (!appModule.includes("AUTH_REFRESH_TOKEN_STORE, authConfig")) {
|
|
157
|
-
appModule = appModule.replace(
|
|
158
|
-
/import\s*\{\s*authConfig,\s*authEnvSchema,\s*ForgeonAuthModule\s*\}\s*from '@forgeon\/auth-api';/m,
|
|
159
|
-
"import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
50
|
+
let contracts = fs.readFileSync(contractsPath, 'utf8').replace(/\r\n/g, '\n');
|
|
51
|
+
const originalContracts = contracts;
|
|
52
|
+
if (!contracts.includes('roles?: string[];')) {
|
|
53
|
+
contracts = contracts.replace(
|
|
54
|
+
" type: 'access';",
|
|
55
|
+
" type: 'access';\n roles?: string[];\n permissions?: string[];",
|
|
160
56
|
);
|
|
161
57
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
appModule,
|
|
167
|
-
"import { HealthController } from './health/health.controller';",
|
|
168
|
-
storeImportLine,
|
|
58
|
+
if (!contracts.includes("jti: string;\n type: 'refresh';\n roles?: string[];")) {
|
|
59
|
+
contracts = contracts.replace(
|
|
60
|
+
" jti: string;\n type: 'refresh';",
|
|
61
|
+
" jti: string;\n type: 'refresh';\n roles?: string[];\n permissions?: string[];",
|
|
169
62
|
);
|
|
170
63
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
/ForgeonAuthModule\.register\(\),/m,
|
|
175
|
-
`ForgeonAuthModule.register({
|
|
176
|
-
imports: [DbPrismaModule],
|
|
177
|
-
refreshTokenStoreProvider: {
|
|
178
|
-
provide: AUTH_REFRESH_TOKEN_STORE,
|
|
179
|
-
useClass: PrismaAuthRefreshTokenStore,
|
|
180
|
-
},
|
|
181
|
-
}),`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (appModule !== originalAppModule) {
|
|
186
|
-
fs.writeFileSync(appModulePath, `${appModule.trimEnd()}\n`, 'utf8');
|
|
187
|
-
changedFiles.add(appModulePath);
|
|
188
|
-
touched = true;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let schema = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
|
|
192
|
-
const originalSchema = schema;
|
|
193
|
-
if (!schema.includes('refreshTokenHash')) {
|
|
194
|
-
schema = schema.replace(/email\s+String\s+@unique/g, 'email String @unique\n refreshTokenHash String?');
|
|
195
|
-
}
|
|
196
|
-
if (schema !== originalSchema) {
|
|
197
|
-
fs.writeFileSync(schemaPath, `${schema.trimEnd()}\n`, 'utf8');
|
|
198
|
-
changedFiles.add(schemaPath);
|
|
64
|
+
if (contracts !== originalContracts) {
|
|
65
|
+
fs.writeFileSync(contractsPath, `${contracts.trimEnd()}\n`, 'utf8');
|
|
66
|
+
changedFiles.add(contractsPath);
|
|
199
67
|
touched = true;
|
|
200
68
|
}
|
|
201
69
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (fs.existsSync(readmePath)) {
|
|
210
|
-
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
211
|
-
const originalReadme = readme;
|
|
212
|
-
const managedReadme = replaceReadmeManagedBlock(
|
|
213
|
-
readme,
|
|
214
|
-
JWT_AUTH_PERSISTENCE_MARKERS.start,
|
|
215
|
-
JWT_AUTH_PERSISTENCE_MARKERS.end,
|
|
216
|
-
JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK,
|
|
70
|
+
let authTypes = fs.readFileSync(authTypesPath, 'utf8').replace(/\r\n/g, '\n');
|
|
71
|
+
const originalAuthTypes = authTypes;
|
|
72
|
+
if (!authTypes.includes('roles?: string[];')) {
|
|
73
|
+
authTypes = authTypes.replace(
|
|
74
|
+
" exp?: number;",
|
|
75
|
+
" exp?: number;\n roles?: string[];\n permissions?: string[];",
|
|
217
76
|
);
|
|
218
|
-
if (managedReadme !== readme) {
|
|
219
|
-
readme = managedReadme;
|
|
220
|
-
} else {
|
|
221
|
-
readme = readme.replace(
|
|
222
|
-
'- refresh token persistence: disabled by default (stateless mode; enable it later through a `db-adapter` provider + integration sync)',
|
|
223
|
-
'- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
|
|
224
|
-
);
|
|
225
|
-
readme = readme.replace(
|
|
226
|
-
/- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to wire auth persistence to the active DB adapter implementation\./m,
|
|
227
|
-
'- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
if (readme !== originalReadme) {
|
|
231
|
-
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
232
|
-
changedFiles.add(readmePath);
|
|
233
|
-
touched = true;
|
|
234
|
-
}
|
|
235
77
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
return { applied: true };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function syncJwtRbacClaims({ rootDir, changedFiles }) {
|
|
244
|
-
const authContractsPath = path.join(rootDir, 'packages', 'auth-contracts', 'src', 'index.ts');
|
|
245
|
-
const authServicePath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.service.ts');
|
|
246
|
-
const authControllerPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.controller.ts');
|
|
247
|
-
const readmePath = path.join(rootDir, 'README.md');
|
|
248
|
-
|
|
249
|
-
if (!fs.existsSync(authContractsPath) || !fs.existsSync(authServicePath) || !fs.existsSync(authControllerPath)) {
|
|
250
|
-
return { applied: false, reason: 'auth package files are missing' };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
let touched = false;
|
|
254
|
-
|
|
255
|
-
let authContracts = fs.readFileSync(authContractsPath, 'utf8').replace(/\r\n/g, '\n');
|
|
256
|
-
const originalAuthContracts = authContracts;
|
|
257
|
-
if (!authContracts.includes('permissions?: string[];')) {
|
|
258
|
-
authContracts = authContracts.replace(
|
|
259
|
-
' roles: string[];',
|
|
260
|
-
` roles: string[];
|
|
261
|
-
permissions?: string[];`,
|
|
78
|
+
if (authTypes.includes('export interface AuthRefreshTokenPayload extends AuthRefreshClaims {') && !authTypes.includes("AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n roles?: string[];")) {
|
|
79
|
+
authTypes = authTypes.replace(
|
|
80
|
+
"export interface AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n}",
|
|
81
|
+
"export interface AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n roles?: string[];\n permissions?: string[];\n}",
|
|
262
82
|
);
|
|
263
83
|
}
|
|
264
|
-
if (
|
|
265
|
-
fs.writeFileSync(
|
|
266
|
-
changedFiles.add(
|
|
84
|
+
if (authTypes !== originalAuthTypes) {
|
|
85
|
+
fs.writeFileSync(authTypesPath, `${authTypes.trimEnd()}\n`, 'utf8');
|
|
86
|
+
changedFiles.add(authTypesPath);
|
|
267
87
|
touched = true;
|
|
268
88
|
}
|
|
269
89
|
|
|
270
|
-
let
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
90
|
+
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
91
|
+
const originalReadme = readme;
|
|
92
|
+
readme = replaceReadmeManagedBlock(
|
|
93
|
+
readme,
|
|
94
|
+
ACCOUNTS_RBAC_MARKERS.start,
|
|
95
|
+
ACCOUNTS_RBAC_MARKERS.end,
|
|
96
|
+
ACCOUNTS_RBAC_ENABLED_BLOCK,
|
|
276
97
|
);
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
` roles: user.roles,
|
|
281
|
-
permissions: user.permissions,`,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
if (!authService.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
|
|
285
|
-
authService = authService.replace(
|
|
286
|
-
" roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
|
|
287
|
-
` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
|
|
288
|
-
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
if (!authService.includes('demoPermissions: [')) {
|
|
292
|
-
authService = authService.replace(
|
|
293
|
-
" demoEmail: this.configService.demoEmail,",
|
|
294
|
-
` demoEmail: this.configService.demoEmail,
|
|
295
|
-
demoPermissions: ['health.rbac'],`,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
if (authService !== originalAuthService) {
|
|
299
|
-
fs.writeFileSync(authServicePath, `${authService.trimEnd()}\n`, 'utf8');
|
|
300
|
-
changedFiles.add(authServicePath);
|
|
98
|
+
if (readme !== originalReadme) {
|
|
99
|
+
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
100
|
+
changedFiles.add(readmePath);
|
|
301
101
|
touched = true;
|
|
302
102
|
}
|
|
303
103
|
|
|
304
|
-
let authController = fs.readFileSync(authControllerPath, 'utf8').replace(/\r\n/g, '\n');
|
|
305
|
-
const originalAuthController = authController;
|
|
306
|
-
if (!authController.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
|
|
307
|
-
authController = authController.replace(
|
|
308
|
-
" roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
|
|
309
|
-
` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
|
|
310
|
-
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
if (authController !== originalAuthController) {
|
|
314
|
-
fs.writeFileSync(authControllerPath, `${authController.trimEnd()}\n`, 'utf8');
|
|
315
|
-
changedFiles.add(authControllerPath);
|
|
316
|
-
touched = true;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (fs.existsSync(readmePath)) {
|
|
320
|
-
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
321
|
-
const originalReadme = readme;
|
|
322
|
-
const managedReadme = replaceReadmeManagedBlock(
|
|
323
|
-
readme,
|
|
324
|
-
JWT_AUTH_RBAC_MARKERS.start,
|
|
325
|
-
JWT_AUTH_RBAC_MARKERS.end,
|
|
326
|
-
JWT_AUTH_RBAC_ENABLED_BLOCK,
|
|
327
|
-
);
|
|
328
|
-
if (managedReadme !== readme) {
|
|
329
|
-
readme = managedReadme;
|
|
330
|
-
} else if (!readme.includes('- RBAC integration: demo auth tokens include `health.rbac` permission')) {
|
|
331
|
-
const marker = 'Default demo credentials:';
|
|
332
|
-
if (readme.includes(marker)) {
|
|
333
|
-
readme = readme.replace(
|
|
334
|
-
marker,
|
|
335
|
-
'- RBAC integration: demo auth tokens include `health.rbac` permission\n\nDefault demo credentials:',
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (readme !== originalReadme) {
|
|
340
|
-
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
341
|
-
changedFiles.add(readmePath);
|
|
342
|
-
touched = true;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
104
|
if (!touched) {
|
|
347
105
|
return { applied: false, reason: 'already synced' };
|
|
348
106
|
}
|
|
@@ -351,35 +109,18 @@ function syncJwtRbacClaims({ rootDir, changedFiles }) {
|
|
|
351
109
|
|
|
352
110
|
function run() {
|
|
353
111
|
const rootDir = process.cwd();
|
|
354
|
-
const changedFiles = new Set();
|
|
355
112
|
const detected = detectModules(rootDir);
|
|
113
|
+
const changedFiles = new Set();
|
|
356
114
|
const summary = [];
|
|
357
|
-
const authPersistence = resolveAuthPersistenceStrategy(detected);
|
|
358
|
-
|
|
359
|
-
if (detected.jwtAuth && authPersistence.kind === 'single') {
|
|
360
|
-
summary.push({
|
|
361
|
-
feature: `jwt-auth + db-adapter (current provider: ${authPersistence.strategy.providerLabel})`,
|
|
362
|
-
result: authPersistence.strategy.apply({ rootDir, changedFiles }),
|
|
363
|
-
});
|
|
364
|
-
} else {
|
|
365
|
-
const reason =
|
|
366
|
-
authPersistence.kind === 'conflict'
|
|
367
|
-
? 'multiple db-adapter providers detected'
|
|
368
|
-
: 'required components are not both available';
|
|
369
|
-
summary.push({
|
|
370
|
-
feature: 'jwt-auth + db-adapter (current provider: db-prisma)',
|
|
371
|
-
result: { applied: false, reason },
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
115
|
|
|
375
|
-
if (detected.
|
|
116
|
+
if (detected.accounts && detected.rbac) {
|
|
376
117
|
summary.push({
|
|
377
|
-
feature: '
|
|
378
|
-
result:
|
|
118
|
+
feature: 'accounts + rbac',
|
|
119
|
+
result: syncAccountsRbac(rootDir, changedFiles),
|
|
379
120
|
});
|
|
380
121
|
} else {
|
|
381
122
|
summary.push({
|
|
382
|
-
feature: '
|
|
123
|
+
feature: 'accounts + rbac',
|
|
383
124
|
result: { applied: false, reason: 'required components are not both available' },
|
|
384
125
|
});
|
|
385
126
|
}
|
|
@@ -396,10 +137,9 @@ function run() {
|
|
|
396
137
|
if (changedFiles.size > 0) {
|
|
397
138
|
console.log('- changed files:');
|
|
398
139
|
for (const filePath of [...changedFiles].sort()) {
|
|
399
|
-
|
|
400
|
-
console.log(` - ${relative}`);
|
|
140
|
+
console.log(` - ${path.relative(rootDir, filePath)}`);
|
|
401
141
|
}
|
|
402
142
|
}
|
|
403
143
|
}
|
|
404
144
|
|
|
405
|
-
run();
|
|
145
|
+
run();
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
# MODULE: {{MODULE_LABEL}}
|
|
1
|
+
# MODULE: {{MODULE_LABEL}}
|
|
2
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
## Overview
|
|
2
|
-
|
|
3
|
-
- Id: `{{MODULE_ID}}`
|
|
4
|
-
- Category: `{{MODULE_CATEGORY}}`
|
|
5
|
-
- Status: {{MODULE_STATUS}}
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
- Id: `{{MODULE_ID}}`
|
|
4
|
+
- Category: `{{MODULE_CATEGORY}}`
|
|
5
|
+
- Status: {{MODULE_STATUS}}
|
|
6
6
|
- Description: {{MODULE_DESCRIPTION}}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
## Scope
|
|
2
|
+
|
|
3
|
+
Implemented scope:
|
|
4
|
+
|
|
5
|
+
1. Public installer surface:
|
|
6
|
+
- single umbrella add-module: `accounts`
|
|
7
|
+
- requires `db-adapter`
|
|
8
|
+
2. Internal runtime split:
|
|
9
|
+
- `@forgeon/accounts-contracts`
|
|
10
|
+
- `@forgeon/accounts-api`
|
|
11
|
+
- users core, auth core, auth-jwt, auth-password, email stub port/adapter
|
|
12
|
+
3. API runtime:
|
|
13
|
+
- `POST /api/auth/register`
|
|
14
|
+
- `POST /api/auth/login`
|
|
15
|
+
- `POST /api/auth/refresh`
|
|
16
|
+
- `POST /api/auth/logout`
|
|
17
|
+
- `GET /api/auth/me`
|
|
18
|
+
- `POST /api/auth/change-password`
|
|
19
|
+
- stub endpoints for verify-email and password reset
|
|
20
|
+
4. Users surface:
|
|
21
|
+
- owner-scoped routes under `/api/users/:id`, `/api/users/:id/profile`, `/api/users/:id/settings`
|
|
22
|
+
- `/users/me` is resolved through the same owner-scoped route surface
|
|
23
|
+
5. Persistence and security:
|
|
24
|
+
- DB-backed `User`, `UserProfile`, `UserSettings`, `AuthIdentity`, `AuthCredential`, `AuthRefreshToken`
|
|
25
|
+
- argon2 for password and refresh-token hashing
|
|
26
|
+
- refresh token rotation + revoke with per-token storage rows
|
|
27
|
+
6. Module checks:
|
|
28
|
+
- API probe endpoint: `GET /api/health/auth`
|
|
29
|
+
- default web probe button + result block
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Current State
|
|
2
|
+
|
|
3
|
+
Status: implemented.
|
|
4
|
+
|
|
5
|
+
Notes:
|
|
6
|
+
- `accounts` is a hard consumer of the `db-adapter` capability.
|
|
7
|
+
- The base accounts schema does not store RBAC roles or permissions.
|
|
8
|
+
- Email verification and password reset are routed through an internal email stub boundary until the public `emails` module is implemented.
|
|
@@ -12,5 +12,6 @@ This module is backend-first. It does not include frontend route guards. If fron
|
|
|
12
12
|
|
|
13
13
|
Optional integration:
|
|
14
14
|
|
|
15
|
-
- `
|
|
16
|
-
- this module does not require `
|
|
15
|
+
- `accounts` can extend demo JWT claims with RBAC permissions through `pnpm forgeon:sync-integrations`
|
|
16
|
+
- this module does not require `accounts` to install or work for header-based/manual probe checks
|
|
17
|
+
|
|
@@ -21,5 +21,6 @@ Failure path:
|
|
|
21
21
|
|
|
22
22
|
Integration note:
|
|
23
23
|
|
|
24
|
-
- if `
|
|
24
|
+
- if `accounts` is also installed, the optional `accounts-rbac` integration can expose demo permissions inside JWT payloads
|
|
25
25
|
- that integration is explicit and is applied through `pnpm forgeon:sync-integrations`
|
|
26
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
-- no enum is required in v1
|
|
3
|
+
|
|
4
|
+
-- AlterTable
|
|
5
|
+
ALTER TABLE "User"
|
|
6
|
+
DROP COLUMN IF EXISTS "email",
|
|
7
|
+
ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'active',
|
|
8
|
+
ADD COLUMN IF NOT EXISTS "data" JSONB,
|
|
9
|
+
ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3),
|
|
10
|
+
ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
11
|
+
|
|
12
|
+
-- CreateTable
|
|
13
|
+
CREATE TABLE IF NOT EXISTS "UserProfile" (
|
|
14
|
+
"userId" TEXT NOT NULL,
|
|
15
|
+
"name" TEXT,
|
|
16
|
+
"avatar" TEXT,
|
|
17
|
+
"data" JSONB,
|
|
18
|
+
CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("userId")
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- CreateTable
|
|
22
|
+
CREATE TABLE IF NOT EXISTS "UserSettings" (
|
|
23
|
+
"userId" TEXT NOT NULL,
|
|
24
|
+
"theme" TEXT,
|
|
25
|
+
"locale" TEXT,
|
|
26
|
+
"data" JSONB,
|
|
27
|
+
CONSTRAINT "UserSettings_pkey" PRIMARY KEY ("userId")
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- CreateTable
|
|
31
|
+
CREATE TABLE IF NOT EXISTS "AuthIdentity" (
|
|
32
|
+
"id" TEXT NOT NULL,
|
|
33
|
+
"userId" TEXT NOT NULL,
|
|
34
|
+
"provider" TEXT NOT NULL,
|
|
35
|
+
"providerId" TEXT NOT NULL,
|
|
36
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("id")
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- CreateTable
|
|
41
|
+
CREATE TABLE IF NOT EXISTS "AuthCredential" (
|
|
42
|
+
"id" TEXT NOT NULL,
|
|
43
|
+
"userId" TEXT NOT NULL,
|
|
44
|
+
"passwordHash" TEXT NOT NULL,
|
|
45
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
CONSTRAINT "AuthCredential_pkey" PRIMARY KEY ("id")
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
-- CreateTable
|
|
50
|
+
CREATE TABLE IF NOT EXISTS "AuthRefreshToken" (
|
|
51
|
+
"id" TEXT NOT NULL,
|
|
52
|
+
"userId" TEXT NOT NULL,
|
|
53
|
+
"tokenHash" TEXT NOT NULL,
|
|
54
|
+
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
55
|
+
"revokedAt" TIMESTAMP(3),
|
|
56
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
57
|
+
CONSTRAINT "AuthRefreshToken_pkey" PRIMARY KEY ("id")
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- Indexes
|
|
61
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "AuthIdentity_provider_providerId_key" ON "AuthIdentity"("provider", "providerId");
|
|
62
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "AuthCredential_userId_key" ON "AuthCredential"("userId");
|
|
63
|
+
CREATE INDEX IF NOT EXISTS "AuthRefreshToken_userId_createdAt_idx" ON "AuthRefreshToken"("userId", "createdAt");
|
|
64
|
+
|
|
65
|
+
-- Foreign keys
|
|
66
|
+
DO $$
|
|
67
|
+
BEGIN
|
|
68
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'UserProfile_userId_fkey') THEN
|
|
69
|
+
ALTER TABLE "UserProfile"
|
|
70
|
+
ADD CONSTRAINT "UserProfile_userId_fkey"
|
|
71
|
+
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
72
|
+
END IF;
|
|
73
|
+
|
|
74
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'UserSettings_userId_fkey') THEN
|
|
75
|
+
ALTER TABLE "UserSettings"
|
|
76
|
+
ADD CONSTRAINT "UserSettings_userId_fkey"
|
|
77
|
+
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
78
|
+
END IF;
|
|
79
|
+
|
|
80
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'AuthIdentity_userId_fkey') THEN
|
|
81
|
+
ALTER TABLE "AuthIdentity"
|
|
82
|
+
ADD CONSTRAINT "AuthIdentity_userId_fkey"
|
|
83
|
+
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
84
|
+
END IF;
|
|
85
|
+
|
|
86
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'AuthCredential_userId_fkey') THEN
|
|
87
|
+
ALTER TABLE "AuthCredential"
|
|
88
|
+
ADD CONSTRAINT "AuthCredential_userId_fkey"
|
|
89
|
+
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
90
|
+
END IF;
|
|
91
|
+
|
|
92
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'AuthRefreshToken_userId_fkey') THEN
|
|
93
|
+
ALTER TABLE "AuthRefreshToken"
|
|
94
|
+
ADD CONSTRAINT "AuthRefreshToken_userId_fkey"
|
|
95
|
+
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
96
|
+
END IF;
|
|
97
|
+
END $$;
|