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.
Files changed (38) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/modules/db-prisma.mjs +401 -0
  4. package/src/modules/executor.mjs +4 -0
  5. package/src/modules/executor.test.mjs +585 -13
  6. package/src/modules/i18n.mjs +244 -22
  7. package/src/modules/jwt-auth.mjs +612 -0
  8. package/src/modules/logger.mjs +76 -27
  9. package/src/modules/registry.mjs +15 -7
  10. package/src/modules/swagger.mjs +12 -2
  11. package/templates/module-fragments/db-prisma/00_title.md +6 -0
  12. package/templates/module-fragments/db-prisma/10_overview.md +10 -0
  13. package/templates/module-fragments/db-prisma/20_scope.md +14 -0
  14. package/templates/module-fragments/db-prisma/90_status_implemented.md +4 -0
  15. package/templates/module-fragments/jwt-auth/20_scope.md +17 -7
  16. package/templates/module-fragments/jwt-auth/90_status_implemented.md +7 -0
  17. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +3 -0
  18. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +36 -0
  19. package/templates/module-presets/jwt-auth/packages/auth-api/package.json +28 -0
  20. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.loader.ts +27 -0
  21. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.module.ts +8 -0
  22. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.service.ts +36 -0
  23. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-env.schema.ts +19 -0
  24. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +23 -0
  25. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +71 -0
  26. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +155 -0
  27. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +6 -0
  28. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +2 -0
  29. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/login.dto.ts +11 -0
  30. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/refresh.dto.ts +8 -0
  31. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +47 -0
  32. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +12 -0
  33. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt-auth.guard.ts +5 -0
  34. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt.strategy.ts +20 -0
  35. package/templates/module-presets/jwt-auth/packages/auth-api/tsconfig.json +9 -0
  36. package/templates/module-presets/jwt-auth/packages/auth-contracts/package.json +21 -0
  37. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +47 -0
  38. package/templates/module-presets/jwt-auth/packages/auth-contracts/tsconfig.json +9 -0
@@ -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 = ensureLineAfter(
145
- content,
146
- "import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
147
- "import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
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.replace(
151
- 'load: [coreConfig, dbPrismaConfig, i18nConfig],',
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
- 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
231
+ packageAnchor,
183
232
  'COPY packages/logger/package.json packages/logger/package.json',
184
233
  );
185
- content = ensureLineAfter(
186
- content,
187
- 'COPY packages/db-prisma packages/db-prisma',
188
- 'COPY packages/logger packages/logger',
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(
@@ -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: false,
31
- description: 'JWT auth preset with guards and passport strategy wiring.',
32
- docFragments: ['00_title', '10_overview', '20_scope', '90_status_planned'],
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',
@@ -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,6 @@
1
+ # {{MODULE_LABEL}}
2
+
3
+ - Id: `{{MODULE_ID}}`
4
+ - Category: `{{MODULE_CATEGORY}}`
5
+ - Status: {{MODULE_STATUS}}
6
+
@@ -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
+
@@ -0,0 +1,4 @@
1
+ ## Status
2
+
3
+ Implemented and applied by `create-forgeon add db-prisma`.
4
+
@@ -1,7 +1,17 @@
1
- ## Scope (Planned)
2
-
3
- Planned implementation target:
4
-
5
- 1. Add reusable auth package wiring under `packages/*`.
6
- 2. Add NestJS guard + strategy integration in `apps/api`.
7
- 3. Add env docs and usage examples.
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
@@ -0,0 +1,7 @@
1
+ ## Current State
2
+
3
+ Status: implemented.
4
+
5
+ Notes:
6
+ - DB adapter auto-detection is currently implemented for `db-prisma`.
7
+ - Unknown/missing DB adapter falls back to stateless refresh flow with explicit warning.
@@ -0,0 +1,3 @@
1
+ -- AlterTable
2
+ ALTER TABLE "User"
3
+ ADD COLUMN "refreshTokenHash" TEXT;
@@ -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,8 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { AuthConfigService } from './auth-config.service';
3
+
4
+ @Module({
5
+ providers: [AuthConfigService],
6
+ exports: [AuthConfigService],
7
+ })
8
+ export class AuthConfigModule {}
@@ -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
+ }