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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/modules/executor.mjs +2 -0
- package/src/modules/executor.test.mjs +172 -8
- package/src/modules/jwt-auth.mjs +612 -0
- package/src/modules/registry.mjs +7 -7
- package/templates/module-fragments/jwt-auth/20_scope.md +17 -7
- package/templates/module-fragments/jwt-auth/90_status_implemented.md +7 -0
- package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +3 -0
- package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +36 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/package.json +28 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.loader.ts +27 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.module.ts +8 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.service.ts +36 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-env.schema.ts +19 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +23 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +71 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +155 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +6 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +2 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/login.dto.ts +11 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/refresh.dto.ts +8 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +47 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +12 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt-auth.guard.ts +5 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt.strategy.ts +20 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/tsconfig.json +9 -0
- package/templates/module-presets/jwt-auth/packages/auth-contracts/package.json +21 -0
- package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +47 -0
- 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
package/src/modules/executor.mjs
CHANGED
|
@@ -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: '
|
|
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, /
|
|
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'],
|