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.
Files changed (107) hide show
  1. package/package.json +1 -1
  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 +1 -0
  6. package/src/core/scaffold.test.mjs +1 -0
  7. package/src/core/validate.test.mjs +1 -0
  8. package/src/modules/accounts.mjs +416 -0
  9. package/src/modules/dependencies.test.mjs +71 -29
  10. package/src/modules/executor.mjs +3 -2
  11. package/src/modules/executor.test.mjs +512 -477
  12. package/src/modules/files-access.mjs +9 -7
  13. package/src/modules/files-image.mjs +9 -7
  14. package/src/modules/files-local.mjs +15 -6
  15. package/src/modules/files-quotas.mjs +8 -6
  16. package/src/modules/files-s3.mjs +17 -6
  17. package/src/modules/files.mjs +21 -21
  18. package/src/modules/idempotency.test.mjs +13 -7
  19. package/src/modules/probes.test.mjs +4 -2
  20. package/src/modules/queue.mjs +9 -6
  21. package/src/modules/rate-limit.mjs +14 -10
  22. package/src/modules/rbac.mjs +12 -11
  23. package/src/modules/registry.mjs +22 -35
  24. package/src/modules/scheduler.mjs +9 -6
  25. package/src/modules/shared/files-runtime-wiring.mjs +81 -0
  26. package/src/modules/shared/patch-utils.mjs +29 -1
  27. package/src/modules/sync-integrations.mjs +102 -422
  28. package/src/modules/sync-integrations.test.mjs +32 -111
  29. package/src/run-add-module.test.mjs +1 -0
  30. package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -325
  31. package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
  32. package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
  33. package/templates/module-fragments/accounts/20_scope.md +29 -0
  34. package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
  35. package/templates/module-fragments/accounts/90_status_planned.md +7 -0
  36. package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
  37. package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
  38. package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
  39. package/templates/module-fragments/swagger/20_scope.md +2 -1
  40. package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
  41. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
  42. package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
  43. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
  44. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
  45. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
  46. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
  47. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
  48. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
  49. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
  50. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
  51. package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
  52. package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
  53. package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
  54. package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
  55. package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
  56. package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
  57. package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
  58. package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
  59. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
  60. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
  61. package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
  62. package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
  63. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
  64. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
  65. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
  66. package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
  67. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
  68. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
  69. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
  70. package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
  71. package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
  72. package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
  73. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
  74. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
  75. package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
  76. package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
  77. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
  78. package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
  79. package/templates/module-presets/files/packages/files/package.json +1 -2
  80. package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
  81. package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
  82. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
  83. package/templates/module-presets/files/packages/files/src/index.ts +2 -1
  84. package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
  85. package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
  86. package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
  87. package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
  88. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
  89. package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
  90. package/src/modules/jwt-auth.mjs +0 -271
  91. package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
  92. package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
  93. package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
  94. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
  95. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
  96. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
  97. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
  98. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
  99. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
  100. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
  101. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
  102. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
  103. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
  104. /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
  105. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
  106. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
  107. /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
@@ -1,12 +1,136 @@
1
- import { Module } from '@nestjs/common';
1
+ import {
2
+ DynamicModule,
3
+ Injectable,
4
+ Module,
5
+ ModuleMetadata,
6
+ Provider,
7
+ ServiceUnavailableException,
8
+ } from '@nestjs/common';
9
+ import type {
10
+ FilesBlobCreateInput,
11
+ FilesPersistencePort,
12
+ FilesRecordCreateInput,
13
+ FilesStorageAdapter,
14
+ FilesVariantCreateInput,
15
+ } from './files.ports';
16
+ import { FILES_PERSISTENCE_PORT, FILES_STORAGE_ADAPTER } from './files.ports';
2
17
  import { FilesController } from './files.controller';
3
18
  import { FilesConfigModule } from './files-config.module';
4
19
  import { FilesService } from './files.service';
5
20
 
21
+ export interface ForgeonFilesModuleOptions {
22
+ imports?: ModuleMetadata['imports'];
23
+ persistenceProvider?: Provider;
24
+ storageAdapterProvider?: Provider;
25
+ }
26
+
27
+ @Injectable()
28
+ class MissingFilesPersistencePort implements FilesPersistencePort {
29
+ private unconfigured(): never {
30
+ throw new ServiceUnavailableException(
31
+ 'Files persistence provider is not configured. Install/add a db-adapter provider and wire FILES_PERSISTENCE_PORT.',
32
+ );
33
+ }
34
+
35
+ async createFileRecord(_data: FilesRecordCreateInput): Promise<{ id: string; publicId: string }> {
36
+ return this.unconfigured();
37
+ }
38
+
39
+ async deleteFileRecordById(_id: string): Promise<void> {
40
+ return this.unconfigured();
41
+ }
42
+
43
+ async deleteFileRecordByPublicId(_publicId: string): Promise<void> {
44
+ return this.unconfigured();
45
+ }
46
+
47
+ async findFileRecordWithVariantKeys(_publicId: string) {
48
+ return this.unconfigured();
49
+ }
50
+
51
+ async findFileRecordForDelete(_publicId: string) {
52
+ return this.unconfigured();
53
+ }
54
+
55
+ async findFileRecordForDownload(_publicId: string) {
56
+ return this.unconfigured();
57
+ }
58
+
59
+ async countOwnerUsage(_ownerType: string, _ownerId: string) {
60
+ return this.unconfigured();
61
+ }
62
+
63
+ async findBlobRef(_hash: string, _size: number, _mimeType: string, _storageDriver: string) {
64
+ return this.unconfigured();
65
+ }
66
+
67
+ async createBlob(_data: FilesBlobCreateInput) {
68
+ return this.unconfigured();
69
+ }
70
+
71
+ async createVariants(_data: FilesVariantCreateInput[]): Promise<void> {
72
+ return this.unconfigured();
73
+ }
74
+
75
+ async findBlobById(_id: string) {
76
+ return this.unconfigured();
77
+ }
78
+
79
+ async deleteBlobIfUnreferenced(_id: string) {
80
+ return this.unconfigured();
81
+ }
82
+ }
83
+
84
+ @Injectable()
85
+ class MissingFilesStorageAdapter implements FilesStorageAdapter {
86
+ readonly driver = 'unconfigured';
87
+
88
+ private unconfigured(): never {
89
+ throw new ServiceUnavailableException(
90
+ 'Files storage adapter is not configured. Install/add a files-storage-adapter provider and wire FILES_STORAGE_ADAPTER.',
91
+ );
92
+ }
93
+
94
+ async put(_buffer: Buffer, _fileName: string): Promise<{ storageKey: string }> {
95
+ return this.unconfigured();
96
+ }
97
+
98
+ async open(_storageKey: string) {
99
+ return this.unconfigured();
100
+ }
101
+
102
+ async delete(_storageKey: string): Promise<void> {
103
+ return this.unconfigured();
104
+ }
105
+ }
106
+
6
107
  @Module({
7
108
  imports: [FilesConfigModule],
8
109
  controllers: [FilesController],
9
110
  providers: [FilesService],
10
111
  exports: [FilesConfigModule, FilesService],
11
112
  })
12
- export class ForgeonFilesModule {}
113
+ export class ForgeonFilesModule {
114
+ static register(options: ForgeonFilesModuleOptions = {}): DynamicModule {
115
+ const persistenceProvider =
116
+ options.persistenceProvider ??
117
+ ({
118
+ provide: FILES_PERSISTENCE_PORT,
119
+ useClass: MissingFilesPersistencePort,
120
+ } satisfies Provider);
121
+
122
+ const storageAdapterProvider =
123
+ options.storageAdapterProvider ??
124
+ ({
125
+ provide: FILES_STORAGE_ADAPTER,
126
+ useClass: MissingFilesStorageAdapter,
127
+ } satisfies Provider);
128
+
129
+ return {
130
+ module: ForgeonFilesModule,
131
+ imports: [...(options.imports ?? [])],
132
+ providers: [persistenceProvider, storageAdapterProvider],
133
+ exports: [FILES_PERSISTENCE_PORT, FILES_STORAGE_ADAPTER],
134
+ };
135
+ }
136
+ }
@@ -5,5 +5,6 @@ export * from './files-config.loader';
5
5
  export * from './files-config.module';
6
6
  export * from './files-config.service';
7
7
  export * from './files-env.schema';
8
+ export * from './files.ports';
8
9
  export * from './files.service';
9
- export * from './files.types';
10
+ export * from './files.types';
@@ -0,0 +1,18 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { FilesLocalConfigModule } from './files-local-config.module';
3
+ import { LocalFilesStorageAdapter } from './local-files-storage.adapter';
4
+
5
+ const FORGEON_FILES_STORAGE_ADAPTER = 'FORGEON_FILES_STORAGE_ADAPTER';
6
+
7
+ @Module({
8
+ imports: [FilesLocalConfigModule],
9
+ providers: [
10
+ LocalFilesStorageAdapter,
11
+ {
12
+ provide: FORGEON_FILES_STORAGE_ADAPTER,
13
+ useExisting: LocalFilesStorageAdapter,
14
+ },
15
+ ],
16
+ exports: [FilesLocalConfigModule, LocalFilesStorageAdapter, FORGEON_FILES_STORAGE_ADAPTER],
17
+ })
18
+ export class ForgeonFilesLocalStorageModule {}
@@ -2,3 +2,5 @@ export * from './files-local-config.loader';
2
2
  export * from './files-local-config.module';
3
3
  export * from './files-local-config.service';
4
4
  export * from './files-local-env.schema';
5
+ export * from './forgeon-files-local-storage.module';
6
+ export * from './local-files-storage.adapter';
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import { promises as fsPromises } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { Readable } from 'node:stream';
5
+ import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
6
+ import { FilesLocalConfigService } from './files-local-config.service';
7
+
8
+ @Injectable()
9
+ export class LocalFilesStorageAdapter {
10
+ readonly driver = 'local';
11
+
12
+ constructor(private readonly configService: FilesLocalConfigService) {}
13
+
14
+ async put(buffer: Buffer, storageKey: string): Promise<{ storageKey: string }> {
15
+ const absoluteRoot = path.resolve(process.cwd(), this.configService.rootDir);
16
+ const absolutePath = path.join(absoluteRoot, storageKey);
17
+
18
+ await fsPromises.mkdir(absoluteRoot, { recursive: true });
19
+ await fsPromises.writeFile(absolutePath, buffer);
20
+
21
+ return { storageKey };
22
+ }
23
+
24
+ async open(storageKey: string): Promise<Readable> {
25
+ const absoluteRoot = path.resolve(process.cwd(), this.configService.rootDir);
26
+ const absolutePath = path.join(absoluteRoot, storageKey);
27
+ if (!fs.existsSync(absolutePath)) {
28
+ throw new NotFoundException('File content not found');
29
+ }
30
+ return fs.createReadStream(absolutePath);
31
+ }
32
+
33
+ async delete(storageKey: string): Promise<void> {
34
+ const absoluteRoot = path.resolve(process.cwd(), this.configService.rootDir);
35
+ const absolutePath = path.join(absoluteRoot, storageKey);
36
+
37
+ if (!fs.existsSync(absolutePath)) {
38
+ return;
39
+ }
40
+
41
+ try {
42
+ await fsPromises.unlink(absolutePath);
43
+ } catch (error) {
44
+ throw new InternalServerErrorException({
45
+ message: 'Failed to delete file content',
46
+ details: {
47
+ storageKey,
48
+ reason: error instanceof Error ? error.message : 'unknown',
49
+ },
50
+ });
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,18 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { FilesS3ConfigModule } from './files-s3-config.module';
3
+ import { S3FilesStorageAdapter } from './s3-files-storage.adapter';
4
+
5
+ const FORGEON_FILES_STORAGE_ADAPTER = 'FORGEON_FILES_STORAGE_ADAPTER';
6
+
7
+ @Module({
8
+ imports: [FilesS3ConfigModule],
9
+ providers: [
10
+ S3FilesStorageAdapter,
11
+ {
12
+ provide: FORGEON_FILES_STORAGE_ADAPTER,
13
+ useExisting: S3FilesStorageAdapter,
14
+ },
15
+ ],
16
+ exports: [FilesS3ConfigModule, S3FilesStorageAdapter, FORGEON_FILES_STORAGE_ADAPTER],
17
+ })
18
+ export class ForgeonFilesS3StorageModule {}
@@ -2,3 +2,5 @@ export * from './files-s3-config.loader';
2
2
  export * from './files-s3-config.module';
3
3
  export * from './files-s3-config.service';
4
4
  export * from './files-s3-env.schema';
5
+ export * from './forgeon-files-s3-storage.module';
6
+ export * from './s3-files-storage.adapter';
@@ -0,0 +1,130 @@
1
+ import { Readable } from 'node:stream';
2
+ import { Injectable, InternalServerErrorException, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
3
+ import { FilesS3ConfigService } from './files-s3-config.service';
4
+
5
+ type S3ModuleLike = {
6
+ S3Client: new (config: Record<string, unknown>) => {
7
+ send: (command: unknown) => Promise<{
8
+ Body?: unknown;
9
+ }>;
10
+ };
11
+ PutObjectCommand: new (input: Record<string, unknown>) => unknown;
12
+ GetObjectCommand: new (input: Record<string, unknown>) => unknown;
13
+ DeleteObjectCommand: new (input: Record<string, unknown>) => unknown;
14
+ };
15
+
16
+ @Injectable()
17
+ export class S3FilesStorageAdapter {
18
+ readonly driver = 's3';
19
+
20
+ private s3Client:
21
+ | {
22
+ send: (command: unknown) => Promise<{
23
+ Body?: unknown;
24
+ }>;
25
+ }
26
+ | null = null;
27
+
28
+ constructor(private readonly configService: FilesS3ConfigService) {}
29
+
30
+ async put(buffer: Buffer, storageKey: string): Promise<{ storageKey: string }> {
31
+ const { PutObjectCommand } = await this.loadS3Module();
32
+ const client = await this.getS3Client();
33
+
34
+ await client.send(
35
+ new PutObjectCommand({
36
+ Bucket: this.configService.bucket,
37
+ Key: storageKey,
38
+ Body: buffer,
39
+ }),
40
+ );
41
+
42
+ return { storageKey };
43
+ }
44
+
45
+ async open(storageKey: string): Promise<Readable> {
46
+ const { GetObjectCommand } = await this.loadS3Module();
47
+ const client = await this.getS3Client();
48
+
49
+ const response = await client.send(
50
+ new GetObjectCommand({
51
+ Bucket: this.configService.bucket,
52
+ Key: storageKey,
53
+ }),
54
+ );
55
+ if (!response.Body) {
56
+ throw new NotFoundException('File content not found');
57
+ }
58
+
59
+ return this.toNodeReadable(response.Body);
60
+ }
61
+
62
+ async delete(storageKey: string): Promise<void> {
63
+ const { DeleteObjectCommand } = await this.loadS3Module();
64
+ const client = await this.getS3Client();
65
+ await client.send(
66
+ new DeleteObjectCommand({
67
+ Bucket: this.configService.bucket,
68
+ Key: storageKey,
69
+ }),
70
+ );
71
+ }
72
+
73
+ private async getS3Client(): Promise<{
74
+ send: (command: unknown) => Promise<{
75
+ Body?: unknown;
76
+ }>;
77
+ }> {
78
+ if (this.s3Client) {
79
+ return this.s3Client;
80
+ }
81
+
82
+ const { S3Client } = await this.loadS3Module();
83
+ const providerPreset = this.configService.providerPreset;
84
+ const endpoint = this.configService.endpoint;
85
+
86
+ if (providerPreset !== 'aws' && !endpoint) {
87
+ throw new ServiceUnavailableException(
88
+ `files-s3 adapter endpoint is required for provider preset "${providerPreset}".`,
89
+ );
90
+ }
91
+
92
+ this.s3Client = new S3Client({
93
+ region: this.configService.region,
94
+ ...(endpoint ? { endpoint } : {}),
95
+ forcePathStyle: this.configService.forcePathStyle,
96
+ maxAttempts: this.configService.maxAttempts,
97
+ credentials: {
98
+ accessKeyId: this.configService.accessKeyId,
99
+ secretAccessKey: this.configService.secretAccessKey,
100
+ },
101
+ });
102
+ return this.s3Client;
103
+ }
104
+
105
+ private toNodeReadable(body: unknown): Readable {
106
+ if (body instanceof Readable) {
107
+ return body;
108
+ }
109
+ if (typeof body === 'string' || Buffer.isBuffer(body) || body instanceof Uint8Array) {
110
+ return Readable.from(body);
111
+ }
112
+ if (
113
+ typeof body === 'object' &&
114
+ body !== null &&
115
+ typeof (body as { transformToWebStream?: unknown }).transformToWebStream === 'function'
116
+ ) {
117
+ return Readable.fromWeb(
118
+ (body as { transformToWebStream: () => unknown }).transformToWebStream() as never,
119
+ );
120
+ }
121
+ throw new InternalServerErrorException('Unsupported S3 response body type');
122
+ }
123
+
124
+ private async loadS3Module(): Promise<S3ModuleLike> {
125
+ const importModule = new Function('specifier', 'return import(specifier)') as (
126
+ specifier: string,
127
+ ) => Promise<S3ModuleLike>;
128
+ return importModule('@aws-sdk/client-s3');
129
+ }
130
+ }
@@ -1,271 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
- import {
5
- ensureBuildSteps,
6
- ensureDependency,
7
- ensureLineAfter,
8
- ensureLineBefore,
9
- upsertEnvLines,
10
- } from './shared/patch-utils.mjs';
11
- import { patchAppModuleRegistration, patchHealthControllerServiceProbe } from './shared/nest-runtime-wiring.mjs';
12
- import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
13
-
14
- const JWT_AUTH_PERSISTENCE_MARKERS = {
15
- start: '<!-- forgeon:jwt-auth:persistence:start -->',
16
- end: '<!-- forgeon:jwt-auth:persistence:end -->',
17
- };
18
-
19
- const JWT_AUTH_RBAC_MARKERS = {
20
- start: '<!-- forgeon:jwt-auth:rbac:start -->',
21
- end: '<!-- forgeon:jwt-auth:rbac:end -->',
22
- };
23
-
24
- const JWT_AUTH_DEFAULT_PERSISTENCE_BLOCK = [
25
- '- refresh token persistence: disabled by default (stateless mode; enable it later through a `db-adapter` provider + integration sync)',
26
- '- to enable persistence later:',
27
- ' 1. install a DB adapter provider first (current provider: `create-forgeon add db-prisma --project .`);',
28
- ' 2. run `pnpm forgeon:sync-integrations` to wire auth persistence to the active DB adapter implementation.',
29
- ].join('\n');
30
-
31
- const JWT_AUTH_DEFAULT_RBAC_BLOCK =
32
- '- RBAC integration: not enabled by default (add `rbac` and run `pnpm forgeon:sync-integrations` to include demo `health.rbac` claims).';
33
-
34
- function copyFromPreset(packageRoot, targetRoot, relativePath) {
35
- const source = path.join(packageRoot, 'templates', 'module-presets', 'jwt-auth', relativePath);
36
- if (!fs.existsSync(source)) {
37
- throw new Error(`Missing jwt-auth preset template: ${source}`);
38
- }
39
- const destination = path.join(targetRoot, relativePath);
40
- fs.mkdirSync(path.dirname(destination), { recursive: true });
41
- copyRecursive(source, destination);
42
- }
43
-
44
- function patchApiPackage(targetRoot) {
45
- const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
46
- if (!fs.existsSync(packagePath)) {
47
- return;
48
- }
49
-
50
- const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
51
- ensureDependency(packageJson, '@forgeon/auth-api', 'workspace:*');
52
- ensureDependency(packageJson, '@forgeon/auth-contracts', 'workspace:*');
53
-
54
- ensureBuildSteps(packageJson, 'predev', [
55
- 'pnpm --filter @forgeon/auth-contracts build',
56
- 'pnpm --filter @forgeon/auth-api build',
57
- ]);
58
-
59
- writeJson(packagePath, packageJson);
60
- }
61
-
62
- function patchAppModule(targetRoot) {
63
- patchAppModuleRegistration(targetRoot, {
64
- importLine: "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
65
- loadItem: 'authConfig',
66
- envSchema: 'authEnvSchema',
67
- moduleLine: ' ForgeonAuthModule.register(),',
68
- beforeAnchors: [
69
- ' ForgeonI18nModule.register({',
70
- ],
71
- afterAnchors: [
72
- ' DbPrismaModule,',
73
- ' ForgeonLoggerModule,',
74
- ' ForgeonSwaggerModule,',
75
- ],
76
- fallbackAnchor: ' CoreErrorsModule,',
77
- });
78
- }
79
-
80
- function patchHealthController(targetRoot, probeTargets) {
81
- patchHealthControllerServiceProbe(targetRoot, probeTargets, {
82
- importLine: "import { AuthService } from '@forgeon/auth-api';",
83
- constructorMember: 'private readonly authService: AuthService',
84
- routePath: 'auth',
85
- methodName: 'getAuthProbe',
86
- serviceCall: 'this.authService.getProbeStatus()',
87
- beforeNeedles: ["@Post('db')"],
88
- beforeNeedle: 'private translate(',
89
- });
90
- }
91
-
92
- function registerWebProbe(targetRoot, probeTargets) {
93
- ensureWebProbeDefinition({
94
- targetRoot,
95
- probeTargets,
96
- definition: {
97
- id: 'auth',
98
- title: 'JWT Auth',
99
- buttonLabel: 'Check JWT auth probe',
100
- resultTitle: 'Auth probe response',
101
- path: '/health/auth',
102
- },
103
- });
104
- }
105
-
106
- function patchApiDockerfile(targetRoot) {
107
- const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
108
- if (!fs.existsSync(dockerfilePath)) {
109
- return;
110
- }
111
-
112
- let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
113
- const packageAnchors = [
114
- 'COPY packages/swagger/package.json packages/swagger/package.json',
115
- 'COPY packages/logger/package.json packages/logger/package.json',
116
- 'COPY packages/i18n/package.json packages/i18n/package.json',
117
- 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
118
- 'COPY packages/core/package.json packages/core/package.json',
119
- ];
120
- const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
121
- content = ensureLineAfter(
122
- content,
123
- packageAnchor,
124
- 'COPY packages/auth-contracts/package.json packages/auth-contracts/package.json',
125
- );
126
- content = ensureLineAfter(
127
- content,
128
- 'COPY packages/auth-contracts/package.json packages/auth-contracts/package.json',
129
- 'COPY packages/auth-api/package.json packages/auth-api/package.json',
130
- );
131
-
132
- const sourceAnchors = [
133
- 'COPY packages/swagger packages/swagger',
134
- 'COPY packages/logger packages/logger',
135
- 'COPY packages/i18n packages/i18n',
136
- 'COPY packages/db-prisma packages/db-prisma',
137
- 'COPY packages/core packages/core',
138
- ];
139
- const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
140
- content = ensureLineAfter(content, sourceAnchor, 'COPY packages/auth-contracts packages/auth-contracts');
141
- content = ensureLineAfter(
142
- content,
143
- 'COPY packages/auth-contracts packages/auth-contracts',
144
- 'COPY packages/auth-api packages/auth-api',
145
- );
146
-
147
- content = content
148
- .replace(/^RUN pnpm --filter @forgeon\/auth-contracts build\r?\n?/gm, '')
149
- .replace(/^RUN pnpm --filter @forgeon\/auth-api build\r?\n?/gm, '');
150
-
151
- const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
152
- ? 'RUN pnpm --filter @forgeon/api prisma:generate'
153
- : 'RUN pnpm --filter @forgeon/api build';
154
- content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/auth-contracts build');
155
- content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/auth-api build');
156
-
157
- fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
158
- }
159
-
160
- function patchCompose(targetRoot) {
161
- const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
162
- if (!fs.existsSync(composePath)) {
163
- return;
164
- }
165
-
166
- let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
167
- if (!content.includes('JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET}')) {
168
- content = content.replace(
169
- /^(\s+API_PREFIX:.*)$/m,
170
- `$1
171
- JWT_ACCESS_SECRET: \${JWT_ACCESS_SECRET}
172
- JWT_ACCESS_EXPIRES_IN: \${JWT_ACCESS_EXPIRES_IN}
173
- JWT_REFRESH_SECRET: \${JWT_REFRESH_SECRET}
174
- JWT_REFRESH_EXPIRES_IN: \${JWT_REFRESH_EXPIRES_IN}
175
- AUTH_BCRYPT_ROUNDS: \${AUTH_BCRYPT_ROUNDS}
176
- AUTH_DEMO_EMAIL: \${AUTH_DEMO_EMAIL}
177
- AUTH_DEMO_PASSWORD: \${AUTH_DEMO_PASSWORD}`,
178
- );
179
- }
180
-
181
- fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
182
- }
183
-
184
- function patchReadme(targetRoot) {
185
- const readmePath = path.join(targetRoot, 'README.md');
186
- if (!fs.existsSync(readmePath)) {
187
- return;
188
- }
189
-
190
- const section = [
191
- '## JWT Auth Module',
192
- '',
193
- 'The jwt-auth add-module provides:',
194
- '- `@forgeon/auth-contracts` shared auth routes/types/error codes',
195
- '- `@forgeon/auth-api` Nest auth module (`login`, `refresh`, `logout`, `me`)',
196
- '- JWT guard + passport strategy',
197
- '- auth probe endpoint: `GET /api/health/auth`',
198
- '',
199
- 'Current mode:',
200
- JWT_AUTH_PERSISTENCE_MARKERS.start,
201
- JWT_AUTH_DEFAULT_PERSISTENCE_BLOCK,
202
- JWT_AUTH_PERSISTENCE_MARKERS.end,
203
- JWT_AUTH_RBAC_MARKERS.start,
204
- JWT_AUTH_DEFAULT_RBAC_BLOCK,
205
- JWT_AUTH_RBAC_MARKERS.end,
206
- '',
207
- 'Default demo credentials:',
208
- '- `AUTH_DEMO_EMAIL=demo@forgeon.local`',
209
- '- `AUTH_DEMO_PASSWORD=forgeon-demo-password`',
210
- '',
211
- 'Default routes:',
212
- '- `POST /api/auth/login`',
213
- '- `POST /api/auth/refresh`',
214
- '- `POST /api/auth/logout`',
215
- '- `GET /api/auth/me`',
216
- ].join('\n');
217
-
218
- let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
219
- const sectionHeading = '## JWT Auth Module';
220
- if (content.includes(sectionHeading)) {
221
- const start = content.indexOf(sectionHeading);
222
- const tail = content.slice(start + sectionHeading.length);
223
- const nextHeadingMatch = tail.match(/\n##\s+/);
224
- const end =
225
- nextHeadingMatch && nextHeadingMatch.index !== undefined
226
- ? start + sectionHeading.length + nextHeadingMatch.index + 1
227
- : content.length;
228
- content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
229
- } else if (content.includes('## Prisma In Docker Start')) {
230
- content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
231
- } else {
232
- content = `${content.trimEnd()}\n\n${section}\n`;
233
- }
234
-
235
- fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
236
- }
237
-
238
- export function applyJwtAuthModule({ packageRoot, targetRoot }) {
239
- copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-contracts'));
240
- copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-api'));
241
-
242
- const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'jwt-auth' });
243
-
244
- patchApiPackage(targetRoot);
245
- patchAppModule(targetRoot);
246
- patchHealthController(targetRoot, probeTargets);
247
- registerWebProbe(targetRoot, probeTargets);
248
- patchApiDockerfile(targetRoot);
249
- patchCompose(targetRoot);
250
- patchReadme(targetRoot);
251
-
252
- upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
253
- 'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
254
- 'JWT_ACCESS_EXPIRES_IN=15m',
255
- 'JWT_REFRESH_SECRET=forgeon-refresh-secret-change-me',
256
- 'JWT_REFRESH_EXPIRES_IN=7d',
257
- 'AUTH_BCRYPT_ROUNDS=10',
258
- 'AUTH_DEMO_EMAIL=demo@forgeon.local',
259
- 'AUTH_DEMO_PASSWORD=forgeon-demo-password',
260
- ]);
261
-
262
- upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
263
- 'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
264
- 'JWT_ACCESS_EXPIRES_IN=15m',
265
- 'JWT_REFRESH_SECRET=forgeon-refresh-secret-change-me',
266
- 'JWT_REFRESH_EXPIRES_IN=7d',
267
- 'AUTH_BCRYPT_ROUNDS=10',
268
- 'AUTH_DEMO_EMAIL=demo@forgeon.local',
269
- 'AUTH_DEMO_PASSWORD=forgeon-demo-password',
270
- ]);
271
- }
@@ -1,19 +0,0 @@
1
- ## Scope
2
-
3
- Implemented scope:
4
-
5
- 1. Split into reusable packages:
6
- - `@forgeon/auth-contracts`
7
- - `@forgeon/auth-api`
8
- 2. API runtime:
9
- - JWT login/refresh/logout/me endpoints
10
- - `JwtStrategy` + `JwtAuthGuard`
11
- - `authConfig` + `authEnvSchema` wiring through root `ConfigModule` validator chain
12
- 3. DB behavior:
13
- - module install stays stateless by default
14
- - refresh token hash persistence is enabled later through the `db-adapter` capability via `pnpm forgeon:sync-integrations`
15
- - current DB adapter implementation for this integration is `db-prisma`
16
- - if no DB adapter is installed, the module stays stateless and prints an optional integration warning with follow-up commands
17
- 4. Module checks:
18
- - API probe endpoint: `GET /api/health/auth`
19
- - default web probe button + result block
@@ -1,8 +0,0 @@
1
- ## Current State
2
-
3
- Status: implemented.
4
-
5
- Notes:
6
- - The persistence boundary is `db-adapter`, not a hard dependency on one concrete DB module.
7
- - The current DB adapter implementation for auth persistence is `db-prisma`.
8
- - If no DB adapter is installed, jwt-auth stays in stateless refresh mode and surfaces an optional integration warning.
@@ -1,3 +0,0 @@
1
- ## Current State
2
-
3
- This module is registered but not implemented yet.