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.
Files changed (129) hide show
  1. package/package.json +4 -2
  2. package/src/cli/add-options.test.mjs +5 -2
  3. package/src/cli/options.test.mjs +1 -0
  4. package/src/cli/prompt-select.test.mjs +1 -0
  5. package/src/core/docs.test.mjs +80 -40
  6. package/src/core/scaffold.test.mjs +100 -0
  7. package/src/core/validate.test.mjs +1 -0
  8. package/src/modules/accounts.mjs +416 -0
  9. package/src/modules/db-prisma.mjs +23 -55
  10. package/src/modules/dependencies.test.mjs +71 -29
  11. package/src/modules/executor.mjs +3 -2
  12. package/src/modules/executor.test.mjs +631 -500
  13. package/src/modules/files-access.mjs +36 -105
  14. package/src/modules/files-image.mjs +35 -107
  15. package/src/modules/files-local.mjs +15 -6
  16. package/src/modules/files-quotas.mjs +75 -93
  17. package/src/modules/files-s3.mjs +17 -6
  18. package/src/modules/files.mjs +56 -125
  19. package/src/modules/i18n.mjs +17 -121
  20. package/src/modules/idempotency.test.mjs +180 -0
  21. package/src/modules/logger.mjs +0 -9
  22. package/src/modules/probes.test.mjs +204 -0
  23. package/src/modules/queue.mjs +325 -440
  24. package/src/modules/rate-limit.mjs +36 -76
  25. package/src/modules/rbac.mjs +39 -78
  26. package/src/modules/registry.mjs +22 -35
  27. package/src/modules/scheduler.mjs +51 -171
  28. package/src/modules/shared/files-runtime-wiring.mjs +81 -0
  29. package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
  30. package/src/modules/shared/patch-utils.mjs +29 -1
  31. package/src/modules/shared/probes.mjs +235 -0
  32. package/src/modules/sync-integrations.mjs +109 -396
  33. package/src/modules/sync-integrations.test.mjs +141 -0
  34. package/src/run-add-module.test.mjs +154 -0
  35. package/templates/base/README.md +7 -55
  36. package/templates/base/apps/web/src/App.tsx +70 -42
  37. package/templates/base/apps/web/src/probes.ts +61 -0
  38. package/templates/base/apps/web/src/styles.css +86 -25
  39. package/templates/base/package.json +21 -15
  40. package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -281
  41. package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
  42. package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
  43. package/templates/module-fragments/accounts/20_scope.md +29 -0
  44. package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
  45. package/templates/module-fragments/accounts/90_status_planned.md +7 -0
  46. package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
  47. package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
  48. package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
  49. package/templates/module-fragments/swagger/20_scope.md +2 -1
  50. package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
  51. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
  52. package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
  53. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
  54. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
  55. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
  56. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
  57. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
  58. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
  59. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
  60. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
  61. package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
  62. package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
  63. package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
  64. package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
  65. package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
  66. package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
  67. package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
  68. package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
  69. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
  70. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
  71. package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
  72. package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
  73. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
  74. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
  75. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
  76. package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
  77. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
  78. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
  79. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
  80. package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
  81. package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
  82. package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
  83. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
  84. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
  85. package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
  86. package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
  87. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
  88. package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
  89. package/templates/module-presets/files/packages/files/package.json +1 -2
  90. package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
  91. package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
  92. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
  93. package/templates/module-presets/files/packages/files/src/index.ts +2 -1
  94. package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
  95. package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
  96. package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
  97. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
  98. package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
  99. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
  100. package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
  101. package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
  102. package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
  103. package/src/modules/jwt-auth.mjs +0 -390
  104. package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
  105. package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
  106. package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
  107. package/templates/base/docs/AI/PROJECT.md +0 -43
  108. package/templates/base/docs/AI/ROADMAP.md +0 -171
  109. package/templates/base/docs/AI/TASKS.md +0 -60
  110. package/templates/base/docs/AI/VALIDATION.md +0 -31
  111. package/templates/base/docs/README.md +0 -18
  112. package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
  113. package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
  114. package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
  115. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
  116. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
  117. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
  118. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
  119. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
  120. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
  121. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
  122. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
  123. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
  124. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
  125. package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
  126. /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
  127. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
  128. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
  129. /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 PRISMA_AUTH_STORE_CONTENT = `import {
6
- AuthRefreshTokenStore,
7
- } from '@forgeon/auth-api';
8
- import { PrismaService } from '@forgeon/db-prisma';
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
- @Injectable()
12
- export class PrismaAuthRefreshTokenStore implements AuthRefreshTokenStore {
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
- constructor(private readonly prisma: PrismaService) {}
16
-
17
- async saveRefreshTokenHash(subject: string, hash: string): Promise<void> {
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
- async removeRefreshTokenHash(subject: string): Promise<void> {
35
- await this.prisma.user.updateMany({
36
- where: { email: subject },
37
- data: { refreshTokenHash: null },
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
- jwtAuth:
63
- fs.existsSync(path.join(rootDir, 'packages', 'auth-api', 'package.json')) ||
64
- appModuleText.includes("from '@forgeon/auth-api'"),
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 ensureLineAfter(content, anchorLine, lineToInsert) {
75
- if (content.includes(lineToInsert)) {
76
- return content;
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(appModulePath) || !fs.existsSync(schemaPath)) {
113
- return { applied: false, reason: 'app module or prisma schema is missing' };
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
- if (!fs.existsSync(storePath)) {
119
- fs.mkdirSync(path.dirname(storePath), { recursive: true });
120
- fs.writeFileSync(storePath, PRISMA_AUTH_STORE_CONTENT, 'utf8');
121
- changedFiles.add(storePath);
122
- touched = true;
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
- const storeImportLine = "import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';";
136
- if (!appModule.includes(storeImportLine)) {
137
- appModule = ensureLineAfter(
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
- if (appModule !== originalAppModule) {
158
- fs.writeFileSync(appModulePath, `${appModule.trimEnd()}\n`, 'utf8');
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
- if (!fs.existsSync(migrationPath)) {
175
- fs.mkdirSync(path.dirname(migrationPath), { recursive: true });
176
- fs.writeFileSync(migrationPath, PRISMA_AUTH_MIGRATION_CONTENT, 'utf8');
177
- changedFiles.add(migrationPath);
178
- touched = true;
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
- let touched = false;
216
-
217
- let authContracts = fs.readFileSync(authContractsPath, 'utf8').replace(/\r\n/g, '\n');
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 (authContracts !== originalAuthContracts) {
227
- fs.writeFileSync(authContractsPath, `${authContracts.trimEnd()}\n`, 'utf8');
228
- changedFiles.add(authContractsPath);
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 authService = fs.readFileSync(authServicePath, 'utf8').replace(/\r\n/g, '\n');
233
- const originalAuthService = authService;
234
- authService = authService.replace(
235
- /roles: \['user'\],/g,
236
- `roles: ['admin'],
237
- permissions: ['health.rbac'],`,
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 (!authService.includes('permissions: user.permissions,')) {
240
- authService = authService.replace(
241
- ' roles: user.roles,',
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.jwtAuth && detected.rbac) {
116
+ if (detected.accounts && detected.rbac) {
332
117
  summary.push({
333
- feature: 'jwt-auth + rbac',
334
- result: syncJwtRbacClaims({ rootDir, changedFiles }),
118
+ feature: 'accounts + rbac',
119
+ result: syncAccountsRbac(rootDir, changedFiles),
335
120
  });
336
121
  } else {
337
122
  summary.push({
338
- feature: 'jwt-auth + rbac',
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
- const relative = path.relative(rootDir, filePath);
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.
@@ -0,0 +1,7 @@
1
+ ## Current State
2
+
3
+ Status: planned.
4
+
5
+ Notes:
6
+ - public provider add-modules and DB-backed RBAC authz storage are intentionally deferred
7
+ - the future `emails` module is expected to plug into the existing `AccountsEmailPort`
@@ -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
- - `jwt-auth` can extend demo JWT claims with RBAC permissions through `pnpm forgeon:sync-integrations`
16
- - this module does not require `jwt-auth` to install or work for header-based/manual probe checks
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 `jwt-auth` is also installed, the optional `auth-rbac-claims` integration can expose demo permissions inside JWT payloads
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
+
@@ -20,5 +20,6 @@ Manual forbidden-path check:
20
20
 
21
21
  Optional follow-up:
22
22
 
23
- 1. install `jwt-auth` if you want RBAC claims in demo JWT payloads
23
+ 1. install `accounts` if you want RBAC claims in demo JWT payloads
24
24
  2. run `pnpm forgeon:sync-integrations`
25
+
@@ -14,4 +14,5 @@
14
14
  Not included:
15
15
 
16
16
  - no auto-patching of Swagger decorators into feature modules
17
- - no implicit integration with `jwt-auth` or other add-modules
17
+ - no implicit integration with `accounts` or other add-modules
18
+
@@ -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 $$;