@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,93 @@
|
|
|
1
|
+
import { NestFactory, Reflector } from '@nestjs/core';
|
|
2
|
+
import { ValidationPipe, VersioningType, ClassSerializerInterceptor } from '@nestjs/common';
|
|
3
|
+
import { ConfigService } from '@nestjs/config';
|
|
4
|
+
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|
5
|
+
import { AppModule } from './app.module';
|
|
6
|
+
|
|
7
|
+
async function bootstrap() {
|
|
8
|
+
const app = await NestFactory.create(AppModule);
|
|
9
|
+
|
|
10
|
+
const configService = app.get(ConfigService);
|
|
11
|
+
|
|
12
|
+
// Get configurations
|
|
13
|
+
const port = configService.get<number>('app.port');
|
|
14
|
+
const apiPrefix = configService.get<string>('app.apiPrefix');
|
|
15
|
+
const corsOrigin = configService.get<string>('app.corsOrigin');
|
|
16
|
+
const swaggerEnabled = configService.get<boolean>('swagger.enabled');
|
|
17
|
+
const swaggerPath = configService.get<string>('swagger.path');
|
|
18
|
+
|
|
19
|
+
// Enable CORS
|
|
20
|
+
app.enableCors({
|
|
21
|
+
origin: corsOrigin,
|
|
22
|
+
credentials: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Global prefix
|
|
26
|
+
app.setGlobalPrefix(apiPrefix);
|
|
27
|
+
|
|
28
|
+
// API Versioning
|
|
29
|
+
app.enableVersioning({
|
|
30
|
+
type: VersioningType.URI,
|
|
31
|
+
defaultVersion: '1',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Global validation pipe
|
|
35
|
+
app.useGlobalPipes(
|
|
36
|
+
new ValidationPipe({
|
|
37
|
+
whitelist: true,
|
|
38
|
+
forbidNonWhitelisted: true,
|
|
39
|
+
transform: true,
|
|
40
|
+
transformOptions: {
|
|
41
|
+
enableImplicitConversion: true,
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Class serializer for excluding fields (like password)
|
|
47
|
+
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
|
48
|
+
|
|
49
|
+
// Swagger Documentation
|
|
50
|
+
if (swaggerEnabled) {
|
|
51
|
+
const config = new DocumentBuilder()
|
|
52
|
+
.setTitle('Urbansolv API')
|
|
53
|
+
.setDescription('Urbansolv NestJS Backend API Documentation')
|
|
54
|
+
.setVersion('1.0')
|
|
55
|
+
.addBearerAuth(
|
|
56
|
+
{
|
|
57
|
+
type: 'http',
|
|
58
|
+
scheme: 'bearer',
|
|
59
|
+
bearerFormat: 'JWT',
|
|
60
|
+
name: 'JWT',
|
|
61
|
+
description: 'Enter JWT token',
|
|
62
|
+
in: 'header',
|
|
63
|
+
},
|
|
64
|
+
'JWT-auth',
|
|
65
|
+
)
|
|
66
|
+
.addTag('Authentication', 'Authentication endpoints')
|
|
67
|
+
.addTag('Users', 'User management endpoints')
|
|
68
|
+
.addTag('Health', 'Health check endpoints')
|
|
69
|
+
.build();
|
|
70
|
+
|
|
71
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
72
|
+
SwaggerModule.setup(swaggerPath, app, document, {
|
|
73
|
+
useGlobalPrefix: true,
|
|
74
|
+
swaggerOptions: {
|
|
75
|
+
persistAuthorization: true,
|
|
76
|
+
tagsSorter: 'alpha',
|
|
77
|
+
operationsSorter: 'alpha',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log(`\n📚 Swagger documentation available at: http://localhost:${port}/api/${swaggerPath}\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await app.listen(port);
|
|
85
|
+
|
|
86
|
+
console.log(`\n🚀 Application is running on: http://localhost:${port}/${apiPrefix}`);
|
|
87
|
+
console.log(`🌍 Environment: ${configService.get<string>('app.env')}`);
|
|
88
|
+
console.log(`📊 Health check: http://localhost:${port}/health`);
|
|
89
|
+
console.log(`🏓 Ping endpoint: http://localhost:${port}/ping\n`);
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
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 {}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
UnauthorizedException,
|
|
4
|
+
ConflictException,
|
|
5
|
+
BadRequestException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { JwtService } from '@nestjs/jwt';
|
|
8
|
+
import { ConfigService } from '@nestjs/config';
|
|
9
|
+
import { PrismaService } from '@common/prisma/prisma.service';
|
|
10
|
+
import { PasswordUtil } from '@common/utils/password.util';
|
|
11
|
+
import { LoginDto } from './core/dto/login.dto';
|
|
12
|
+
import { RegisterDto } from './core/dto/register.dto';
|
|
13
|
+
import { AuthResponseDto } from './core/dto/auth-response.dto';
|
|
14
|
+
import { JwtPayload } from './core/interfaces/jwt-payload.interface';
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class AuthService {
|
|
18
|
+
constructor(
|
|
19
|
+
private prisma: PrismaService,
|
|
20
|
+
private jwtService: JwtService,
|
|
21
|
+
private configService: ConfigService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async login(loginDto: LoginDto): Promise<AuthResponseDto> {
|
|
25
|
+
const { email, password } = loginDto;
|
|
26
|
+
|
|
27
|
+
// Find user with position and permissions
|
|
28
|
+
const user = await this.prisma.user.findUnique({
|
|
29
|
+
where: { email },
|
|
30
|
+
include: {
|
|
31
|
+
position: {
|
|
32
|
+
include: {
|
|
33
|
+
position_permissions: {
|
|
34
|
+
include: {
|
|
35
|
+
permission: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!user) {
|
|
44
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!user.is_active) {
|
|
48
|
+
throw new UnauthorizedException('Account is inactive');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Verify password
|
|
52
|
+
const isPasswordValid = await PasswordUtil.compare(password, user.password);
|
|
53
|
+
if (!isPasswordValid) {
|
|
54
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build permissions array
|
|
58
|
+
const permissions = user.position.position_permissions.map(
|
|
59
|
+
(pp) => pp.permission.name,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Generate JWT token
|
|
63
|
+
const payload: JwtPayload = {
|
|
64
|
+
userId: user.id,
|
|
65
|
+
email: user.email,
|
|
66
|
+
positionId: user.position.id,
|
|
67
|
+
positionName: user.position.name,
|
|
68
|
+
permissions,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const access_token = this.jwtService.sign(payload);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
access_token,
|
|
75
|
+
user: {
|
|
76
|
+
id: user.id,
|
|
77
|
+
email: user.email,
|
|
78
|
+
first_name: user.first_name,
|
|
79
|
+
last_name: user.last_name,
|
|
80
|
+
position: {
|
|
81
|
+
id: user.position.id,
|
|
82
|
+
name: user.position.name,
|
|
83
|
+
},
|
|
84
|
+
permissions,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async register(registerDto: RegisterDto): Promise<AuthResponseDto> {
|
|
90
|
+
const { email, password, first_name, last_name, position_id } = registerDto;
|
|
91
|
+
|
|
92
|
+
// Check if user already exists
|
|
93
|
+
const existingUser = await this.prisma.user.findUnique({
|
|
94
|
+
where: { email },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (existingUser) {
|
|
98
|
+
throw new ConflictException('Email already exists');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate position exists
|
|
102
|
+
const position = await this.prisma.position.findUnique({
|
|
103
|
+
where: { id: position_id },
|
|
104
|
+
include: {
|
|
105
|
+
position_permissions: {
|
|
106
|
+
include: {
|
|
107
|
+
permission: true,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!position) {
|
|
114
|
+
throw new BadRequestException('Invalid position ID');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Hash password
|
|
118
|
+
const hashedPassword = await PasswordUtil.hash(password);
|
|
119
|
+
|
|
120
|
+
// Create user
|
|
121
|
+
const user = await this.prisma.user.create({
|
|
122
|
+
data: {
|
|
123
|
+
email,
|
|
124
|
+
password: hashedPassword,
|
|
125
|
+
first_name,
|
|
126
|
+
last_name,
|
|
127
|
+
position_id,
|
|
128
|
+
},
|
|
129
|
+
include: {
|
|
130
|
+
position: {
|
|
131
|
+
include: {
|
|
132
|
+
position_permissions: {
|
|
133
|
+
include: {
|
|
134
|
+
permission: true,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Build permissions array
|
|
143
|
+
const permissions = user.position.position_permissions.map(
|
|
144
|
+
(pp) => pp.permission.name,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Generate JWT token
|
|
148
|
+
const payload: JwtPayload = {
|
|
149
|
+
userId: user.id,
|
|
150
|
+
email: user.email,
|
|
151
|
+
positionId: user.position.id,
|
|
152
|
+
positionName: user.position.name,
|
|
153
|
+
permissions,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const access_token = this.jwtService.sign(payload);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
access_token,
|
|
160
|
+
user: {
|
|
161
|
+
id: user.id,
|
|
162
|
+
email: user.email,
|
|
163
|
+
first_name: user.first_name,
|
|
164
|
+
last_name: user.last_name,
|
|
165
|
+
position: {
|
|
166
|
+
id: user.position.id,
|
|
167
|
+
name: user.position.name,
|
|
168
|
+
},
|
|
169
|
+
permissions,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
|
2
|
+
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
|
3
|
+
import { AuthService } from '../../auth.service';
|
|
4
|
+
import { LoginDto } from '../../core/dto/login.dto';
|
|
5
|
+
import { RegisterDto } from '../../core/dto/register.dto';
|
|
6
|
+
import { AuthResponseDto } from '../../core/dto/auth-response.dto';
|
|
7
|
+
import { Public } from '@common/decorators/public.decorator';
|
|
8
|
+
import { ApiSuccessResponse } from '@common/decorators/api-response.decorator';
|
|
9
|
+
import { JwtAuthGuard } from '@common/guards/jwt-auth.guard';
|
|
10
|
+
import { PermissionsGuard } from '@common/guards/permissions.guard';
|
|
11
|
+
|
|
12
|
+
@ApiTags('Authentication')
|
|
13
|
+
@ApiBearerAuth('JWT-auth')
|
|
14
|
+
@Controller({ path: 'auth', version: '1' })
|
|
15
|
+
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
|
16
|
+
export class AuthController {
|
|
17
|
+
constructor(private readonly authService: AuthService) {}
|
|
18
|
+
|
|
19
|
+
@Public()
|
|
20
|
+
@Post('login')
|
|
21
|
+
@HttpCode(HttpStatus.OK)
|
|
22
|
+
@ApiOperation({ summary: 'User login' })
|
|
23
|
+
@ApiSuccessResponse(AuthResponseDto)
|
|
24
|
+
@ApiResponse({ status: 401, description: 'Invalid credentials' })
|
|
25
|
+
async login(@Body() loginDto: LoginDto): Promise<AuthResponseDto> {
|
|
26
|
+
return this.authService.login(loginDto);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Public()
|
|
30
|
+
@Post('register')
|
|
31
|
+
@ApiOperation({ summary: 'User registration' })
|
|
32
|
+
@ApiSuccessResponse(AuthResponseDto)
|
|
33
|
+
@ApiResponse({ status: 409, description: 'Email already exists' })
|
|
34
|
+
async register(@Body() registerDto: RegisterDto): Promise<AuthResponseDto> {
|
|
35
|
+
return this.authService.register(registerDto);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
export class AuthResponseDto {
|
|
4
|
+
@ApiProperty()
|
|
5
|
+
access_token: string;
|
|
6
|
+
|
|
7
|
+
@ApiProperty()
|
|
8
|
+
user: {
|
|
9
|
+
id: number;
|
|
10
|
+
email: string;
|
|
11
|
+
first_name: string;
|
|
12
|
+
last_name: string;
|
|
13
|
+
position: {
|
|
14
|
+
id: number;
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
permissions: string[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
export class LoginDto {
|
|
5
|
+
@ApiProperty({ example: 'admin@urbansolv.co.id' })
|
|
6
|
+
@IsEmail({}, { message: 'Please provide a valid email address' })
|
|
7
|
+
@IsNotEmpty({ message: 'Email is required' })
|
|
8
|
+
email: string;
|
|
9
|
+
|
|
10
|
+
@ApiProperty({ example: 'password123' })
|
|
11
|
+
@IsString()
|
|
12
|
+
@IsNotEmpty({ message: 'Password is required' })
|
|
13
|
+
@MinLength(6, { message: 'Password must be at least 6 characters long' })
|
|
14
|
+
password: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsEmail, IsNotEmpty, IsString, MinLength, IsInt } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
export class RegisterDto {
|
|
5
|
+
@ApiProperty({ example: 'bhagaskoro@urbansolv.co.id' })
|
|
6
|
+
@IsEmail({}, { message: 'Please provide a valid email address' })
|
|
7
|
+
@IsNotEmpty({ message: 'Email is required' })
|
|
8
|
+
email: string;
|
|
9
|
+
|
|
10
|
+
@ApiProperty({ example: 'password123' })
|
|
11
|
+
@IsString()
|
|
12
|
+
@IsNotEmpty({ message: 'Password is required' })
|
|
13
|
+
@MinLength(6, { message: 'Password must be at least 6 characters long' })
|
|
14
|
+
password: string;
|
|
15
|
+
|
|
16
|
+
@ApiProperty({ example: 'Bhagas' })
|
|
17
|
+
@IsString()
|
|
18
|
+
@IsNotEmpty({ message: 'First name is required' })
|
|
19
|
+
first_name: string;
|
|
20
|
+
|
|
21
|
+
@ApiProperty({ example: 'Koro' })
|
|
22
|
+
@IsString()
|
|
23
|
+
@IsNotEmpty({ message: 'Last name is required' })
|
|
24
|
+
last_name: string;
|
|
25
|
+
|
|
26
|
+
@ApiProperty({ example: 2, description: 'Position ID' })
|
|
27
|
+
@IsInt({ message: 'Position ID must be an integer' })
|
|
28
|
+
@IsNotEmpty({ message: 'Position ID is required' })
|
|
29
|
+
position_id: number;
|
|
30
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
4
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
5
|
+
import { PrismaService } from '@common/prisma/prisma.service';
|
|
6
|
+
import { JwtPayload } from '../interfaces/jwt-payload.interface';
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
10
|
+
constructor(
|
|
11
|
+
private configService: ConfigService,
|
|
12
|
+
private prisma: PrismaService,
|
|
13
|
+
) {
|
|
14
|
+
super({
|
|
15
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
16
|
+
ignoreExpiration: false,
|
|
17
|
+
secretOrKey: configService.get<string>('jwt.secret'),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async validate(payload: JwtPayload): Promise<JwtPayload> {
|
|
22
|
+
const user = await this.prisma.user.findUnique({
|
|
23
|
+
where: { id: payload.userId },
|
|
24
|
+
include: {
|
|
25
|
+
position: {
|
|
26
|
+
include: {
|
|
27
|
+
position_permissions: {
|
|
28
|
+
include: {
|
|
29
|
+
permission: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!user || !user.is_active) {
|
|
38
|
+
throw new UnauthorizedException('User not found or inactive');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build permissions array from position
|
|
42
|
+
const permissions = user.position.position_permissions.map(
|
|
43
|
+
(pp) => pp.permission.name,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
userId: user.id,
|
|
48
|
+
email: user.email,
|
|
49
|
+
positionId: user.position.id,
|
|
50
|
+
positionName: user.position.name,
|
|
51
|
+
permissions,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common';
|
|
2
|
+
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
|
3
|
+
import { Public } from '@common/decorators/public.decorator';
|
|
4
|
+
|
|
5
|
+
@ApiTags('Health')
|
|
6
|
+
@Controller()
|
|
7
|
+
export class HealthController {
|
|
8
|
+
@Public()
|
|
9
|
+
@Get('health')
|
|
10
|
+
@ApiOperation({ summary: 'Health check endpoint' })
|
|
11
|
+
health() {
|
|
12
|
+
return {
|
|
13
|
+
status: 'ok',
|
|
14
|
+
timestamp: new Date().toISOString(),
|
|
15
|
+
uptime: process.uptime(),
|
|
16
|
+
environment: process.env.NODE_ENV,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Public()
|
|
21
|
+
@Get('ping')
|
|
22
|
+
@ApiOperation({ summary: 'Ping endpoint' })
|
|
23
|
+
ping() {
|
|
24
|
+
return {
|
|
25
|
+
message: 'pong',
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Controller,
|
|
3
|
+
Get,
|
|
4
|
+
Post,
|
|
5
|
+
Body,
|
|
6
|
+
Patch,
|
|
7
|
+
Param,
|
|
8
|
+
Delete,
|
|
9
|
+
Query,
|
|
10
|
+
ParseIntPipe,
|
|
11
|
+
HttpCode,
|
|
12
|
+
HttpStatus,
|
|
13
|
+
UseGuards,
|
|
14
|
+
} from '@nestjs/common';
|
|
15
|
+
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
|
16
|
+
import { UsersService } from '../../users.service';
|
|
17
|
+
import { CreateUserDto } from '@modules/users/core/dto/create-user.dto';
|
|
18
|
+
import { UpdateUserDto } from '@modules/users/core/dto/update-user.dto';
|
|
19
|
+
import { ChangePositionDto } from '@modules/users/core/dto/change-position.dto';
|
|
20
|
+
import { ManagePermissionsDto } from '@modules/users/core/dto/manage-permissions.dto';
|
|
21
|
+
import { UserQueryDto } from '@modules/users/core/dto/user-query.dto';
|
|
22
|
+
import { UserEntity } from '../../core//entities/user.entity';
|
|
23
|
+
import { Permissions } from '@common/decorators/permissions.decorator';
|
|
24
|
+
import { PERMISSIONS } from '@common/constants/permissions.constant';
|
|
25
|
+
import { ApiSuccessResponse, ApiSuccessArrayResponse } from '@common/decorators/api-response.decorator';
|
|
26
|
+
import { PaginatedResponseDto } from '@common/dto/pagination.dto';
|
|
27
|
+
import { PermissionsGuard } from '@common/guards/permissions.guard';
|
|
28
|
+
import { JwtAuthGuard } from '@common/guards/jwt-auth.guard';
|
|
29
|
+
|
|
30
|
+
@ApiTags('Users')
|
|
31
|
+
@ApiBearerAuth('JWT-auth')
|
|
32
|
+
@Controller({ path: 'users', version: '1' })
|
|
33
|
+
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
|
34
|
+
export class UsersController {
|
|
35
|
+
constructor(private readonly usersService: UsersService) {}
|
|
36
|
+
|
|
37
|
+
@Post()
|
|
38
|
+
@Permissions(PERMISSIONS.USER.ADD)
|
|
39
|
+
@ApiOperation({ summary: 'Create a new user' })
|
|
40
|
+
@ApiSuccessResponse(UserEntity)
|
|
41
|
+
async create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> {
|
|
42
|
+
return this.usersService.create(createUserDto);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Get()
|
|
46
|
+
@Permissions(PERMISSIONS.USER.VIEW)
|
|
47
|
+
@ApiOperation({ summary: 'Get all users with pagination and filters' })
|
|
48
|
+
@ApiSuccessResponse(PaginatedResponseDto<UserEntity>)
|
|
49
|
+
async findAll(@Query() query: UserQueryDto): Promise<PaginatedResponseDto<UserEntity>> {
|
|
50
|
+
return this.usersService.findAll(query);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Get(':id')
|
|
54
|
+
@Permissions(PERMISSIONS.USER.VIEW)
|
|
55
|
+
@ApiOperation({ summary: 'Get user by ID' })
|
|
56
|
+
@ApiParam({ name: 'id', type: Number })
|
|
57
|
+
@ApiSuccessResponse(UserEntity)
|
|
58
|
+
async findOne(@Param('id', ParseIntPipe) id: number): Promise<UserEntity> {
|
|
59
|
+
return this.usersService.findOne(id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Patch(':id')
|
|
63
|
+
@Permissions(PERMISSIONS.USER.UPDATE)
|
|
64
|
+
@ApiOperation({ summary: 'Update user' })
|
|
65
|
+
@ApiParam({ name: 'id', type: Number })
|
|
66
|
+
@ApiSuccessResponse(UserEntity)
|
|
67
|
+
async update(
|
|
68
|
+
@Param('id', ParseIntPipe) id: number,
|
|
69
|
+
@Body() updateUserDto: UpdateUserDto,
|
|
70
|
+
): Promise<UserEntity> {
|
|
71
|
+
return this.usersService.update(id, updateUserDto);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Delete(':id')
|
|
75
|
+
@Permissions(PERMISSIONS.USER.DELETE)
|
|
76
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
77
|
+
@ApiOperation({ summary: 'Delete user (soft delete)' })
|
|
78
|
+
@ApiParam({ name: 'id', type: Number })
|
|
79
|
+
async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
|
|
80
|
+
return this.usersService.remove(id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Patch(':id/position')
|
|
84
|
+
@Permissions(PERMISSIONS.USER.CHANGE_POSITION)
|
|
85
|
+
@ApiOperation({ summary: 'Change user position/role' })
|
|
86
|
+
@ApiParam({ name: 'id', type: Number })
|
|
87
|
+
@ApiSuccessResponse(UserEntity)
|
|
88
|
+
async changePosition(
|
|
89
|
+
@Param('id', ParseIntPipe) id: number,
|
|
90
|
+
@Body() changePositionDto: ChangePositionDto,
|
|
91
|
+
): Promise<UserEntity> {
|
|
92
|
+
return this.usersService.changePosition(id, changePositionDto);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Post(':id/permissions/assign')
|
|
96
|
+
@Permissions(PERMISSIONS.USER.MANAGE_PERMISSION)
|
|
97
|
+
@ApiOperation({ summary: 'Assign permissions to user position' })
|
|
98
|
+
@ApiParam({ name: 'id', type: Number })
|
|
99
|
+
@ApiSuccessResponse(UserEntity)
|
|
100
|
+
async assignPermissions(
|
|
101
|
+
@Param('id', ParseIntPipe) id: number,
|
|
102
|
+
@Body() managePermissionsDto: ManagePermissionsDto,
|
|
103
|
+
): Promise<UserEntity> {
|
|
104
|
+
return this.usersService.assignPermissions(id, managePermissionsDto);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Post(':id/permissions/revoke')
|
|
108
|
+
@Permissions(PERMISSIONS.USER.MANAGE_PERMISSION)
|
|
109
|
+
@ApiOperation({ summary: 'Revoke permissions from user position' })
|
|
110
|
+
@ApiParam({ name: 'id', type: Number })
|
|
111
|
+
@ApiSuccessResponse(UserEntity)
|
|
112
|
+
async revokePermissions(
|
|
113
|
+
@Param('id', ParseIntPipe) id: number,
|
|
114
|
+
@Body() managePermissionsDto: ManagePermissionsDto,
|
|
115
|
+
): Promise<UserEntity> {
|
|
116
|
+
return this.usersService.revokePermissions(id, managePermissionsDto);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsInt, IsNotEmpty } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
export class ChangePositionDto {
|
|
5
|
+
@ApiProperty({ example: 2, description: 'New position ID' })
|
|
6
|
+
@IsInt({ message: 'Position ID must be an integer' })
|
|
7
|
+
@IsNotEmpty({ message: 'Position ID is required' })
|
|
8
|
+
position_id: number;
|
|
9
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsEmail, IsNotEmpty, IsString, MinLength, IsInt, IsBoolean, IsOptional } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
export class CreateUserDto {
|
|
5
|
+
@ApiProperty({ example: 'bhagaskoro@urbansolv.co.id' })
|
|
6
|
+
@IsEmail({}, { message: 'Please provide a valid email address' })
|
|
7
|
+
@IsNotEmpty({ message: 'Email is required' })
|
|
8
|
+
email: string;
|
|
9
|
+
|
|
10
|
+
@ApiProperty({ example: 'password123' })
|
|
11
|
+
@IsString()
|
|
12
|
+
@IsNotEmpty({ message: 'Password is required' })
|
|
13
|
+
@MinLength(6, { message: 'Password must be at least 6 characters long' })
|
|
14
|
+
password: string;
|
|
15
|
+
|
|
16
|
+
@ApiProperty({ example: 'Bhagas' })
|
|
17
|
+
@IsString()
|
|
18
|
+
@IsNotEmpty({ message: 'First name is required' })
|
|
19
|
+
first_name: string;
|
|
20
|
+
|
|
21
|
+
@ApiProperty({ example: 'Koro' })
|
|
22
|
+
@IsString()
|
|
23
|
+
@IsNotEmpty({ message: 'Last name is required' })
|
|
24
|
+
last_name: string;
|
|
25
|
+
|
|
26
|
+
@ApiProperty({ example: 2, description: 'Position ID' })
|
|
27
|
+
@IsInt({ message: 'Position ID must be an integer' })
|
|
28
|
+
@IsNotEmpty({ message: 'Position ID is required' })
|
|
29
|
+
position_id: number;
|
|
30
|
+
|
|
31
|
+
@ApiProperty({ example: true, default: true })
|
|
32
|
+
@IsBoolean()
|
|
33
|
+
@IsOptional()
|
|
34
|
+
is_active?: boolean = true;
|
|
35
|
+
}
|