create-forgeon 0.1.34 → 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/db-prisma.mjs +401 -0
- package/src/modules/executor.mjs +4 -0
- package/src/modules/executor.test.mjs +585 -13
- package/src/modules/i18n.mjs +244 -22
- package/src/modules/jwt-auth.mjs +612 -0
- package/src/modules/logger.mjs +76 -27
- package/src/modules/registry.mjs +15 -7
- package/src/modules/swagger.mjs +12 -2
- package/templates/module-fragments/db-prisma/00_title.md +6 -0
- package/templates/module-fragments/db-prisma/10_overview.md +10 -0
- package/templates/module-fragments/db-prisma/20_scope.md +14 -0
- package/templates/module-fragments/db-prisma/90_status_implemented.md +4 -0
- 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/src/modules/logger.mjs
CHANGED
|
@@ -71,6 +71,48 @@ function upsertEnvLines(filePath, lines) {
|
|
|
71
71
|
fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function ensureLoadItem(content, itemName) {
|
|
75
|
+
const pattern = /load:\s*\[([^\]]*)\]/m;
|
|
76
|
+
const match = content.match(pattern);
|
|
77
|
+
if (!match) {
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rawList = match[1];
|
|
82
|
+
const items = rawList
|
|
83
|
+
.split(',')
|
|
84
|
+
.map((item) => item.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
|
|
87
|
+
if (!items.includes(itemName)) {
|
|
88
|
+
items.push(itemName);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const next = `load: [${items.join(', ')}]`;
|
|
92
|
+
return content.replace(pattern, next);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureValidatorSchema(content, schemaName) {
|
|
96
|
+
const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
|
|
97
|
+
const match = content.match(pattern);
|
|
98
|
+
if (!match) {
|
|
99
|
+
return content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const rawList = match[1];
|
|
103
|
+
const items = rawList
|
|
104
|
+
.split(',')
|
|
105
|
+
.map((item) => item.trim())
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
|
|
108
|
+
if (!items.includes(schemaName)) {
|
|
109
|
+
items.push(schemaName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const next = `validate: createEnvValidator([${items.join(', ')}])`;
|
|
113
|
+
return content.replace(pattern, next);
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
function patchApiPackage(targetRoot) {
|
|
75
117
|
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
76
118
|
if (!fs.existsSync(packagePath)) {
|
|
@@ -141,28 +183,32 @@ function patchAppModule(targetRoot) {
|
|
|
141
183
|
|
|
142
184
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
143
185
|
|
|
144
|
-
content
|
|
145
|
-
content,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
186
|
+
if (!content.includes("from '@forgeon/logger';")) {
|
|
187
|
+
if (content.includes("import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';")) {
|
|
188
|
+
content = ensureLineAfter(
|
|
189
|
+
content,
|
|
190
|
+
"import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
|
|
191
|
+
"import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
|
|
192
|
+
);
|
|
193
|
+
} else if (
|
|
194
|
+
content.includes("import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';")
|
|
195
|
+
) {
|
|
196
|
+
content = ensureLineAfter(
|
|
197
|
+
content,
|
|
198
|
+
"import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
|
|
199
|
+
"import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
content = ensureLineAfter(
|
|
203
|
+
content,
|
|
204
|
+
"import { ConfigModule } from '@nestjs/config';",
|
|
205
|
+
"import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
149
209
|
|
|
150
|
-
content = content
|
|
151
|
-
|
|
152
|
-
'load: [coreConfig, dbPrismaConfig, i18nConfig, loggerConfig],',
|
|
153
|
-
);
|
|
154
|
-
content = content.replace(
|
|
155
|
-
'load: [coreConfig, dbPrismaConfig],',
|
|
156
|
-
'load: [coreConfig, dbPrismaConfig, loggerConfig],',
|
|
157
|
-
);
|
|
158
|
-
content = content.replace(
|
|
159
|
-
'validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema, i18nEnvSchema]),',
|
|
160
|
-
'validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema, i18nEnvSchema, loggerEnvSchema]),',
|
|
161
|
-
);
|
|
162
|
-
content = content.replace(
|
|
163
|
-
'validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema]),',
|
|
164
|
-
'validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema, loggerEnvSchema]),',
|
|
165
|
-
);
|
|
210
|
+
content = ensureLoadItem(content, 'loggerConfig');
|
|
211
|
+
content = ensureValidatorSchema(content, 'loggerEnvSchema');
|
|
166
212
|
|
|
167
213
|
content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonLoggerModule,');
|
|
168
214
|
|
|
@@ -177,16 +223,19 @@ function patchApiDockerfile(targetRoot) {
|
|
|
177
223
|
|
|
178
224
|
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
179
225
|
|
|
226
|
+
const packageAnchor = content.includes('COPY packages/db-prisma/package.json packages/db-prisma/package.json')
|
|
227
|
+
? 'COPY packages/db-prisma/package.json packages/db-prisma/package.json'
|
|
228
|
+
: 'COPY packages/core/package.json packages/core/package.json';
|
|
180
229
|
content = ensureLineAfter(
|
|
181
230
|
content,
|
|
182
|
-
|
|
231
|
+
packageAnchor,
|
|
183
232
|
'COPY packages/logger/package.json packages/logger/package.json',
|
|
184
233
|
);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
'COPY packages/db-prisma packages/db-prisma'
|
|
188
|
-
'COPY packages/
|
|
189
|
-
);
|
|
234
|
+
|
|
235
|
+
const sourceAnchor = content.includes('COPY packages/db-prisma packages/db-prisma')
|
|
236
|
+
? 'COPY packages/db-prisma packages/db-prisma'
|
|
237
|
+
: 'COPY packages/core packages/core';
|
|
238
|
+
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/logger packages/logger');
|
|
190
239
|
|
|
191
240
|
content = content.replace(/^RUN pnpm --filter @forgeon\/logger build\r?\n?/gm, '');
|
|
192
241
|
content = ensureLineBefore(
|
package/src/modules/registry.mjs
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
const MODULE_PRESETS = {
|
|
2
|
+
'db-prisma': {
|
|
3
|
+
id: 'db-prisma',
|
|
4
|
+
label: 'DB Prisma',
|
|
5
|
+
category: 'database-layer',
|
|
6
|
+
implemented: true,
|
|
7
|
+
description: 'Prisma/Postgres module wiring with env config, scripts, and DB probe endpoint.',
|
|
8
|
+
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
9
|
+
},
|
|
2
10
|
i18n: {
|
|
3
11
|
id: 'i18n',
|
|
4
12
|
label: 'I18n',
|
|
@@ -24,13 +32,13 @@ const MODULE_PRESETS = {
|
|
|
24
32
|
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
25
33
|
},
|
|
26
34
|
'jwt-auth': {
|
|
27
|
-
id: 'jwt-auth',
|
|
28
|
-
label: 'JWT Auth',
|
|
29
|
-
category: 'auth-security',
|
|
30
|
-
implemented:
|
|
31
|
-
description: 'JWT auth preset with
|
|
32
|
-
docFragments: ['00_title', '10_overview', '20_scope', '
|
|
33
|
-
},
|
|
35
|
+
id: 'jwt-auth',
|
|
36
|
+
label: 'JWT Auth',
|
|
37
|
+
category: 'auth-security',
|
|
38
|
+
implemented: true,
|
|
39
|
+
description: 'JWT auth preset with contracts/api module split, guard+strategy, and DB-aware refresh token storage wiring.',
|
|
40
|
+
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
41
|
+
},
|
|
34
42
|
queue: {
|
|
35
43
|
id: 'queue',
|
|
36
44
|
label: 'Queue Worker',
|
package/src/modules/swagger.mjs
CHANGED
|
@@ -197,6 +197,12 @@ function patchAppModule(targetRoot) {
|
|
|
197
197
|
"import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
|
|
198
198
|
"import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
|
|
199
199
|
);
|
|
200
|
+
} else {
|
|
201
|
+
content = ensureLineAfter(
|
|
202
|
+
content,
|
|
203
|
+
"import { ConfigModule } from '@nestjs/config';",
|
|
204
|
+
"import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
|
|
205
|
+
);
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
|
|
@@ -218,7 +224,9 @@ function patchApiDockerfile(targetRoot) {
|
|
|
218
224
|
|
|
219
225
|
const packageAnchor = content.includes('COPY packages/logger/package.json packages/logger/package.json')
|
|
220
226
|
? 'COPY packages/logger/package.json packages/logger/package.json'
|
|
221
|
-
: 'COPY packages/db-prisma/package.json packages/db-prisma/package.json'
|
|
227
|
+
: content.includes('COPY packages/db-prisma/package.json packages/db-prisma/package.json')
|
|
228
|
+
? 'COPY packages/db-prisma/package.json packages/db-prisma/package.json'
|
|
229
|
+
: 'COPY packages/core/package.json packages/core/package.json';
|
|
222
230
|
content = ensureLineAfter(
|
|
223
231
|
content,
|
|
224
232
|
packageAnchor,
|
|
@@ -227,7 +235,9 @@ function patchApiDockerfile(targetRoot) {
|
|
|
227
235
|
|
|
228
236
|
const sourceAnchor = content.includes('COPY packages/logger packages/logger')
|
|
229
237
|
? 'COPY packages/logger packages/logger'
|
|
230
|
-
: 'COPY packages/db-prisma packages/db-prisma'
|
|
238
|
+
: content.includes('COPY packages/db-prisma packages/db-prisma')
|
|
239
|
+
? 'COPY packages/db-prisma packages/db-prisma'
|
|
240
|
+
: 'COPY packages/core packages/core';
|
|
231
241
|
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/swagger packages/swagger');
|
|
232
242
|
|
|
233
243
|
content = content.replace(/^RUN pnpm --filter @forgeon\/swagger build\r?\n?/gm, '');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
Adds Prisma/Postgres database wiring to the API.
|
|
4
|
+
|
|
5
|
+
Included parts:
|
|
6
|
+
- `@forgeon/db-prisma` package
|
|
7
|
+
- Prisma schema + migration files in `apps/api/prisma`
|
|
8
|
+
- API scripts for `prisma generate/migrate/studio/seed`
|
|
9
|
+
- DB health probe endpoint wiring
|
|
10
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Applied Scope
|
|
2
|
+
|
|
3
|
+
- Adds `packages/db-prisma` workspace package
|
|
4
|
+
- Restores/creates `apps/api/prisma` schema and migrations
|
|
5
|
+
- Wires db config/env schema into API `ConfigModule` load/validation
|
|
6
|
+
- Registers `DbPrismaModule` in API `AppModule`
|
|
7
|
+
- Ensures `PrismaService` is available in health controller (`POST /api/health/db`)
|
|
8
|
+
- Updates API scripts and dependencies for Prisma workflows
|
|
9
|
+
- Updates API Docker build steps to include db package and prisma generate
|
|
10
|
+
- Ensures `DATABASE_URL` in:
|
|
11
|
+
- `apps/api/.env.example`
|
|
12
|
+
- `infra/docker/.env.example`
|
|
13
|
+
- `infra/docker/compose.yml`
|
|
14
|
+
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
## Scope
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
1.
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
- if supported DB adapter is present (`db-prisma`), refresh token hash persistence is auto-wired
|
|
14
|
+
- if DB is missing/unsupported, module installs in stateless mode and prints red warning
|
|
15
|
+
4. Module checks:
|
|
16
|
+
- API probe endpoint: `GET /api/health/auth`
|
|
17
|
+
- default web probe button + result block
|
package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthRefreshTokenStore,
|
|
3
|
+
} from '@forgeon/auth-api';
|
|
4
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
5
|
+
import { Injectable } from '@nestjs/common';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class PrismaAuthRefreshTokenStore implements AuthRefreshTokenStore {
|
|
9
|
+
readonly kind = 'prisma';
|
|
10
|
+
|
|
11
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
12
|
+
|
|
13
|
+
async saveRefreshTokenHash(subject: string, hash: string): Promise<void> {
|
|
14
|
+
await this.prisma.user.upsert({
|
|
15
|
+
where: { email: subject },
|
|
16
|
+
create: { email: subject, refreshTokenHash: hash },
|
|
17
|
+
update: { refreshTokenHash: hash },
|
|
18
|
+
select: { id: true },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getRefreshTokenHash(subject: string): Promise<string | null> {
|
|
23
|
+
const user = await this.prisma.user.findUnique({
|
|
24
|
+
where: { email: subject },
|
|
25
|
+
select: { refreshTokenHash: true },
|
|
26
|
+
});
|
|
27
|
+
return user?.refreshTokenHash ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async removeRefreshTokenHash(subject: string): Promise<void> {
|
|
31
|
+
await this.prisma.user.updateMany({
|
|
32
|
+
where: { email: subject },
|
|
33
|
+
data: { refreshTokenHash: null },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeon/auth-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@forgeon/auth-contracts": "workspace:*",
|
|
12
|
+
"@nestjs/common": "^11.0.1",
|
|
13
|
+
"@nestjs/config": "^4.0.2",
|
|
14
|
+
"@nestjs/jwt": "^11.0.1",
|
|
15
|
+
"@nestjs/passport": "^11.0.5",
|
|
16
|
+
"bcryptjs": "^2.4.3",
|
|
17
|
+
"class-validator": "^0.14.1",
|
|
18
|
+
"passport": "^0.7.0",
|
|
19
|
+
"passport-jwt": "^4.0.1",
|
|
20
|
+
"zod": "^3.23.8"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bcryptjs": "^2.4.6",
|
|
24
|
+
"@types/node": "^22.10.7",
|
|
25
|
+
"@types/passport-jwt": "^4.0.1",
|
|
26
|
+
"typescript": "^5.7.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { parseAuthEnv } from './auth-env.schema';
|
|
3
|
+
|
|
4
|
+
export const AUTH_CONFIG_NAMESPACE = 'auth';
|
|
5
|
+
|
|
6
|
+
export interface AuthConfigValues {
|
|
7
|
+
accessSecret: string;
|
|
8
|
+
accessExpiresIn: string;
|
|
9
|
+
refreshSecret: string;
|
|
10
|
+
refreshExpiresIn: string;
|
|
11
|
+
bcryptRounds: number;
|
|
12
|
+
demoEmail: string;
|
|
13
|
+
demoPassword: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const authConfig = registerAs(AUTH_CONFIG_NAMESPACE, (): AuthConfigValues => {
|
|
17
|
+
const env = parseAuthEnv(process.env);
|
|
18
|
+
return {
|
|
19
|
+
accessSecret: env.JWT_ACCESS_SECRET,
|
|
20
|
+
accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
|
|
21
|
+
refreshSecret: env.JWT_REFRESH_SECRET,
|
|
22
|
+
refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
|
|
23
|
+
bcryptRounds: env.AUTH_BCRYPT_ROUNDS,
|
|
24
|
+
demoEmail: env.AUTH_DEMO_EMAIL.toLowerCase(),
|
|
25
|
+
demoPassword: env.AUTH_DEMO_PASSWORD,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { AUTH_CONFIG_NAMESPACE } from './auth-config.loader';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class AuthConfigService {
|
|
7
|
+
constructor(private readonly configService: ConfigService) {}
|
|
8
|
+
|
|
9
|
+
get accessSecret(): string {
|
|
10
|
+
return this.configService.getOrThrow<string>(`${AUTH_CONFIG_NAMESPACE}.accessSecret`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get accessExpiresIn(): string {
|
|
14
|
+
return this.configService.getOrThrow<string>(`${AUTH_CONFIG_NAMESPACE}.accessExpiresIn`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get refreshSecret(): string {
|
|
18
|
+
return this.configService.getOrThrow<string>(`${AUTH_CONFIG_NAMESPACE}.refreshSecret`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get refreshExpiresIn(): string {
|
|
22
|
+
return this.configService.getOrThrow<string>(`${AUTH_CONFIG_NAMESPACE}.refreshExpiresIn`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get bcryptRounds(): number {
|
|
26
|
+
return this.configService.getOrThrow<number>(`${AUTH_CONFIG_NAMESPACE}.bcryptRounds`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get demoEmail(): string {
|
|
30
|
+
return this.configService.getOrThrow<string>(`${AUTH_CONFIG_NAMESPACE}.demoEmail`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get demoPassword(): string {
|
|
34
|
+
return this.configService.getOrThrow<string>(`${AUTH_CONFIG_NAMESPACE}.demoPassword`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const authEnvSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
JWT_ACCESS_SECRET: z.string().trim().min(16).default('forgeon-access-secret-change-me'),
|
|
6
|
+
JWT_ACCESS_EXPIRES_IN: z.string().trim().min(2).default('15m'),
|
|
7
|
+
JWT_REFRESH_SECRET: z.string().trim().min(16).default('forgeon-refresh-secret-change-me'),
|
|
8
|
+
JWT_REFRESH_EXPIRES_IN: z.string().trim().min(2).default('7d'),
|
|
9
|
+
AUTH_BCRYPT_ROUNDS: z.coerce.number().int().min(4).max(15).default(10),
|
|
10
|
+
AUTH_DEMO_EMAIL: z.string().trim().email().default('demo@forgeon.local'),
|
|
11
|
+
AUTH_DEMO_PASSWORD: z.string().min(8).default('forgeon-demo-password'),
|
|
12
|
+
})
|
|
13
|
+
.passthrough();
|
|
14
|
+
|
|
15
|
+
export type AuthEnv = z.infer<typeof authEnvSchema>;
|
|
16
|
+
|
|
17
|
+
export function parseAuthEnv(input: Record<string, unknown>): AuthEnv {
|
|
18
|
+
return authEnvSchema.parse(input);
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
export const AUTH_REFRESH_TOKEN_STORE = Symbol('AUTH_REFRESH_TOKEN_STORE');
|
|
4
|
+
|
|
5
|
+
export interface AuthRefreshTokenStore {
|
|
6
|
+
readonly kind: string;
|
|
7
|
+
saveRefreshTokenHash(subject: string, hash: string): Promise<void>;
|
|
8
|
+
getRefreshTokenHash(subject: string): Promise<string | null>;
|
|
9
|
+
removeRefreshTokenHash(subject: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class NoopAuthRefreshTokenStore implements AuthRefreshTokenStore {
|
|
14
|
+
readonly kind = 'none';
|
|
15
|
+
|
|
16
|
+
async saveRefreshTokenHash(): Promise<void> {}
|
|
17
|
+
|
|
18
|
+
async getRefreshTokenHash(): Promise<string | null> {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async removeRefreshTokenHash(): Promise<void> {}
|
|
23
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { AuthUser } from '@forgeon/auth-contracts';
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
Post,
|
|
7
|
+
Req,
|
|
8
|
+
UseGuards,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
import { AuthService } from './auth.service';
|
|
11
|
+
import { LoginDto, RefreshDto } from './dto';
|
|
12
|
+
import { JwtAuthGuard } from './jwt-auth.guard';
|
|
13
|
+
import { AuthJwtPayload } from './auth.types';
|
|
14
|
+
|
|
15
|
+
type RequestWithUser = { user?: AuthJwtPayload };
|
|
16
|
+
|
|
17
|
+
@Controller('auth')
|
|
18
|
+
export class AuthController {
|
|
19
|
+
constructor(private readonly authService: AuthService) {}
|
|
20
|
+
|
|
21
|
+
@Post('login')
|
|
22
|
+
login(@Body() body: LoginDto) {
|
|
23
|
+
return this.authService.login(body);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Post('refresh')
|
|
27
|
+
refresh(@Body() body: RefreshDto) {
|
|
28
|
+
return this.authService.refresh(body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@UseGuards(JwtAuthGuard)
|
|
32
|
+
@Post('logout')
|
|
33
|
+
async logout(@Req() request: RequestWithUser) {
|
|
34
|
+
const user = this.getRequestUser(request);
|
|
35
|
+
await this.authService.logout(user);
|
|
36
|
+
return {
|
|
37
|
+
status: 'ok',
|
|
38
|
+
tokenStore: this.authService.getTokenStoreKind(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@UseGuards(JwtAuthGuard)
|
|
43
|
+
@Get('me')
|
|
44
|
+
me(@Req() request: RequestWithUser) {
|
|
45
|
+
const user = this.getRequestUser(request);
|
|
46
|
+
return {
|
|
47
|
+
user: this.toAuthUser(user),
|
|
48
|
+
tokenStore: this.authService.getTokenStoreKind(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private getRequestUser(request: RequestWithUser): AuthJwtPayload {
|
|
53
|
+
const user = request.user;
|
|
54
|
+
if (!user || typeof user.sub !== 'string' || typeof user.email !== 'string') {
|
|
55
|
+
return {
|
|
56
|
+
sub: 'unknown',
|
|
57
|
+
email: 'unknown@invalid.local',
|
|
58
|
+
roles: ['user'],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return user;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private toAuthUser(payload: AuthJwtPayload): AuthUser {
|
|
65
|
+
return {
|
|
66
|
+
sub: payload.sub,
|
|
67
|
+
email: payload.email,
|
|
68
|
+
roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|