@gyxer-studio/generator 0.1.0
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 +63 -0
- package/dist/generators/app.generator.d.ts +19 -0
- package/dist/generators/app.generator.d.ts.map +1 -0
- package/dist/generators/app.generator.js +237 -0
- package/dist/generators/app.generator.js.map +1 -0
- package/dist/generators/controller.generator.d.ts +10 -0
- package/dist/generators/controller.generator.d.ts.map +1 -0
- package/dist/generators/controller.generator.js +88 -0
- package/dist/generators/controller.generator.js.map +1 -0
- package/dist/generators/docker.generator.d.ts +15 -0
- package/dist/generators/docker.generator.d.ts.map +1 -0
- package/dist/generators/docker.generator.js +82 -0
- package/dist/generators/docker.generator.js.map +1 -0
- package/dist/generators/dto.generator.d.ts +23 -0
- package/dist/generators/dto.generator.d.ts.map +1 -0
- package/dist/generators/dto.generator.js +271 -0
- package/dist/generators/dto.generator.js.map +1 -0
- package/dist/generators/module.generator.d.ts +6 -0
- package/dist/generators/module.generator.d.ts.map +1 -0
- package/dist/generators/module.generator.js +20 -0
- package/dist/generators/module.generator.js.map +1 -0
- package/dist/generators/prisma.generator.d.ts +6 -0
- package/dist/generators/prisma.generator.d.ts.map +1 -0
- package/dist/generators/prisma.generator.js +243 -0
- package/dist/generators/prisma.generator.js.map +1 -0
- package/dist/generators/service.generator.d.ts +6 -0
- package/dist/generators/service.generator.d.ts.map +1 -0
- package/dist/generators/service.generator.js +97 -0
- package/dist/generators/service.generator.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/auth-jwt.generator.d.ts +20 -0
- package/dist/modules/auth-jwt.generator.d.ts.map +1 -0
- package/dist/modules/auth-jwt.generator.js +431 -0
- package/dist/modules/auth-jwt.generator.js.map +1 -0
- package/dist/project-generator.d.ts +16 -0
- package/dist/project-generator.d.ts.map +1 -0
- package/dist/project-generator.js +199 -0
- package/dist/project-generator.js.map +1 -0
- package/dist/security/report.d.ts +24 -0
- package/dist/security/report.d.ts.map +1 -0
- package/dist/security/report.js +108 -0
- package/dist/security/report.js.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +44 -0
- package/dist/utils.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { toCamelCase, toKebabCase } from '../utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a NestJS service for an entity with full CRUD.
|
|
4
|
+
*/
|
|
5
|
+
export function generateService(entity, project) {
|
|
6
|
+
const name = entity.name;
|
|
7
|
+
const camel = toCamelCase(name);
|
|
8
|
+
const className = `${name}Service`;
|
|
9
|
+
const hasAuthJwt = name === 'User' &&
|
|
10
|
+
project.modules?.some((m) => m.name === 'auth-jwt' && m.enabled !== false);
|
|
11
|
+
// User + auth-jwt: create method hashes password
|
|
12
|
+
if (hasAuthJwt) {
|
|
13
|
+
return `import { Injectable, NotFoundException } from '@nestjs/common';
|
|
14
|
+
import * as bcrypt from 'bcrypt';
|
|
15
|
+
import { Prisma } from '@prisma/client';
|
|
16
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
17
|
+
import { Create${name}Dto } from './dto/create-${toKebabCase(name)}.dto';
|
|
18
|
+
import { Update${name}Dto } from './dto/update-${toKebabCase(name)}.dto';
|
|
19
|
+
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class ${className} {
|
|
22
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
23
|
+
|
|
24
|
+
async create(dto: Create${name}Dto) {
|
|
25
|
+
const { password, ...rest } = dto;
|
|
26
|
+
const passwordHash = await bcrypt.hash(password, 12);
|
|
27
|
+
return this.prisma.${camel}.create({ data: { ...rest, passwordHash } as Prisma.${name}UncheckedCreateInput });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findAll() {
|
|
31
|
+
return this.prisma.${camel}.findMany({
|
|
32
|
+
select: { id: true, email: true, name: true, createdAt: true, updatedAt: true },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findOne(id: number) {
|
|
37
|
+
const ${camel} = await this.prisma.${camel}.findUnique({
|
|
38
|
+
where: { id },
|
|
39
|
+
select: { id: true, email: true, name: true, createdAt: true, updatedAt: true },
|
|
40
|
+
});
|
|
41
|
+
if (!${camel}) {
|
|
42
|
+
throw new NotFoundException(\`${name} with id \${id} not found\`);
|
|
43
|
+
}
|
|
44
|
+
return ${camel};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async update(id: number, data: Update${name}Dto) {
|
|
48
|
+
await this.findOne(id);
|
|
49
|
+
return this.prisma.${camel}.update({ where: { id }, data: data as Prisma.${name}UncheckedUpdateInput });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async remove(id: number) {
|
|
53
|
+
await this.findOne(id);
|
|
54
|
+
return this.prisma.${camel}.delete({ where: { id } });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
return `import { Injectable, NotFoundException } from '@nestjs/common';
|
|
60
|
+
import { Prisma } from '@prisma/client';
|
|
61
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
62
|
+
import { Create${name}Dto } from './dto/create-${toKebabCase(name)}.dto';
|
|
63
|
+
import { Update${name}Dto } from './dto/update-${toKebabCase(name)}.dto';
|
|
64
|
+
|
|
65
|
+
@Injectable()
|
|
66
|
+
export class ${className} {
|
|
67
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
68
|
+
|
|
69
|
+
async create(data: Create${name}Dto) {
|
|
70
|
+
return this.prisma.${camel}.create({ data: data as Prisma.${name}UncheckedCreateInput });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async findAll() {
|
|
74
|
+
return this.prisma.${camel}.findMany();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async findOne(id: number) {
|
|
78
|
+
const ${camel} = await this.prisma.${camel}.findUnique({ where: { id } });
|
|
79
|
+
if (!${camel}) {
|
|
80
|
+
throw new NotFoundException(\`${name} with id \${id} not found\`);
|
|
81
|
+
}
|
|
82
|
+
return ${camel};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async update(id: number, data: Update${name}Dto) {
|
|
86
|
+
await this.findOne(id);
|
|
87
|
+
return this.prisma.${camel}.update({ where: { id }, data: data as Prisma.${name}UncheckedUpdateInput });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async remove(id: number) {
|
|
91
|
+
await this.findOne(id);
|
|
92
|
+
return this.prisma.${camel}.delete({ where: { id } });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=service.generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.generator.js","sourceRoot":"","sources":["../../src/generators/service.generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,OAAqB;IACnE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,SAAS,GAAG,GAAG,IAAI,SAAS,CAAC;IAEnC,MAAM,UAAU,GACd,IAAI,KAAK,MAAM;QACf,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC;IAE7E,iDAAiD;IACjD,IAAI,UAAU,EAAE,CAAC;QACf,OAAO;;;;iBAIM,IAAI,4BAA4B,WAAW,CAAC,IAAI,CAAC;iBACjD,IAAI,4BAA4B,WAAW,CAAC,IAAI,CAAC;;;eAGnD,SAAS;;;4BAGI,IAAI;;;yBAGP,KAAK,uDAAuD,IAAI;;;;yBAIhE,KAAK;;;;;;YAMlB,KAAK,wBAAwB,KAAK;;;;WAInC,KAAK;sCACsB,IAAI;;aAE7B,KAAK;;;yCAGuB,IAAI;;yBAEpB,KAAK,iDAAiD,IAAI;;;;;yBAK1D,KAAK;;;CAG7B,CAAC;IACA,CAAC;IAED,OAAO;;;iBAGQ,IAAI,4BAA4B,WAAW,CAAC,IAAI,CAAC;iBACjD,IAAI,4BAA4B,WAAW,CAAC,IAAI,CAAC;;;eAGnD,SAAS;;;6BAGK,IAAI;yBACR,KAAK,kCAAkC,IAAI;;;;yBAI3C,KAAK;;;;YAIlB,KAAK,wBAAwB,KAAK;WACnC,KAAK;sCACsB,IAAI;;aAE7B,KAAK;;;yCAGuB,IAAI;;yBAEpB,KAAK,iDAAiD,IAAI;;;;;yBAK1D,KAAK;;;CAG7B,CAAC;AACF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { generateProject } from './project-generator.js';
|
|
2
|
+
export type { GenerateOptions, GenerateResult } from './project-generator.js';
|
|
3
|
+
export { generatePrismaSchema } from './generators/prisma.generator.js';
|
|
4
|
+
export { generateCreateDto, generateUpdateDto } from './generators/dto.generator.js';
|
|
5
|
+
export { generateService } from './generators/service.generator.js';
|
|
6
|
+
export { generateController } from './generators/controller.generator.js';
|
|
7
|
+
export { generateModule } from './generators/module.generator.js';
|
|
8
|
+
export { generateMain, generateAppModule, generatePrismaService, generatePrismaModule, generatePrismaExceptionFilter, } from './generators/app.generator.js';
|
|
9
|
+
export { generateDockerfile, generateDockerCompose, generateEnvFile, generateEnvExample, } from './generators/docker.generator.js';
|
|
10
|
+
export { generateAuthJwtFiles } from './modules/auth-jwt.generator.js';
|
|
11
|
+
export { generateSecurityReport, formatSecurityReport } from './security/report.js';
|
|
12
|
+
export type { SecurityCheck, SecurityReport } from './security/report.js';
|
|
13
|
+
export { toKebabCase, toCamelCase, toSnakeCase, pluralize } from './utils.js';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAGvE,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG1E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { generateProject } from './project-generator.js';
|
|
2
|
+
// Individual generators (for advanced usage)
|
|
3
|
+
export { generatePrismaSchema } from './generators/prisma.generator.js';
|
|
4
|
+
export { generateCreateDto, generateUpdateDto } from './generators/dto.generator.js';
|
|
5
|
+
export { generateService } from './generators/service.generator.js';
|
|
6
|
+
export { generateController } from './generators/controller.generator.js';
|
|
7
|
+
export { generateModule } from './generators/module.generator.js';
|
|
8
|
+
export { generateMain, generateAppModule, generatePrismaService, generatePrismaModule, generatePrismaExceptionFilter, } from './generators/app.generator.js';
|
|
9
|
+
export { generateDockerfile, generateDockerCompose, generateEnvFile, generateEnvExample, } from './generators/docker.generator.js';
|
|
10
|
+
// Modules
|
|
11
|
+
export { generateAuthJwtFiles } from './modules/auth-jwt.generator.js';
|
|
12
|
+
// Security
|
|
13
|
+
export { generateSecurityReport, formatSecurityReport } from './security/report.js';
|
|
14
|
+
// Utils
|
|
15
|
+
export { toKebabCase, toCamelCase, toSnakeCase, pluralize } from './utils.js';
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,6CAA6C;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAE1C,UAAU;AACV,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAEvE,WAAW;AACX,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGpF,QAAQ;AACR,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GyxerProject } from '@gyxer-studio/schema';
|
|
2
|
+
/**
|
|
3
|
+
* Generate all files needed for JWT authentication module.
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateAuthJwtFiles(project: GyxerProject): Map<string, string>;
|
|
6
|
+
/**
|
|
7
|
+
* Returns the additional Prisma fields that need to be added to the User model
|
|
8
|
+
* when auth-jwt module is enabled.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getAuthPrismaFields(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Returns additional env variables needed for auth-jwt.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAuthEnvVars(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Returns additional npm dependencies for auth-jwt.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getAuthDependencies(): Record<string, string>;
|
|
19
|
+
export declare function getAuthDevDependencies(): Record<string, string>;
|
|
20
|
+
//# sourceMappingURL=auth-jwt.generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-jwt.generator.d.ts","sourceRoot":"","sources":["../../src/modules/auth-jwt.generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAuB/E;AA0YD;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAMvC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ5D;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK/D"}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate all files needed for JWT authentication module.
|
|
3
|
+
*/
|
|
4
|
+
export function generateAuthJwtFiles(project) {
|
|
5
|
+
const files = new Map();
|
|
6
|
+
// Auth module
|
|
7
|
+
files.set('src/auth/auth.module.ts', generateAuthModule());
|
|
8
|
+
files.set('src/auth/auth.service.ts', generateAuthService());
|
|
9
|
+
files.set('src/auth/auth.controller.ts', generateAuthController());
|
|
10
|
+
// DTOs
|
|
11
|
+
files.set('src/auth/dto/register.dto.ts', generateRegisterDto());
|
|
12
|
+
files.set('src/auth/dto/login.dto.ts', generateLoginDto());
|
|
13
|
+
files.set('src/auth/dto/auth-response.dto.ts', generateAuthResponseDto());
|
|
14
|
+
files.set('src/auth/dto/refresh-token.dto.ts', generateRefreshTokenDto());
|
|
15
|
+
// JWT strategy & guard
|
|
16
|
+
files.set('src/auth/strategies/jwt.strategy.ts', generateJwtStrategy());
|
|
17
|
+
files.set('src/auth/guards/jwt-auth.guard.ts', generateJwtAuthGuard());
|
|
18
|
+
// Decorators
|
|
19
|
+
files.set('src/auth/decorators/current-user.decorator.ts', generateCurrentUserDecorator());
|
|
20
|
+
files.set('src/auth/decorators/public.decorator.ts', generatePublicDecorator());
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
// ─── Auth Module ────────────────────────────────────────────
|
|
24
|
+
function generateAuthModule() {
|
|
25
|
+
return `import { Module } from '@nestjs/common';
|
|
26
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
27
|
+
import { PassportModule } from '@nestjs/passport';
|
|
28
|
+
import { AuthService } from './auth.service';
|
|
29
|
+
import { AuthController } from './auth.controller';
|
|
30
|
+
import { JwtStrategy } from './strategies/jwt.strategy';
|
|
31
|
+
|
|
32
|
+
@Module({
|
|
33
|
+
imports: [
|
|
34
|
+
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
35
|
+
JwtModule.register({
|
|
36
|
+
secret: process.env.JWT_SECRET || 'change-me-in-production',
|
|
37
|
+
signOptions: { expiresIn: process.env.JWT_EXPIRES_IN || '15m' },
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
controllers: [AuthController],
|
|
41
|
+
providers: [AuthService, JwtStrategy],
|
|
42
|
+
exports: [AuthService],
|
|
43
|
+
})
|
|
44
|
+
export class AuthModule {}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
// ─── Auth Service ───────────────────────────────────────────
|
|
48
|
+
function generateAuthService() {
|
|
49
|
+
return `import {
|
|
50
|
+
Injectable,
|
|
51
|
+
UnauthorizedException,
|
|
52
|
+
ConflictException,
|
|
53
|
+
} from '@nestjs/common';
|
|
54
|
+
import { JwtService } from '@nestjs/jwt';
|
|
55
|
+
import * as bcrypt from 'bcrypt';
|
|
56
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
57
|
+
|
|
58
|
+
interface JwtPayload {
|
|
59
|
+
sub: number;
|
|
60
|
+
email: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Injectable()
|
|
64
|
+
export class AuthService {
|
|
65
|
+
constructor(
|
|
66
|
+
private readonly prisma: PrismaService,
|
|
67
|
+
private readonly jwtService: JwtService,
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
async register(email: string, password: string, name: string) {
|
|
71
|
+
// Check if user already exists
|
|
72
|
+
const existing = await this.prisma.user.findUnique({ where: { email } });
|
|
73
|
+
if (existing) {
|
|
74
|
+
throw new ConflictException('User with this email already exists');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Hash password
|
|
78
|
+
const passwordHash = await bcrypt.hash(password, 12);
|
|
79
|
+
|
|
80
|
+
// Create user
|
|
81
|
+
const user = await this.prisma.user.create({
|
|
82
|
+
data: { email, name, passwordHash },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return this.generateTokens(user.id, user.email);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async login(email: string, password: string) {
|
|
89
|
+
const user = await this.prisma.user.findUnique({ where: { email } });
|
|
90
|
+
if (!user) {
|
|
91
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
|
95
|
+
if (!isPasswordValid) {
|
|
96
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this.generateTokens(user.id, user.email);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async refreshToken(refreshToken: string) {
|
|
103
|
+
try {
|
|
104
|
+
const payload = this.jwtService.verify<JwtPayload>(refreshToken, {
|
|
105
|
+
secret: process.env.JWT_REFRESH_SECRET || 'change-me-refresh-secret',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const user = await this.prisma.user.findUnique({
|
|
109
|
+
where: { id: payload.sub },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!user) {
|
|
113
|
+
throw new UnauthorizedException('User not found');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return this.generateTokens(user.id, user.email);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new UnauthorizedException('Invalid refresh token');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getProfile(userId: number) {
|
|
123
|
+
const user = await this.prisma.user.findUnique({
|
|
124
|
+
where: { id: userId },
|
|
125
|
+
select: {
|
|
126
|
+
id: true,
|
|
127
|
+
email: true,
|
|
128
|
+
name: true,
|
|
129
|
+
createdAt: true,
|
|
130
|
+
updatedAt: true,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!user) {
|
|
135
|
+
throw new UnauthorizedException('User not found');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return user;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private generateTokens(userId: number, email: string) {
|
|
142
|
+
const payload: JwtPayload = { sub: userId, email };
|
|
143
|
+
|
|
144
|
+
const accessToken = this.jwtService.sign(payload);
|
|
145
|
+
const refreshToken = this.jwtService.sign(payload, {
|
|
146
|
+
secret: process.env.JWT_REFRESH_SECRET || 'change-me-refresh-secret',
|
|
147
|
+
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return { accessToken, refreshToken };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
// ─── Auth Controller ────────────────────────────────────────
|
|
156
|
+
function generateAuthController() {
|
|
157
|
+
return `import {
|
|
158
|
+
Controller,
|
|
159
|
+
Post,
|
|
160
|
+
Get,
|
|
161
|
+
Body,
|
|
162
|
+
UseGuards,
|
|
163
|
+
HttpCode,
|
|
164
|
+
HttpStatus,
|
|
165
|
+
} from '@nestjs/common';
|
|
166
|
+
import {
|
|
167
|
+
ApiTags,
|
|
168
|
+
ApiOperation,
|
|
169
|
+
ApiResponse,
|
|
170
|
+
ApiBearerAuth,
|
|
171
|
+
} from '@nestjs/swagger';
|
|
172
|
+
import { AuthService } from './auth.service';
|
|
173
|
+
import { RegisterDto } from './dto/register.dto';
|
|
174
|
+
import { LoginDto } from './dto/login.dto';
|
|
175
|
+
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
|
176
|
+
import { AuthResponseDto } from './dto/auth-response.dto';
|
|
177
|
+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|
178
|
+
import { CurrentUser } from './decorators/current-user.decorator';
|
|
179
|
+
import { Public } from './decorators/public.decorator';
|
|
180
|
+
|
|
181
|
+
@ApiTags('auth')
|
|
182
|
+
@Controller('auth')
|
|
183
|
+
export class AuthController {
|
|
184
|
+
constructor(private readonly authService: AuthService) {}
|
|
185
|
+
|
|
186
|
+
@Public()
|
|
187
|
+
@Post('register')
|
|
188
|
+
@ApiOperation({ summary: 'Register a new user' })
|
|
189
|
+
@ApiResponse({ status: 201, description: 'User registered', type: AuthResponseDto })
|
|
190
|
+
@ApiResponse({ status: 409, description: 'Email already taken' })
|
|
191
|
+
async register(@Body() dto: RegisterDto) {
|
|
192
|
+
return this.authService.register(dto.email, dto.password, dto.name);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Public()
|
|
196
|
+
@Post('login')
|
|
197
|
+
@HttpCode(HttpStatus.OK)
|
|
198
|
+
@ApiOperation({ summary: 'Login with email and password' })
|
|
199
|
+
@ApiResponse({ status: 200, description: 'Login successful', type: AuthResponseDto })
|
|
200
|
+
@ApiResponse({ status: 401, description: 'Invalid credentials' })
|
|
201
|
+
async login(@Body() dto: LoginDto) {
|
|
202
|
+
return this.authService.login(dto.email, dto.password);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Public()
|
|
206
|
+
@Post('refresh')
|
|
207
|
+
@HttpCode(HttpStatus.OK)
|
|
208
|
+
@ApiOperation({ summary: 'Refresh access token' })
|
|
209
|
+
@ApiResponse({ status: 200, description: 'Tokens refreshed', type: AuthResponseDto })
|
|
210
|
+
@ApiResponse({ status: 401, description: 'Invalid refresh token' })
|
|
211
|
+
async refresh(@Body() dto: RefreshTokenDto) {
|
|
212
|
+
return this.authService.refreshToken(dto.refreshToken);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@Get('profile')
|
|
216
|
+
@UseGuards(JwtAuthGuard)
|
|
217
|
+
@ApiBearerAuth()
|
|
218
|
+
@ApiOperation({ summary: 'Get current user profile' })
|
|
219
|
+
@ApiResponse({ status: 200, description: 'User profile' })
|
|
220
|
+
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
221
|
+
async getProfile(@CurrentUser('sub') userId: number) {
|
|
222
|
+
return this.authService.getProfile(userId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
// ─── DTOs ───────────────────────────────────────────────────
|
|
228
|
+
function generateRegisterDto() {
|
|
229
|
+
return `import { ApiProperty } from '@nestjs/swagger';
|
|
230
|
+
import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator';
|
|
231
|
+
|
|
232
|
+
export class RegisterDto {
|
|
233
|
+
@ApiProperty({ example: 'user@example.com' })
|
|
234
|
+
@IsEmail()
|
|
235
|
+
email: string;
|
|
236
|
+
|
|
237
|
+
@ApiProperty({ example: 'strongPassword123' })
|
|
238
|
+
@IsString()
|
|
239
|
+
@MinLength(8)
|
|
240
|
+
password: string;
|
|
241
|
+
|
|
242
|
+
@ApiProperty({ example: 'John Doe' })
|
|
243
|
+
@IsString()
|
|
244
|
+
@IsNotEmpty()
|
|
245
|
+
name: string;
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
function generateLoginDto() {
|
|
250
|
+
return `import { ApiProperty } from '@nestjs/swagger';
|
|
251
|
+
import { IsEmail, IsString, IsNotEmpty } from 'class-validator';
|
|
252
|
+
|
|
253
|
+
export class LoginDto {
|
|
254
|
+
@ApiProperty({ example: 'user@example.com' })
|
|
255
|
+
@IsEmail()
|
|
256
|
+
email: string;
|
|
257
|
+
|
|
258
|
+
@ApiProperty({ example: 'strongPassword123' })
|
|
259
|
+
@IsString()
|
|
260
|
+
@IsNotEmpty()
|
|
261
|
+
password: string;
|
|
262
|
+
}
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
function generateAuthResponseDto() {
|
|
266
|
+
return `import { ApiProperty } from '@nestjs/swagger';
|
|
267
|
+
|
|
268
|
+
export class AuthResponseDto {
|
|
269
|
+
@ApiProperty({ description: 'JWT access token (short-lived)' })
|
|
270
|
+
accessToken: string;
|
|
271
|
+
|
|
272
|
+
@ApiProperty({ description: 'JWT refresh token (long-lived)' })
|
|
273
|
+
refreshToken: string;
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
function generateRefreshTokenDto() {
|
|
278
|
+
return `import { ApiProperty } from '@nestjs/swagger';
|
|
279
|
+
import { IsString, IsNotEmpty } from 'class-validator';
|
|
280
|
+
|
|
281
|
+
export class RefreshTokenDto {
|
|
282
|
+
@ApiProperty({ description: 'Refresh token from login/register response' })
|
|
283
|
+
@IsString()
|
|
284
|
+
@IsNotEmpty()
|
|
285
|
+
refreshToken: string;
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
// ─── JWT Strategy ───────────────────────────────────────────
|
|
290
|
+
function generateJwtStrategy() {
|
|
291
|
+
return `import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
292
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
293
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
294
|
+
import { PrismaService } from '../../prisma/prisma.service';
|
|
295
|
+
|
|
296
|
+
interface JwtPayload {
|
|
297
|
+
sub: number;
|
|
298
|
+
email: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@Injectable()
|
|
302
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
303
|
+
constructor(private readonly prisma: PrismaService) {
|
|
304
|
+
super({
|
|
305
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
306
|
+
ignoreExpiration: false,
|
|
307
|
+
secretOrKey: process.env.JWT_SECRET || 'change-me-in-production',
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async validate(payload: JwtPayload) {
|
|
312
|
+
const user = await this.prisma.user.findUnique({
|
|
313
|
+
where: { id: payload.sub },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (!user) {
|
|
317
|
+
throw new UnauthorizedException('User not found');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { sub: user.id, email: user.email };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
// ─── Guards ─────────────────────────────────────────────────
|
|
326
|
+
function generateJwtAuthGuard() {
|
|
327
|
+
return `import { Injectable, ExecutionContext } from '@nestjs/common';
|
|
328
|
+
import { AuthGuard } from '@nestjs/passport';
|
|
329
|
+
import { Reflector } from '@nestjs/core';
|
|
330
|
+
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
|
331
|
+
|
|
332
|
+
@Injectable()
|
|
333
|
+
export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
334
|
+
constructor(private reflector: Reflector) {
|
|
335
|
+
super();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
canActivate(context: ExecutionContext) {
|
|
339
|
+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
340
|
+
context.getHandler(),
|
|
341
|
+
context.getClass(),
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
if (isPublic) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return super.canActivate(context);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
// ─── Decorators ─────────────────────────────────────────────
|
|
354
|
+
function generateCurrentUserDecorator() {
|
|
355
|
+
return `import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Extract the current authenticated user from the request.
|
|
359
|
+
*
|
|
360
|
+
* Usage:
|
|
361
|
+
* @CurrentUser() user — full user payload { sub, email }
|
|
362
|
+
* @CurrentUser('sub') userId — just the user ID
|
|
363
|
+
* @CurrentUser('email') email — just the email
|
|
364
|
+
*/
|
|
365
|
+
export const CurrentUser = createParamDecorator(
|
|
366
|
+
(data: string | undefined, ctx: ExecutionContext) => {
|
|
367
|
+
const request = ctx.switchToHttp().getRequest();
|
|
368
|
+
const user = request.user;
|
|
369
|
+
|
|
370
|
+
if (data) {
|
|
371
|
+
return user?.[data];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return user;
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
function generatePublicDecorator() {
|
|
380
|
+
return `import { SetMetadata } from '@nestjs/common';
|
|
381
|
+
|
|
382
|
+
export const IS_PUBLIC_KEY = 'isPublic';
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Mark an endpoint as public (no JWT required).
|
|
386
|
+
*
|
|
387
|
+
* Usage:
|
|
388
|
+
* @Public()
|
|
389
|
+
* @Get('health')
|
|
390
|
+
* healthCheck() { ... }
|
|
391
|
+
*/
|
|
392
|
+
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
// ─── Extra Prisma fields for User entity when auth is enabled ──
|
|
396
|
+
/**
|
|
397
|
+
* Returns the additional Prisma fields that need to be added to the User model
|
|
398
|
+
* when auth-jwt module is enabled.
|
|
399
|
+
*/
|
|
400
|
+
export function getAuthPrismaFields() {
|
|
401
|
+
return ` passwordHash String @map("password_hash")`;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Returns additional env variables needed for auth-jwt.
|
|
405
|
+
*/
|
|
406
|
+
export function getAuthEnvVars() {
|
|
407
|
+
return `JWT_SECRET=change-me-in-production
|
|
408
|
+
JWT_EXPIRES_IN=15m
|
|
409
|
+
JWT_REFRESH_SECRET=change-me-refresh-secret
|
|
410
|
+
JWT_REFRESH_EXPIRES_IN=7d
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Returns additional npm dependencies for auth-jwt.
|
|
415
|
+
*/
|
|
416
|
+
export function getAuthDependencies() {
|
|
417
|
+
return {
|
|
418
|
+
'@nestjs/jwt': '^10.2.0',
|
|
419
|
+
'@nestjs/passport': '^10.0.0',
|
|
420
|
+
'passport': '^0.7.0',
|
|
421
|
+
'passport-jwt': '^4.0.1',
|
|
422
|
+
'bcrypt': '^5.1.0',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
export function getAuthDevDependencies() {
|
|
426
|
+
return {
|
|
427
|
+
'@types/passport-jwt': '^4.0.0',
|
|
428
|
+
'@types/bcrypt': '^5.0.0',
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
//# sourceMappingURL=auth-jwt.generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-jwt.generator.js","sourceRoot":"","sources":["../../src/modules/auth-jwt.generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GyxerProject } from '@gyxer-studio/schema';
|
|
2
|
+
import type { SecurityReport } from './security/report.js';
|
|
3
|
+
export interface GenerateOptions {
|
|
4
|
+
outputDir: string;
|
|
5
|
+
silent?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface GenerateResult {
|
|
8
|
+
outputDir: string;
|
|
9
|
+
filesCreated: string[];
|
|
10
|
+
securityReport: SecurityReport;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate a complete NestJS project from a GyxerProject schema.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateProject(project: GyxerProject, options: GenerateOptions): Promise<GenerateResult>;
|
|
16
|
+
//# sourceMappingURL=project-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-generator.d.ts","sourceRoot":"","sources":["../src/project-generator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAqBzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAQ3D,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,cAAc,CAAC;CAChC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CA4KzB"}
|