create-forgeon 0.3.15 → 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 +4 -2
- 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 +80 -40
- package/src/core/scaffold.test.mjs +100 -0
- package/src/core/validate.test.mjs +1 -0
- package/src/modules/accounts.mjs +416 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +631 -500
- package/src/modules/files-access.mjs +36 -105
- package/src/modules/files-image.mjs +35 -107
- package/src/modules/files-local.mjs +15 -6
- package/src/modules/files-quotas.mjs +75 -93
- package/src/modules/files-s3.mjs +17 -6
- package/src/modules/files.mjs +56 -125
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +180 -0
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +204 -0
- package/src/modules/queue.mjs +325 -440
- package/src/modules/rate-limit.mjs +36 -76
- package/src/modules/rbac.mjs +39 -78
- package/src/modules/registry.mjs +22 -35
- package/src/modules/scheduler.mjs +51 -171
- package/src/modules/shared/files-runtime-wiring.mjs +81 -0
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/patch-utils.mjs +29 -1
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +109 -396
- package/src/modules/sync-integrations.test.mjs +141 -0
- package/src/run-add-module.test.mjs +154 -0
- package/templates/base/README.md +7 -55
- package/templates/base/apps/web/src/App.tsx +70 -42
- package/templates/base/apps/web/src/probes.ts +61 -0
- package/templates/base/apps/web/src/styles.css +86 -25
- package/templates/base/package.json +21 -15
- package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -281
- 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-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- 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/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
- package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
- package/src/modules/jwt-auth.mjs +0 -390
- package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
- package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
- package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
- package/templates/base/docs/AI/PROJECT.md +0 -43
- package/templates/base/docs/AI/ROADMAP.md +0 -171
- package/templates/base/docs/AI/TASKS.md +0 -60
- package/templates/base/docs/AI/VALIDATION.md +0 -31
- package/templates/base/docs/README.md +0 -18
- 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/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
- /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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "forgeon",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"packageManager": "pnpm@10.0.0",
|
|
1
|
+
{
|
|
2
|
+
"name": "forgeon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "pnpm --parallel --filter @forgeon/api --filter @forgeon/web dev",
|
|
8
8
|
"build": "pnpm -r build",
|
|
@@ -12,13 +12,19 @@
|
|
|
12
12
|
"docker:down": "docker compose -f infra/docker/compose.yml down -v"
|
|
13
13
|
},
|
|
14
14
|
"pnpm": {
|
|
15
|
-
"onlyBuiltDependencies": [
|
|
16
|
-
"@nestjs/core",
|
|
17
|
-
"@prisma/client",
|
|
18
|
-
"@prisma/engines",
|
|
19
|
-
"esbuild",
|
|
20
|
-
"prisma"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
"onlyBuiltDependencies": [
|
|
16
|
+
"@nestjs/core",
|
|
17
|
+
"@prisma/client",
|
|
18
|
+
"@prisma/engines",
|
|
19
|
+
"esbuild",
|
|
20
|
+
"prisma"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"forgeon": {
|
|
24
|
+
"diagnostics": {
|
|
25
|
+
"probes": {
|
|
26
|
+
"enabled": true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -2,303 +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';
|
|
5
|
+
const ACCOUNTS_RBAC_MARKERS = {
|
|
6
|
+
start: '<!-- forgeon:accounts:rbac:start -->',
|
|
7
|
+
end: '<!-- forgeon:accounts:rbac:end -->',
|
|
8
|
+
};
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
readonly kind = 'prisma';
|
|
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.';
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
await this.prisma.user.upsert({
|
|
19
|
-
where: { email: subject },
|
|
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
|
-
function resolveAuthPersistenceStrategy(detected) {
|
|
87
|
-
const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
|
|
88
|
-
if (matched.length === 0) {
|
|
89
|
-
return { kind: 'none' };
|
|
90
|
-
}
|
|
91
|
-
if (matched.length > 1) {
|
|
92
|
-
return { kind: 'conflict', strategies: matched };
|
|
93
|
-
}
|
|
94
|
-
return { kind: 'single', strategy: matched[0] };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
98
|
-
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
99
|
-
const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
100
|
-
const storePath = path.join(rootDir, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts');
|
|
101
|
-
const migrationPath = path.join(
|
|
102
|
-
rootDir,
|
|
103
|
-
'apps',
|
|
104
|
-
'api',
|
|
105
|
-
'prisma',
|
|
106
|
-
'migrations',
|
|
107
|
-
'0002_auth_refresh_token_hash',
|
|
108
|
-
'migration.sql',
|
|
109
|
-
);
|
|
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');
|
|
110
42
|
const readmePath = path.join(rootDir, 'README.md');
|
|
111
43
|
|
|
112
|
-
if (!fs.existsSync(
|
|
113
|
-
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' };
|
|
114
46
|
}
|
|
115
47
|
|
|
116
48
|
let touched = false;
|
|
117
49
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
let appModule = fs.readFileSync(appModulePath, 'utf8').replace(/\r\n/g, '\n');
|
|
126
|
-
const originalAppModule = appModule;
|
|
127
|
-
|
|
128
|
-
if (!appModule.includes("AUTH_REFRESH_TOKEN_STORE, authConfig")) {
|
|
129
|
-
appModule = appModule.replace(
|
|
130
|
-
/import\s*\{\s*authConfig,\s*authEnvSchema,\s*ForgeonAuthModule\s*\}\s*from '@forgeon\/auth-api';/m,
|
|
131
|
-
"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[];",
|
|
132
56
|
);
|
|
133
57
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
appModule,
|
|
139
|
-
"import { HealthController } from './health/health.controller';",
|
|
140
|
-
storeImportLine,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!appModule.includes('refreshTokenStoreProvider')) {
|
|
145
|
-
appModule = appModule.replace(
|
|
146
|
-
/ForgeonAuthModule\.register\(\),/m,
|
|
147
|
-
`ForgeonAuthModule.register({
|
|
148
|
-
imports: [DbPrismaModule],
|
|
149
|
-
refreshTokenStoreProvider: {
|
|
150
|
-
provide: AUTH_REFRESH_TOKEN_STORE,
|
|
151
|
-
useClass: PrismaAuthRefreshTokenStore,
|
|
152
|
-
},
|
|
153
|
-
}),`,
|
|
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[];",
|
|
154
62
|
);
|
|
155
63
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
changedFiles.add(appModulePath);
|
|
160
|
-
touched = true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let schema = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
|
|
164
|
-
const originalSchema = schema;
|
|
165
|
-
if (!schema.includes('refreshTokenHash')) {
|
|
166
|
-
schema = schema.replace(/email\s+String\s+@unique/g, 'email String @unique\n refreshTokenHash String?');
|
|
167
|
-
}
|
|
168
|
-
if (schema !== originalSchema) {
|
|
169
|
-
fs.writeFileSync(schemaPath, `${schema.trimEnd()}\n`, 'utf8');
|
|
170
|
-
changedFiles.add(schemaPath);
|
|
64
|
+
if (contracts !== originalContracts) {
|
|
65
|
+
fs.writeFileSync(contractsPath, `${contracts.trimEnd()}\n`, 'utf8');
|
|
66
|
+
changedFiles.add(contractsPath);
|
|
171
67
|
touched = true;
|
|
172
68
|
}
|
|
173
69
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (fs.existsSync(readmePath)) {
|
|
182
|
-
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
183
|
-
const originalReadme = readme;
|
|
184
|
-
readme = readme.replace(
|
|
185
|
-
'- refresh token persistence: disabled by default (stateless mode; enable it later through a `db-adapter` provider + integration sync)',
|
|
186
|
-
'- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
|
|
187
|
-
);
|
|
188
|
-
readme = readme.replace(
|
|
189
|
-
/- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to wire auth persistence to the active DB adapter implementation\./m,
|
|
190
|
-
'- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
|
|
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[];",
|
|
191
76
|
);
|
|
192
|
-
if (readme !== originalReadme) {
|
|
193
|
-
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
194
|
-
changedFiles.add(readmePath);
|
|
195
|
-
touched = true;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!touched) {
|
|
200
|
-
return { applied: false, reason: 'already synced' };
|
|
201
|
-
}
|
|
202
|
-
return { applied: true };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function syncJwtRbacClaims({ rootDir, changedFiles }) {
|
|
206
|
-
const authContractsPath = path.join(rootDir, 'packages', 'auth-contracts', 'src', 'index.ts');
|
|
207
|
-
const authServicePath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.service.ts');
|
|
208
|
-
const authControllerPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.controller.ts');
|
|
209
|
-
const readmePath = path.join(rootDir, 'README.md');
|
|
210
|
-
|
|
211
|
-
if (!fs.existsSync(authContractsPath) || !fs.existsSync(authServicePath) || !fs.existsSync(authControllerPath)) {
|
|
212
|
-
return { applied: false, reason: 'auth package files are missing' };
|
|
213
77
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const originalAuthContracts = authContracts;
|
|
219
|
-
if (!authContracts.includes('permissions?: string[];')) {
|
|
220
|
-
authContracts = authContracts.replace(
|
|
221
|
-
' roles: string[];',
|
|
222
|
-
` roles: string[];
|
|
223
|
-
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}",
|
|
224
82
|
);
|
|
225
83
|
}
|
|
226
|
-
if (
|
|
227
|
-
fs.writeFileSync(
|
|
228
|
-
changedFiles.add(
|
|
84
|
+
if (authTypes !== originalAuthTypes) {
|
|
85
|
+
fs.writeFileSync(authTypesPath, `${authTypes.trimEnd()}\n`, 'utf8');
|
|
86
|
+
changedFiles.add(authTypesPath);
|
|
229
87
|
touched = true;
|
|
230
88
|
}
|
|
231
89
|
|
|
232
|
-
let
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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,
|
|
238
97
|
);
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
` roles: user.roles,
|
|
243
|
-
permissions: user.permissions,`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
if (!authService.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
|
|
247
|
-
authService = authService.replace(
|
|
248
|
-
" roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
|
|
249
|
-
` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
|
|
250
|
-
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
if (!authService.includes('demoPermissions: [')) {
|
|
254
|
-
authService = authService.replace(
|
|
255
|
-
" demoEmail: this.configService.demoEmail,",
|
|
256
|
-
` demoEmail: this.configService.demoEmail,
|
|
257
|
-
demoPermissions: ['health.rbac'],`,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
if (authService !== originalAuthService) {
|
|
261
|
-
fs.writeFileSync(authServicePath, `${authService.trimEnd()}\n`, 'utf8');
|
|
262
|
-
changedFiles.add(authServicePath);
|
|
98
|
+
if (readme !== originalReadme) {
|
|
99
|
+
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
100
|
+
changedFiles.add(readmePath);
|
|
263
101
|
touched = true;
|
|
264
102
|
}
|
|
265
103
|
|
|
266
|
-
let authController = fs.readFileSync(authControllerPath, 'utf8').replace(/\r\n/g, '\n');
|
|
267
|
-
const originalAuthController = authController;
|
|
268
|
-
if (!authController.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
|
|
269
|
-
authController = authController.replace(
|
|
270
|
-
" roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
|
|
271
|
-
` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
|
|
272
|
-
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
if (authController !== originalAuthController) {
|
|
276
|
-
fs.writeFileSync(authControllerPath, `${authController.trimEnd()}\n`, 'utf8');
|
|
277
|
-
changedFiles.add(authControllerPath);
|
|
278
|
-
touched = true;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (fs.existsSync(readmePath)) {
|
|
282
|
-
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
283
|
-
const originalReadme = readme;
|
|
284
|
-
if (!readme.includes('- RBAC integration: demo auth tokens include `health.rbac` permission')) {
|
|
285
|
-
const marker = 'Default demo credentials:';
|
|
286
|
-
if (readme.includes(marker)) {
|
|
287
|
-
readme = readme.replace(
|
|
288
|
-
marker,
|
|
289
|
-
`- RBAC integration: demo auth tokens include \`health.rbac\` permission
|
|
290
|
-
|
|
291
|
-
Default demo credentials:`,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (readme !== originalReadme) {
|
|
296
|
-
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
297
|
-
changedFiles.add(readmePath);
|
|
298
|
-
touched = true;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
104
|
if (!touched) {
|
|
303
105
|
return { applied: false, reason: 'already synced' };
|
|
304
106
|
}
|
|
@@ -307,35 +109,18 @@ Default demo credentials:`,
|
|
|
307
109
|
|
|
308
110
|
function run() {
|
|
309
111
|
const rootDir = process.cwd();
|
|
310
|
-
const changedFiles = new Set();
|
|
311
112
|
const detected = detectModules(rootDir);
|
|
113
|
+
const changedFiles = new Set();
|
|
312
114
|
const summary = [];
|
|
313
|
-
const authPersistence = resolveAuthPersistenceStrategy(detected);
|
|
314
|
-
|
|
315
|
-
if (detected.jwtAuth && authPersistence.kind === 'single') {
|
|
316
|
-
summary.push({
|
|
317
|
-
feature: `jwt-auth + db-adapter (current provider: ${authPersistence.strategy.providerLabel})`,
|
|
318
|
-
result: authPersistence.strategy.apply({ rootDir, changedFiles }),
|
|
319
|
-
});
|
|
320
|
-
} else {
|
|
321
|
-
const reason =
|
|
322
|
-
authPersistence.kind === 'conflict'
|
|
323
|
-
? 'multiple db-adapter providers detected'
|
|
324
|
-
: 'required components are not both available';
|
|
325
|
-
summary.push({
|
|
326
|
-
feature: 'jwt-auth + db-adapter (current provider: db-prisma)',
|
|
327
|
-
result: { applied: false, reason },
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
115
|
|
|
331
|
-
if (detected.
|
|
116
|
+
if (detected.accounts && detected.rbac) {
|
|
332
117
|
summary.push({
|
|
333
|
-
feature: '
|
|
334
|
-
result:
|
|
118
|
+
feature: 'accounts + rbac',
|
|
119
|
+
result: syncAccountsRbac(rootDir, changedFiles),
|
|
335
120
|
});
|
|
336
121
|
} else {
|
|
337
122
|
summary.push({
|
|
338
|
-
feature: '
|
|
123
|
+
feature: 'accounts + rbac',
|
|
339
124
|
result: { applied: false, reason: 'required components are not both available' },
|
|
340
125
|
});
|
|
341
126
|
}
|
|
@@ -352,10 +137,9 @@ function run() {
|
|
|
352
137
|
if (changedFiles.size > 0) {
|
|
353
138
|
console.log('- changed files:');
|
|
354
139
|
for (const filePath of [...changedFiles].sort()) {
|
|
355
|
-
|
|
356
|
-
console.log(` - ${relative}`);
|
|
140
|
+
console.log(` - ${path.relative(rootDir, filePath)}`);
|
|
357
141
|
}
|
|
358
142
|
}
|
|
359
143
|
}
|
|
360
144
|
|
|
361
|
-
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 $$;
|