@urbansolv/create-nestjs-app 1.2.4 → 1.2.7
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/dist/templates/nestjs-app/src/common/dto/api-response.dto.ts +2 -2
- package/dist/templates/nestjs-app/src/common/filters/http-exception.filter.ts +1 -1
- package/dist/templates/nestjs-app/src/common/interceptors/transform.interceptor.ts +2 -2
- package/dist/templates/nestjs-app/src/common/prisma/prisma.service.ts +0 -1
- package/dist/templates/nestjs-app/src/modules/auth/auth.service.ts +0 -7
- package/dist/templates/nestjs-app/src/modules/auth/controllers/v1/auth.controller.ts +4 -0
- package/dist/templates/nestjs-app/src/modules/users/controllers/v1/users.controller.ts +2 -2
- package/dist/templates/nestjs-app/src/modules/users/users.service.ts +4 -0
- package/package.json +2 -1
- package/templates/nestjs-app/.editorconfig +12 -0
- package/templates/nestjs-app/.env.example +24 -0
- package/templates/nestjs-app/.eslintrc.js +25 -0
- package/templates/nestjs-app/.prettierrc +8 -0
- package/templates/nestjs-app/README.md +133 -0
- package/templates/nestjs-app/nest-cli.json +10 -0
- package/templates/nestjs-app/package.json +88 -0
- package/templates/nestjs-app/prisma/schema.prisma +79 -0
- package/templates/nestjs-app/prisma/seed.ts +153 -0
- package/templates/nestjs-app/src/app.module.ts +68 -0
- package/templates/nestjs-app/src/common/constants/permissions.constant.ts +27 -0
- package/templates/nestjs-app/src/common/decorators/api-response.decorator.ts +44 -0
- package/templates/nestjs-app/src/common/decorators/get-user.decorator.ts +11 -0
- package/templates/nestjs-app/src/common/decorators/permissions.decorator.ts +5 -0
- package/templates/nestjs-app/src/common/decorators/public.decorator.ts +4 -0
- package/templates/nestjs-app/src/common/dto/api-response.dto.ts +21 -0
- package/templates/nestjs-app/src/common/dto/pagination.dto.ts +33 -0
- package/templates/nestjs-app/src/common/filters/http-exception.filter.ts +56 -0
- package/templates/nestjs-app/src/common/guards/jwt-auth.guard.ts +32 -0
- package/templates/nestjs-app/src/common/guards/permissions.guard.ts +53 -0
- package/templates/nestjs-app/src/common/interceptors/logging.interceptor.ts +37 -0
- package/templates/nestjs-app/src/common/interceptors/transform.interceptor.ts +55 -0
- package/templates/nestjs-app/src/common/prisma/prisma.module.ts +9 -0
- package/templates/nestjs-app/src/common/prisma/prisma.service.ts +45 -0
- package/templates/nestjs-app/src/common/utils/password.util.ts +13 -0
- package/templates/nestjs-app/src/config/app.config.ts +10 -0
- package/templates/nestjs-app/src/config/database.config.ts +5 -0
- package/templates/nestjs-app/src/config/env.validation.ts +30 -0
- package/templates/nestjs-app/src/config/jwt.config.ts +8 -0
- package/templates/nestjs-app/src/config/swagger.config.ts +6 -0
- package/templates/nestjs-app/src/main.ts +93 -0
- package/templates/nestjs-app/src/modules/auth/auth.module.ts +28 -0
- package/templates/nestjs-app/src/modules/auth/auth.service.ts +173 -0
- package/templates/nestjs-app/src/modules/auth/controllers/v1/auth.controller.ts +37 -0
- package/templates/nestjs-app/src/modules/auth/core/dto/auth-response.dto.ts +19 -0
- package/templates/nestjs-app/src/modules/auth/core/dto/login-response.dto.ts +10 -0
- package/templates/nestjs-app/src/modules/auth/core/dto/login.dto.ts +15 -0
- package/templates/nestjs-app/src/modules/auth/core/dto/register.dto.ts +30 -0
- package/templates/nestjs-app/src/modules/auth/core/interfaces/jwt-payload.interface.ts +7 -0
- package/templates/nestjs-app/src/modules/auth/core/strategies/jwt.strategy.ts +54 -0
- package/templates/nestjs-app/src/modules/health/health.controller.ts +29 -0
- package/templates/nestjs-app/src/modules/health/health.module.ts +7 -0
- package/templates/nestjs-app/src/modules/users/controllers/v1/users.controller.ts +118 -0
- package/templates/nestjs-app/src/modules/users/core/dto/change-position.dto.ts +9 -0
- package/templates/nestjs-app/src/modules/users/core/dto/create-user.dto.ts +35 -0
- package/templates/nestjs-app/src/modules/users/core/dto/manage-permissions.dto.ts +13 -0
- package/templates/nestjs-app/src/modules/users/core/dto/update-user.dto.ts +30 -0
- package/templates/nestjs-app/src/modules/users/core/dto/user-query.dto.ts +22 -0
- package/templates/nestjs-app/src/modules/users/core/dto/user-response.dto.ts +32 -0
- package/templates/nestjs-app/src/modules/users/core/entities/user.entity.ts +45 -0
- package/templates/nestjs-app/src/modules/users/core/helpers/user-transform.helper.ts +31 -0
- package/templates/nestjs-app/src/modules/users/users.module.ts +10 -0
- package/templates/nestjs-app/src/modules/users/users.service.ts +344 -0
- package/templates/nestjs-app/test/app.e2e-spec.ts +40 -0
- package/templates/nestjs-app/test/jest-e2e.json +9 -0
- package/templates/nestjs-app/tsconfig.json +26 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { APP_GUARD, APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
|
4
|
+
import { validationSchema } from './config/env.validation';
|
|
5
|
+
import appConfig from './config/app.config';
|
|
6
|
+
import databaseConfig from './config/database.config';
|
|
7
|
+
import jwtConfig from './config/jwt.config';
|
|
8
|
+
import swaggerConfig from './config/swagger.config';
|
|
9
|
+
|
|
10
|
+
// Common modules
|
|
11
|
+
import { PrismaModule } from './common/prisma/prisma.module';
|
|
12
|
+
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
|
13
|
+
import { PermissionsGuard } from './common/guards/permissions.guard';
|
|
14
|
+
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
|
15
|
+
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
|
16
|
+
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
|
|
17
|
+
|
|
18
|
+
// Feature modules
|
|
19
|
+
import { AuthModule } from './modules/auth/auth.module';
|
|
20
|
+
import { UsersModule } from './modules/users/users.module';
|
|
21
|
+
import { HealthModule } from './modules/health/health.module';
|
|
22
|
+
|
|
23
|
+
@Module({
|
|
24
|
+
imports: [
|
|
25
|
+
// Configuration
|
|
26
|
+
ConfigModule.forRoot({
|
|
27
|
+
isGlobal: true,
|
|
28
|
+
validationSchema,
|
|
29
|
+
load: [appConfig, databaseConfig, jwtConfig, swaggerConfig],
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
// Common modules
|
|
33
|
+
PrismaModule,
|
|
34
|
+
|
|
35
|
+
// Feature modules
|
|
36
|
+
AuthModule,
|
|
37
|
+
UsersModule,
|
|
38
|
+
HealthModule,
|
|
39
|
+
],
|
|
40
|
+
providers: [
|
|
41
|
+
// Global guards
|
|
42
|
+
{
|
|
43
|
+
provide: APP_GUARD,
|
|
44
|
+
useClass: JwtAuthGuard,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
provide: APP_GUARD,
|
|
48
|
+
useClass: PermissionsGuard,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Global filters
|
|
52
|
+
{
|
|
53
|
+
provide: APP_FILTER,
|
|
54
|
+
useClass: HttpExceptionFilter,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Global interceptors
|
|
58
|
+
{
|
|
59
|
+
provide: APP_INTERCEPTOR,
|
|
60
|
+
useClass: LoggingInterceptor,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
provide: APP_INTERCEPTOR,
|
|
64
|
+
useClass: TransformInterceptor,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
export class AppModule {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const PERMISSIONS = {
|
|
2
|
+
// User Management
|
|
3
|
+
USER: {
|
|
4
|
+
VIEW: 'VIEW_USER',
|
|
5
|
+
ADD: 'ADD_USER',
|
|
6
|
+
UPDATE: 'UPDATE_USER',
|
|
7
|
+
DELETE: 'DELETE_USER',
|
|
8
|
+
MANAGE_PERMISSION: 'MANAGE_USER_PERMISSION',
|
|
9
|
+
CHANGE_POSITION: 'CHANGE_USER_POSITION',
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
// Position Management
|
|
13
|
+
POSITION: {
|
|
14
|
+
VIEW: 'VIEW_POSITION',
|
|
15
|
+
ADD: 'ADD_POSITION',
|
|
16
|
+
UPDATE: 'UPDATE_POSITION',
|
|
17
|
+
DELETE: 'DELETE_POSITION',
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// Permission Management
|
|
21
|
+
PERMISSION: {
|
|
22
|
+
VIEW: 'VIEW_PERMISSION',
|
|
23
|
+
ADD: 'ADD_PERMISSION',
|
|
24
|
+
UPDATE: 'UPDATE_PERMISSION',
|
|
25
|
+
DELETE: 'DELETE_PERMISSION',
|
|
26
|
+
},
|
|
27
|
+
} as const;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { applyDecorators, Type } from '@nestjs/common';
|
|
2
|
+
import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
|
|
3
|
+
import { ApiResponseDto } from '@common/dto/api-response.dto';
|
|
4
|
+
|
|
5
|
+
export const ApiSuccessResponse = <TModel extends Type<any>>(model: TModel) => {
|
|
6
|
+
return applyDecorators(
|
|
7
|
+
ApiExtraModels(ApiResponseDto, model),
|
|
8
|
+
ApiOkResponse({
|
|
9
|
+
schema: {
|
|
10
|
+
allOf: [
|
|
11
|
+
{ $ref: getSchemaPath(ApiResponseDto) },
|
|
12
|
+
{
|
|
13
|
+
properties: {
|
|
14
|
+
data: {
|
|
15
|
+
$ref: getSchemaPath(model),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const ApiSuccessArrayResponse = <TModel extends Type<any>>(model: TModel) => {
|
|
26
|
+
return applyDecorators(
|
|
27
|
+
ApiExtraModels(ApiResponseDto, model),
|
|
28
|
+
ApiOkResponse({
|
|
29
|
+
schema: {
|
|
30
|
+
allOf: [
|
|
31
|
+
{ $ref: getSchemaPath(ApiResponseDto) },
|
|
32
|
+
{
|
|
33
|
+
properties: {
|
|
34
|
+
data: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
items: { $ref: getSchemaPath(model) },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { JwtPayload } from '@modules/auth/core/interfaces/jwt-payload.interface';
|
|
3
|
+
|
|
4
|
+
export const GetUser = createParamDecorator(
|
|
5
|
+
(data: keyof JwtPayload | undefined, ctx: ExecutionContext): JwtPayload | any => {
|
|
6
|
+
const request = ctx.switchToHttp().getRequest();
|
|
7
|
+
const user = request.user;
|
|
8
|
+
|
|
9
|
+
return data ? user?.[data] : user;
|
|
10
|
+
},
|
|
11
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
export class ApiResponseDto<T = any> {
|
|
4
|
+
@ApiProperty({ example: 200 })
|
|
5
|
+
statusCode: number;
|
|
6
|
+
|
|
7
|
+
@ApiProperty({ example: 'Operation successful' })
|
|
8
|
+
message: string;
|
|
9
|
+
|
|
10
|
+
@ApiPropertyOptional()
|
|
11
|
+
data?: T;
|
|
12
|
+
|
|
13
|
+
@ApiPropertyOptional()
|
|
14
|
+
errors?: any;
|
|
15
|
+
|
|
16
|
+
@ApiProperty({ example: '2026-01-01T00:00:00.000Z' })
|
|
17
|
+
timestamp: string;
|
|
18
|
+
|
|
19
|
+
@ApiProperty({ example: '/api/v1/users' })
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { Type } from 'class-transformer';
|
|
3
|
+
import { IsOptional, IsInt, Min, Max } from 'class-validator';
|
|
4
|
+
|
|
5
|
+
export class PaginationDto {
|
|
6
|
+
@ApiPropertyOptional({ minimum: 1, default: 1 })
|
|
7
|
+
@Type(() => Number)
|
|
8
|
+
@IsInt()
|
|
9
|
+
@Min(1)
|
|
10
|
+
@IsOptional()
|
|
11
|
+
page?: number = 1;
|
|
12
|
+
|
|
13
|
+
@ApiPropertyOptional({ minimum: 1, maximum: 100, default: 10 })
|
|
14
|
+
@Type(() => Number)
|
|
15
|
+
@IsInt()
|
|
16
|
+
@Min(1)
|
|
17
|
+
@Max(100)
|
|
18
|
+
@IsOptional()
|
|
19
|
+
limit?: number = 10;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class PaginatedResponseDto<T> {
|
|
23
|
+
@ApiPropertyOptional()
|
|
24
|
+
data: T[];
|
|
25
|
+
|
|
26
|
+
@ApiPropertyOptional()
|
|
27
|
+
meta: {
|
|
28
|
+
total: number;
|
|
29
|
+
page: number;
|
|
30
|
+
limit: number;
|
|
31
|
+
totalPages: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExceptionFilter,
|
|
3
|
+
Catch,
|
|
4
|
+
ArgumentsHost,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
Logger,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
import { Response } from 'express';
|
|
10
|
+
import { ApiResponseDto } from '@common/dto/api-response.dto';
|
|
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
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
path: request.url,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
response.status(status).json(errorResponse);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { AuthGuard } from '@nestjs/passport';
|
|
4
|
+
import { IS_PUBLIC_KEY } from '@common/decorators/public.decorator';
|
|
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 { Reflector } from '@nestjs/core';
|
|
3
|
+
import { PERMISSIONS_KEY } from '@common/decorators/permissions.decorator';
|
|
4
|
+
import { IS_PUBLIC_KEY } from '@common/decorators/public.decorator';
|
|
5
|
+
import { JwtPayload } from '@modules/auth/core/interfaces/jwt-payload.interface';
|
|
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,37 @@
|
|
|
1
|
+
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { tap } from 'rxjs/operators';
|
|
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,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
NestInterceptor,
|
|
4
|
+
ExecutionContext,
|
|
5
|
+
CallHandler,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Observable } from 'rxjs';
|
|
9
|
+
import { map } from 'rxjs/operators';
|
|
10
|
+
import { ApiResponseDto } from '@common/dto/api-response.dto';
|
|
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
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
path: request.url,
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
|
|
2
|
+
import { PrismaClient } from '@prisma/client';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
6
|
+
private readonly logger = new Logger(PrismaService.name);
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
super({
|
|
10
|
+
log: [
|
|
11
|
+
{ level: 'error', emit: 'stdout' },
|
|
12
|
+
{ level: 'warn', emit: 'stdout' },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async onModuleInit() {
|
|
18
|
+
await this.$connect();
|
|
19
|
+
this.logger.log('✅ Database connected successfully');
|
|
20
|
+
|
|
21
|
+
// Log queries in development
|
|
22
|
+
if (process.env.NODE_ENV === 'development') {
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
this.$on('query', (e) => {
|
|
25
|
+
this.logger.debug(`Query: ${e.query}`);
|
|
26
|
+
this.logger.debug(`Duration: ${e.duration}ms`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async onModuleDestroy() {
|
|
32
|
+
await this.$disconnect();
|
|
33
|
+
this.logger.log('Database disconnected');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async cleanDatabase() {
|
|
37
|
+
if (process.env.NODE_ENV === 'production') {
|
|
38
|
+
throw new Error('Cannot clean database in production');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const models = Reflect.ownKeys(this).filter((key) => key[0] !== '_');
|
|
42
|
+
|
|
43
|
+
return Promise.all(models.map((modelKey) => (this as any)[modelKey].deleteMany()));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as bcrypt from 'bcryptjs';
|
|
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
|
+
apiVersion: process.env.API_VERSION || 'v1',
|
|
9
|
+
corsOrigin: process.env.CORS_ORIGIN || '*',
|
|
10
|
+
}));
|
|
@@ -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
|
+
API_VERSION: Joi.string().default('v1'),
|
|
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
|
+
}));
|