create-forgeon 0.1.35 → 0.1.36

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 (30) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/modules/executor.mjs +2 -0
  4. package/src/modules/executor.test.mjs +172 -8
  5. package/src/modules/jwt-auth.mjs +612 -0
  6. package/src/modules/registry.mjs +7 -7
  7. package/templates/module-fragments/jwt-auth/20_scope.md +17 -7
  8. package/templates/module-fragments/jwt-auth/90_status_implemented.md +7 -0
  9. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +3 -0
  10. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +36 -0
  11. package/templates/module-presets/jwt-auth/packages/auth-api/package.json +28 -0
  12. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.loader.ts +27 -0
  13. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.module.ts +8 -0
  14. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.service.ts +36 -0
  15. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-env.schema.ts +19 -0
  16. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +23 -0
  17. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +71 -0
  18. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +155 -0
  19. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +6 -0
  20. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +2 -0
  21. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/login.dto.ts +11 -0
  22. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/refresh.dto.ts +8 -0
  23. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +47 -0
  24. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +12 -0
  25. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt-auth.guard.ts +5 -0
  26. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt.strategy.ts +20 -0
  27. package/templates/module-presets/jwt-auth/packages/auth-api/tsconfig.json +9 -0
  28. package/templates/module-presets/jwt-auth/packages/auth-contracts/package.json +21 -0
  29. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +47 -0
  30. package/templates/module-presets/jwt-auth/packages/auth-contracts/tsconfig.json +9 -0
package/README.md CHANGED
@@ -28,4 +28,5 @@ npx create-forgeon@latest add jwt-auth --project ./my-app
28
28
  - Canonical stack is fixed: NestJS + React + Prisma/Postgres + Docker.
29
29
  - Reverse proxy options: `caddy` (default), `nginx`, `none`.
30
30
  - `add i18n` is implemented and applies backend/frontend i18n wiring.
31
+ - `add jwt-auth` is implemented and auto-detects DB adapter support for refresh-token persistence.
31
32
  - Planned modules write docs notes under `docs/AI/MODULES/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -4,6 +4,7 @@ import { ensureModuleExists } from './registry.mjs';
4
4
  import { writeModuleDocs } from './docs.mjs';
5
5
  import { applyDbPrismaModule } from './db-prisma.mjs';
6
6
  import { applyI18nModule } from './i18n.mjs';
7
+ import { applyJwtAuthModule } from './jwt-auth.mjs';
7
8
  import { applyLoggerModule } from './logger.mjs';
8
9
  import { applySwaggerModule } from './swagger.mjs';
9
10
 
@@ -26,6 +27,7 @@ function ensureForgeonLikeProject(targetRoot) {
26
27
  const MODULE_APPLIERS = {
27
28
  'db-prisma': applyDbPrismaModule,
28
29
  i18n: applyI18nModule,
30
+ 'jwt-auth': applyJwtAuthModule,
29
31
  logger: applyLoggerModule,
30
32
  swagger: applySwaggerModule,
31
33
  };
@@ -41,6 +41,61 @@ function assertDbPrismaWiring(projectRoot) {
41
41
  assert.match(healthController, /PrismaService/);
42
42
  }
43
43
 
44
+ function assertJwtAuthWiring(projectRoot, withPrismaStore) {
45
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
46
+ assert.match(apiPackage, /@forgeon\/auth-api/);
47
+ assert.match(apiPackage, /@forgeon\/auth-contracts/);
48
+ assert.match(apiPackage, /pnpm --filter @forgeon\/auth-contracts build/);
49
+ assert.match(apiPackage, /pnpm --filter @forgeon\/auth-api build/);
50
+
51
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
52
+ assert.match(appModule, /authConfig/);
53
+ assert.match(appModule, /authEnvSchema/);
54
+ assert.match(appModule, /ForgeonAuthModule\.register\(/);
55
+ if (withPrismaStore) {
56
+ assert.match(appModule, /AUTH_REFRESH_TOKEN_STORE/);
57
+ assert.match(appModule, /PrismaAuthRefreshTokenStore/);
58
+ } else {
59
+ assert.doesNotMatch(appModule, /PrismaAuthRefreshTokenStore/);
60
+ }
61
+
62
+ const healthController = fs.readFileSync(
63
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
64
+ 'utf8',
65
+ );
66
+ assert.match(healthController, /@Get\('auth'\)/);
67
+ assert.match(healthController, /authService\.getProbeStatus/);
68
+ assert.doesNotMatch(healthController, /,\s*,/);
69
+
70
+ const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
71
+ assert.match(appTsx, /Check JWT auth probe/);
72
+ assert.match(appTsx, /Auth probe response/);
73
+
74
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
75
+ assert.match(
76
+ apiDockerfile,
77
+ /COPY packages\/auth-contracts\/package\.json packages\/auth-contracts\/package\.json/,
78
+ );
79
+ assert.match(apiDockerfile, /COPY packages\/auth-api\/package\.json packages\/auth-api\/package\.json/);
80
+ assert.match(apiDockerfile, /COPY packages\/auth-contracts packages\/auth-contracts/);
81
+ assert.match(apiDockerfile, /COPY packages\/auth-api packages\/auth-api/);
82
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/auth-contracts build/);
83
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/auth-api build/);
84
+
85
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
86
+ assert.match(apiEnv, /JWT_ACCESS_SECRET=/);
87
+ assert.match(apiEnv, /JWT_REFRESH_SECRET=/);
88
+ assert.match(apiEnv, /AUTH_DEMO_EMAIL=/);
89
+ assert.match(apiEnv, /AUTH_DEMO_PASSWORD=/);
90
+
91
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
92
+ assert.match(compose, /JWT_ACCESS_SECRET: \$\{JWT_ACCESS_SECRET\}/);
93
+ assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
94
+
95
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
96
+ assert.match(readme, /## JWT Auth Module/);
97
+ }
98
+
44
99
  function stripDbPrismaArtifacts(projectRoot) {
45
100
  const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
46
101
  if (fs.existsSync(dbPackageDir)) {
@@ -146,23 +201,23 @@ describe('addModule', () => {
146
201
  const modulesDir = path.dirname(fileURLToPath(import.meta.url));
147
202
  const packageRoot = path.resolve(modulesDir, '..', '..');
148
203
 
149
- it('creates module docs note for planned module', () => {
204
+ it('creates module docs note for planned module', () => {
150
205
  const targetRoot = mkTmp('forgeon-module-');
151
206
  try {
152
207
  createMinimalForgeonProject(targetRoot);
153
- const result = addModule({
154
- moduleId: 'jwt-auth',
155
- targetRoot,
156
- packageRoot,
157
- });
208
+ const result = addModule({
209
+ moduleId: 'queue',
210
+ targetRoot,
211
+ packageRoot,
212
+ });
158
213
 
159
214
  assert.equal(result.applied, false);
160
215
  assert.match(result.message, /planned/);
161
216
  assert.equal(fs.existsSync(result.docsPath), true);
162
217
 
163
218
  const note = fs.readFileSync(result.docsPath, 'utf8');
164
- assert.match(note, /JWT Auth/);
165
- assert.match(note, /Status: planned/);
219
+ assert.match(note, /Queue Worker/);
220
+ assert.match(note, /Status: planned/);
166
221
  } finally {
167
222
  fs.rmSync(targetRoot, { recursive: true, force: true });
168
223
  }
@@ -804,6 +859,115 @@ describe('addModule', () => {
804
859
  }
805
860
  });
806
861
 
862
+ it('applies jwt-auth with db-prisma adapter and wires persistent token store', () => {
863
+ const targetRoot = mkTmp('forgeon-module-jwt-db-');
864
+ const projectRoot = path.join(targetRoot, 'demo-jwt-db');
865
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
866
+
867
+ try {
868
+ scaffoldProject({
869
+ templateRoot,
870
+ packageRoot,
871
+ targetRoot: projectRoot,
872
+ projectName: 'demo-jwt-db',
873
+ frontend: 'react',
874
+ db: 'prisma',
875
+ i18nEnabled: true,
876
+ proxy: 'caddy',
877
+ });
878
+
879
+ const result = addModule({
880
+ moduleId: 'jwt-auth',
881
+ targetRoot: projectRoot,
882
+ packageRoot,
883
+ });
884
+
885
+ assert.equal(result.applied, true);
886
+ assertJwtAuthWiring(projectRoot, true);
887
+
888
+ const storeFile = path.join(
889
+ projectRoot,
890
+ 'apps',
891
+ 'api',
892
+ 'src',
893
+ 'auth',
894
+ 'prisma-auth-refresh-token.store.ts',
895
+ );
896
+ assert.equal(fs.existsSync(storeFile), true);
897
+
898
+ const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
899
+ assert.match(schema, /refreshTokenHash/);
900
+
901
+ const migrationPath = path.join(
902
+ projectRoot,
903
+ 'apps',
904
+ 'api',
905
+ 'prisma',
906
+ 'migrations',
907
+ '0002_auth_refresh_token_hash',
908
+ 'migration.sql',
909
+ );
910
+ assert.equal(fs.existsSync(migrationPath), true);
911
+
912
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
913
+ assert.match(readme, /refresh token persistence: enabled/);
914
+ assert.match(readme, /0002_auth_refresh_token_hash/);
915
+
916
+ const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
917
+ assert.match(moduleDoc, /Status: implemented/);
918
+ } finally {
919
+ fs.rmSync(targetRoot, { recursive: true, force: true });
920
+ }
921
+ });
922
+
923
+ it('applies jwt-auth without db and prints warning with stateless fallback', () => {
924
+ const targetRoot = mkTmp('forgeon-module-jwt-nodb-');
925
+ const projectRoot = path.join(targetRoot, 'demo-jwt-nodb');
926
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
927
+
928
+ const originalError = console.error;
929
+ const warnings = [];
930
+ console.error = (...args) => warnings.push(args.join(' '));
931
+
932
+ try {
933
+ scaffoldProject({
934
+ templateRoot,
935
+ packageRoot,
936
+ targetRoot: projectRoot,
937
+ projectName: 'demo-jwt-nodb',
938
+ frontend: 'react',
939
+ db: 'prisma',
940
+ i18nEnabled: false,
941
+ proxy: 'caddy',
942
+ });
943
+
944
+ stripDbPrismaArtifacts(projectRoot);
945
+
946
+ const result = addModule({
947
+ moduleId: 'jwt-auth',
948
+ targetRoot: projectRoot,
949
+ packageRoot,
950
+ });
951
+
952
+ assert.equal(result.applied, true);
953
+ assertJwtAuthWiring(projectRoot, false);
954
+ assert.equal(
955
+ fs.existsSync(path.join(projectRoot, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts')),
956
+ false,
957
+ );
958
+
959
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
960
+ assert.match(readme, /refresh token persistence: disabled/);
961
+ assert.match(readme, /create-forgeon add db-prisma/);
962
+
963
+ assert.equal(warnings.length > 0, true);
964
+ assert.match(warnings.join('\n'), /jwt-auth installed without persistent refresh token store/);
965
+ } finally {
966
+ console.error = originalError;
967
+ fs.rmSync(targetRoot, { recursive: true, force: true });
968
+ }
969
+ });
970
+
807
971
  it('keeps db-prisma wiring across module installation orders', () => {
808
972
  const sequences = [
809
973
  ['logger', 'swagger', 'i18n'],