@kuldi/create-nestjs 1.0.1 → 1.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.
Files changed (68) hide show
  1. package/README.md +27 -0
  2. package/package.json +13 -3
  3. package/src/cli.js +139 -0
  4. package/template/.editorconfig +12 -0
  5. package/template/.env.example +23 -0
  6. package/template/.eslintrc.js +25 -0
  7. package/template/.prettierrc +8 -0
  8. package/template/README.md +133 -0
  9. package/template/nest-cli.json +10 -0
  10. package/template/package-lock.json +11539 -0
  11. package/template/package.json +99 -0
  12. package/template/prisma/migrations/20260625045841_init/migration.sql +73 -0
  13. package/template/prisma/migrations/migration_lock.toml +3 -0
  14. package/template/prisma/schema.prisma +75 -0
  15. package/template/prisma/seeder/data/permission.seed.ts +67 -0
  16. package/template/prisma/seeder/data/position.seed.ts +21 -0
  17. package/template/prisma/seeder/data/user.seed.ts +39 -0
  18. package/template/prisma/seeder/index.ts +56 -0
  19. package/template/prisma.config.ts +8 -0
  20. package/template/src/app.module.ts +68 -0
  21. package/template/src/common/constants/permissions.constant.ts +27 -0
  22. package/template/src/common/decorators/api-response.decorator.ts +44 -0
  23. package/template/src/common/decorators/get-user.decorator.ts +11 -0
  24. package/template/src/common/decorators/permissions.decorator.ts +5 -0
  25. package/template/src/common/decorators/public.decorator.ts +4 -0
  26. package/template/src/common/dto/api-response.dto.ts +15 -0
  27. package/template/src/common/dto/pagination.dto.ts +33 -0
  28. package/template/src/common/filters/http-exception.filter.ts +54 -0
  29. package/template/src/common/guards/jwt-auth.guard.ts +32 -0
  30. package/template/src/common/guards/permissions.guard.ts +53 -0
  31. package/template/src/common/helpers/function/error-helper.ts +35 -0
  32. package/template/src/common/interceptors/logging.interceptor.ts +37 -0
  33. package/template/src/common/interceptors/transform.interceptor.ts +53 -0
  34. package/template/src/common/prisma/prisma.module.ts +9 -0
  35. package/template/src/common/prisma/prisma.service.ts +52 -0
  36. package/template/src/common/utils/password.util.ts +13 -0
  37. package/template/src/config/app.config.ts +10 -0
  38. package/template/src/config/database.config.ts +5 -0
  39. package/template/src/config/env.validation.ts +30 -0
  40. package/template/src/config/jwt.config.ts +8 -0
  41. package/template/src/config/swagger.config.ts +6 -0
  42. package/template/src/main.ts +84 -0
  43. package/template/src/modules/auth/auth.module.ts +28 -0
  44. package/template/src/modules/auth/auth.service.ts +173 -0
  45. package/template/src/modules/auth/controllers/v1/auth.controller.ts +71 -0
  46. package/template/src/modules/auth/core/dto/auth-response.dto.ts +19 -0
  47. package/template/src/modules/auth/core/dto/login-response.dto.ts +10 -0
  48. package/template/src/modules/auth/core/dto/login.dto.ts +15 -0
  49. package/template/src/modules/auth/core/dto/register.dto.ts +30 -0
  50. package/template/src/modules/auth/core/interfaces/jwt-payload.interface.ts +7 -0
  51. package/template/src/modules/auth/core/strategies/jwt.strategy.ts +59 -0
  52. package/template/src/modules/health/health.controller.ts +29 -0
  53. package/template/src/modules/health/health.module.ts +7 -0
  54. package/template/src/modules/users/controllers/v1/users.controller.ts +120 -0
  55. package/template/src/modules/users/core/dto/change-position.dto.ts +9 -0
  56. package/template/src/modules/users/core/dto/create-user.dto.ts +35 -0
  57. package/template/src/modules/users/core/dto/manage-permissions.dto.ts +13 -0
  58. package/template/src/modules/users/core/dto/update-user.dto.ts +30 -0
  59. package/template/src/modules/users/core/dto/user-query.dto.ts +22 -0
  60. package/template/src/modules/users/core/dto/user-response.dto.ts +32 -0
  61. package/template/src/modules/users/core/entities/user.entity.ts +45 -0
  62. package/template/src/modules/users/core/helpers/user-transform.helper.ts +31 -0
  63. package/template/src/modules/users/users.module.ts +10 -0
  64. package/template/src/modules/users/users.service.ts +344 -0
  65. package/template/test/app.e2e-spec.ts +40 -0
  66. package/template/test/jest-e2e.json +9 -0
  67. package/template/tsconfig.json +30 -0
  68. package/bin/cli.js +0 -71
@@ -0,0 +1,54 @@
1
+ import { ApiResponseDto } from '@common/dto/api-response.dto';
2
+ import { Response } from 'express';
3
+ import {
4
+ Catch,
5
+ Logger,
6
+ HttpStatus,
7
+ ArgumentsHost,
8
+ HttpException,
9
+ ExceptionFilter,
10
+ } from '@nestjs/common';
11
+
12
+ @Catch()
13
+ export class HttpExceptionFilter implements ExceptionFilter {
14
+ private readonly logger = new Logger(HttpExceptionFilter.name);
15
+
16
+ catch(exception: unknown, host: ArgumentsHost) {
17
+ const ctx = host.switchToHttp();
18
+ const response = ctx.getResponse<Response>();
19
+ const request = ctx.getRequest<Request>();
20
+
21
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
22
+ let message = 'Internal server error';
23
+ let errors: any = null;
24
+
25
+ if (exception instanceof HttpException) {
26
+ status = exception.getStatus();
27
+ const exceptionResponse = exception.getResponse();
28
+
29
+ if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
30
+ message = (exceptionResponse as any).message || exception.message;
31
+ errors = (exceptionResponse as any).errors || null;
32
+ } else {
33
+ message = exceptionResponse as string;
34
+ }
35
+ } else if (exception instanceof Error) {
36
+ message = exception.message;
37
+ }
38
+
39
+ // Log error for monitoring
40
+ this.logger.error(
41
+ `${request.method} ${request.url} - Status: ${status} - Message: ${message}`,
42
+ exception instanceof Error ? exception.stack : undefined,
43
+ );
44
+
45
+ const errorResponse: ApiResponseDto<null> = {
46
+ statusCode: status,
47
+ message,
48
+ data: null,
49
+ errors,
50
+ };
51
+
52
+ response.status(status).json(errorResponse);
53
+ }
54
+ }
@@ -0,0 +1,32 @@
1
+ import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
2
+ import { IS_PUBLIC_KEY } from '@common/decorators/public.decorator';
3
+ import { AuthGuard } from '@nestjs/passport';
4
+ import { Reflector } from '@nestjs/core';
5
+ import { Observable } from 'rxjs';
6
+
7
+ @Injectable()
8
+ export class JwtAuthGuard extends AuthGuard('jwt') {
9
+ constructor(private reflector: Reflector) {
10
+ super();
11
+ }
12
+
13
+ canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
14
+ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
15
+ context.getHandler(),
16
+ context.getClass(),
17
+ ]);
18
+
19
+ if (isPublic) {
20
+ return true;
21
+ }
22
+
23
+ return super.canActivate(context);
24
+ }
25
+
26
+ handleRequest(err: any, user: any, info: any) {
27
+ if (err || !user) {
28
+ throw err || new UnauthorizedException('Invalid or expired token');
29
+ }
30
+ return user;
31
+ }
32
+ }
@@ -0,0 +1,53 @@
1
+ import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
2
+ import { JwtPayload } from '@modules/auth/core/interfaces/jwt-payload.interface';
3
+ import { PERMISSIONS_KEY } from '@common/decorators/permissions.decorator';
4
+ import { IS_PUBLIC_KEY } from '@common/decorators/public.decorator';
5
+ import { Reflector } from '@nestjs/core';
6
+
7
+ @Injectable()
8
+ export class PermissionsGuard implements CanActivate {
9
+ constructor(private reflector: Reflector) {}
10
+
11
+ canActivate(context: ExecutionContext): boolean {
12
+ // Check if route is public
13
+ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
14
+ context.getHandler(),
15
+ context.getClass(),
16
+ ]);
17
+
18
+ if (isPublic) {
19
+ return true;
20
+ }
21
+
22
+ // Get required permissions from decorator
23
+ const requiredPermissions = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
24
+ context.getHandler(),
25
+ context.getClass(),
26
+ ]);
27
+
28
+ if (!requiredPermissions || requiredPermissions.length === 0) {
29
+ return true;
30
+ }
31
+
32
+ // Get user from request
33
+ const request = context.switchToHttp().getRequest();
34
+ const user: JwtPayload = request.user;
35
+
36
+ if (!user) {
37
+ throw new ForbiddenException('User not authenticated');
38
+ }
39
+
40
+ // Check if user has required permissions
41
+ const hasPermission = requiredPermissions.every((permission) =>
42
+ user.permissions.includes(permission),
43
+ );
44
+
45
+ if (!hasPermission) {
46
+ throw new ForbiddenException(
47
+ `You do not have the required permissions: ${requiredPermissions.join(', ')}`,
48
+ );
49
+ }
50
+
51
+ return true;
52
+ }
53
+ }
@@ -0,0 +1,35 @@
1
+ import { Prisma } from '@prisma/client';
2
+
3
+ export function getErrorMessage(error: unknown): string {
4
+ if (error instanceof Error) {
5
+ return error.message;
6
+ }
7
+
8
+ // Prisma known error
9
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
10
+ return (error as any).message;
11
+ }
12
+
13
+ // Prisma validation error
14
+ if (error instanceof Prisma.PrismaClientValidationError) {
15
+ return (error as any).message;
16
+ }
17
+
18
+ // String thrown
19
+ if (typeof error === 'string') {
20
+ return error;
21
+ }
22
+
23
+ // Object with message
24
+ if (
25
+ typeof error === 'object' &&
26
+ error !== null &&
27
+ 'message' in error &&
28
+ typeof (error as any).message === 'string'
29
+ ) {
30
+ return (error as any).message;
31
+ }
32
+
33
+ // Fallback
34
+ return 'Unexpected error occurred';
35
+ }
@@ -0,0 +1,37 @@
1
+ import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
2
+ import { tap } from 'rxjs/operators';
3
+ import { Observable } from 'rxjs';
4
+
5
+ @Injectable()
6
+ export class LoggingInterceptor implements NestInterceptor {
7
+ private readonly logger = new Logger('HTTP');
8
+
9
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
10
+ const request = context.switchToHttp().getRequest();
11
+ const { method, url, body, user } = request;
12
+ const now = Date.now();
13
+
14
+ const userId = user?.userId || 'anonymous';
15
+
16
+ this.logger.log(
17
+ `→ [${method}] ${url} - User: ${userId} ${
18
+ Object.keys(body || {}).length > 0 ? `- Body: ${JSON.stringify(body)}` : ''
19
+ }`,
20
+ );
21
+
22
+ return next.handle().pipe(
23
+ tap({
24
+ next: () => {
25
+ const responseTime = Date.now() - now;
26
+ this.logger.log(`← [${method}] ${url} - ${responseTime}ms - User: ${userId}`);
27
+ },
28
+ error: (error) => {
29
+ const responseTime = Date.now() - now;
30
+ this.logger.error(
31
+ `← [${method}] ${url} - ${responseTime}ms - User: ${userId} - Error: ${error.message}`,
32
+ );
33
+ },
34
+ }),
35
+ );
36
+ }
37
+ }
@@ -0,0 +1,53 @@
1
+ import { ApiResponseDto } from '@common/dto/api-response.dto';
2
+ import { map } from 'rxjs/operators';
3
+ import { Observable } from 'rxjs';
4
+ import {
5
+ Injectable,
6
+ HttpStatus,
7
+ CallHandler,
8
+ NestInterceptor,
9
+ ExecutionContext,
10
+ } from '@nestjs/common';
11
+
12
+ @Injectable()
13
+ export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponseDto<T>> {
14
+ intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponseDto<T>> {
15
+ const request = context.switchToHttp().getRequest();
16
+ const response = context.switchToHttp().getResponse();
17
+
18
+ return next.handle().pipe(
19
+ map((data) => {
20
+ // If data is already in ApiResponseDto format, return as is
21
+ if (data && typeof data === 'object' && 'statusCode' in data && 'message' in data) {
22
+ return data;
23
+ }
24
+
25
+ // Default success message based on HTTP method
26
+ let message = 'Operation successful';
27
+
28
+ switch (request.method) {
29
+ case 'POST':
30
+ message = 'Resource created successfully';
31
+ response.status(HttpStatus.CREATED);
32
+ break;
33
+ case 'PUT':
34
+ case 'PATCH':
35
+ message = 'Resource updated successfully';
36
+ break;
37
+ case 'DELETE':
38
+ message = 'Resource deleted successfully';
39
+ break;
40
+ case 'GET':
41
+ message = 'Data retrieved successfully';
42
+ break;
43
+ }
44
+
45
+ return {
46
+ statusCode: response.statusCode || HttpStatus.OK,
47
+ message,
48
+ data,
49
+ };
50
+ }),
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,9 @@
1
+ import { PrismaService } from './prisma.service';
2
+ import { Global, Module } from '@nestjs/common';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [PrismaService],
7
+ exports: [PrismaService],
8
+ })
9
+ export class PrismaModule {}
@@ -0,0 +1,52 @@
1
+ import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
2
+ import { PrismaClient } from '@prisma/client';
3
+ import { PrismaPg } from '@prisma/adapter-pg';
4
+ import { Pool } from 'pg';
5
+
6
+ @Injectable()
7
+ export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
8
+ private readonly logger = new Logger(PrismaService.name);
9
+
10
+ constructor() {
11
+ const connectionString = process.env.DATABASE_URL;
12
+ const pool = new Pool({ connectionString });
13
+ const adapter = new PrismaPg(pool as any);
14
+
15
+ super({
16
+ adapter,
17
+ log: [
18
+ { level: 'error', emit: 'stdout' },
19
+ { level: 'warn', emit: 'stdout' },
20
+ ],
21
+ });
22
+ }
23
+
24
+ async onModuleInit() {
25
+ await this.$connect();
26
+ this.logger.log('āœ… Database connected successfully');
27
+
28
+ // Log queries in development
29
+ if (process.env.NODE_ENV === 'development') {
30
+ // @ts-ignore
31
+ this.$on('query', (e) => {
32
+ this.logger.debug(`Query: ${e.query}`);
33
+ this.logger.debug(`Duration: ${e.duration}ms`);
34
+ });
35
+ }
36
+ }
37
+
38
+ async onModuleDestroy() {
39
+ await this.$disconnect();
40
+ this.logger.log('Database disconnected');
41
+ }
42
+
43
+ async cleanDatabase() {
44
+ if (process.env.NODE_ENV === 'production') {
45
+ throw new Error('Cannot clean database in production');
46
+ }
47
+
48
+ const models = Reflect.ownKeys(this).filter((key) => key[0] !== '_');
49
+
50
+ return Promise.all(models.map((modelKey) => (this as any)[modelKey].deleteMany()));
51
+ }
52
+ }
@@ -0,0 +1,13 @@
1
+ import * as bcrypt from 'bcrypt';
2
+
3
+ export class PasswordUtil {
4
+ private static readonly SALT_ROUNDS = 10;
5
+
6
+ static async hash(password: string): Promise<string> {
7
+ return bcrypt.hash(password, this.SALT_ROUNDS);
8
+ }
9
+
10
+ static async compare(password: string, hashedPassword: string): Promise<boolean> {
11
+ return bcrypt.compare(password, hashedPassword);
12
+ }
13
+ }
@@ -0,0 +1,10 @@
1
+ import { registerAs } from '@nestjs/config';
2
+
3
+ export default registerAs('app', () => ({
4
+ env: process.env.NODE_ENV || 'development',
5
+ port: parseInt(process.env.PORT, 10) || 3000,
6
+ name: process.env.APP_NAME,
7
+ apiPrefix: process.env.API_PREFIX || 'api',
8
+
9
+ corsOrigin: process.env.CORS_ORIGIN || '*',
10
+ }));
@@ -0,0 +1,5 @@
1
+ import { registerAs } from '@nestjs/config';
2
+
3
+ export default registerAs('database', () => ({
4
+ url: process.env.DATABASE_URL,
5
+ }));
@@ -0,0 +1,30 @@
1
+ import * as Joi from 'joi';
2
+
3
+ export const validationSchema = Joi.object({
4
+ // Application
5
+ NODE_ENV: Joi.string()
6
+ .valid('development', 'production', 'test', 'staging')
7
+ .default('development'),
8
+ PORT: Joi.number().default(3000),
9
+ APP_NAME: Joi.string().required(),
10
+
11
+ // Database
12
+ DATABASE_URL: Joi.string().required(),
13
+
14
+ // JWT
15
+ JWT_SECRET: Joi.string().required(),
16
+ JWT_EXPIRATION: Joi.string().default('7d'),
17
+ JWT_REFRESH_SECRET: Joi.string().required(),
18
+ JWT_REFRESH_EXPIRATION: Joi.string().default('30d'),
19
+
20
+ // CORS
21
+ CORS_ORIGIN: Joi.string().default('*'),
22
+
23
+ // API
24
+ API_PREFIX: Joi.string().default('api'),
25
+
26
+
27
+ // Swagger
28
+ SWAGGER_ENABLED: Joi.boolean().default(true),
29
+ SWAGGER_PATH: Joi.string().default('api-docs'),
30
+ });
@@ -0,0 +1,8 @@
1
+ import { registerAs } from '@nestjs/config';
2
+
3
+ export default registerAs('jwt', () => ({
4
+ secret: process.env.JWT_SECRET,
5
+ expiresIn: process.env.JWT_EXPIRATION || '7d',
6
+ refreshSecret: process.env.JWT_REFRESH_SECRET,
7
+ refreshExpiresIn: process.env.JWT_REFRESH_EXPIRATION || '30d',
8
+ }));
@@ -0,0 +1,6 @@
1
+ import { registerAs } from '@nestjs/config';
2
+
3
+ export default registerAs('swagger', () => ({
4
+ enabled: process.env.SWAGGER_ENABLED === 'true',
5
+ path: process.env.SWAGGER_PATH || 'docs',
6
+ }));
@@ -0,0 +1,84 @@
1
+ import { ValidationPipe, VersioningType, ClassSerializerInterceptor } from '@nestjs/common';
2
+ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
3
+ import { NestFactory, Reflector } from '@nestjs/core';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import cookieParser = require('cookie-parser');
6
+ import { AppModule } from './app.module';
7
+
8
+ async function bootstrap() {
9
+ const app = await NestFactory.create(AppModule);
10
+
11
+ const configService = app.get(ConfigService);
12
+
13
+ // Get configurations
14
+ const swaggerEnabled = configService.get<boolean>('swagger.enabled');
15
+ const corsOrigin = configService.get<string>('app.corsOrigin');
16
+ const swaggerPath = configService.get<string>('swagger.path');
17
+ const apiPrefix = configService.get<string>('app.apiPrefix');
18
+ const port = configService.get<number>('app.port');
19
+
20
+ // Enable CORS
21
+ app.enableCors({
22
+ origin: corsOrigin,
23
+ credentials: true,
24
+ });
25
+
26
+ app.use(cookieParser());
27
+
28
+ // Global prefix
29
+ app.setGlobalPrefix(apiPrefix);
30
+
31
+ // API Versioning
32
+ app.enableVersioning({
33
+ type: VersioningType.URI,
34
+ defaultVersion: '1',
35
+ });
36
+
37
+ // Global validation pipe
38
+ app.useGlobalPipes(
39
+ new ValidationPipe({
40
+ whitelist: true,
41
+ forbidNonWhitelisted: true,
42
+ transform: true,
43
+ transformOptions: {
44
+ enableImplicitConversion: true,
45
+ },
46
+ }),
47
+ );
48
+
49
+ // Class serializer for excluding fields (like password)
50
+ app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
51
+
52
+ // Swagger Documentation
53
+ if (swaggerEnabled) {
54
+ const config = new DocumentBuilder()
55
+ .setTitle('Kuli Digital Standard API')
56
+ .setDescription('Kuli Digital NestJS Backend API Documentation')
57
+ .setVersion('1.0')
58
+ .addTag('Authentication', 'Authentication endpoints')
59
+ .addTag('Users', 'User management endpoints')
60
+ .addTag('Health', 'Health check endpoints')
61
+ .build();
62
+
63
+ const document = SwaggerModule.createDocument(app, config);
64
+ SwaggerModule.setup(swaggerPath, app, document, {
65
+ useGlobalPrefix: true,
66
+ swaggerOptions: {
67
+ persistAuthorization: true,
68
+ defaultModelsExpandDepth: -1,
69
+ },
70
+ });
71
+ }
72
+
73
+ await app.listen(port);
74
+
75
+ console.log(`\nšŸš€ Application is running on: http://localhost:${port}/`);
76
+ console.log(`šŸŒ Environment: ${configService.get<string>('app.env')}\n`);
77
+ if (swaggerEnabled) {
78
+ console.log(`šŸ“š Swagger Documentaion on : http://localhost:${port}/api/${swaggerPath}`);
79
+ }
80
+ console.log(`šŸ“Š Health check: http://localhost:${port}/health`);
81
+ console.log(`šŸ“ Ping endpoint: http://localhost:${port}/ping\n`);
82
+ }
83
+
84
+ bootstrap();
@@ -0,0 +1,28 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { JwtModule } from '@nestjs/jwt';
3
+ import { PassportModule } from '@nestjs/passport';
4
+ import { ConfigModule, ConfigService } from '@nestjs/config';
5
+ import { AuthService } from './auth.service';
6
+ import { AuthController } from './controllers/v1/auth.controller';
7
+ import { JwtStrategy } from './core/strategies/jwt.strategy';
8
+ import type { StringValue } from 'ms';
9
+
10
+ @Module({
11
+ imports: [
12
+ PassportModule.register({ defaultStrategy: 'jwt' }),
13
+ JwtModule.registerAsync({
14
+ imports: [ConfigModule],
15
+ inject: [ConfigService],
16
+ useFactory: (configService: ConfigService) => ({
17
+ secret: configService.get<string>('jwt.secret'),
18
+ signOptions: {
19
+ expiresIn: configService.get('jwt.expiresIn', '7d') as StringValue,
20
+ },
21
+ }),
22
+ }),
23
+ ],
24
+ controllers: [AuthController],
25
+ providers: [AuthService, JwtStrategy],
26
+ exports: [JwtStrategy, PassportModule, JwtModule],
27
+ })
28
+ export class AuthModule {}