speedrun-cli 2.6.1
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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/cli.js +224 -0
- package/index.js +12 -0
- package/package.json +74 -0
- package/src/constants.js +65 -0
- package/src/generator.js +271 -0
- package/src/index.js +13 -0
- package/src/moduleGenerator.js +586 -0
- package/src/postSetup.js +365 -0
- package/src/prompts.js +189 -0
- package/src/utils.js +112 -0
- package/templates/README.md +81 -0
- package/templates/base/eslint.config.mjs +34 -0
- package/templates/base/gitignore +78 -0
- package/templates/base/nest-cli.json +8 -0
- package/templates/base/src/common/constants/cookie.config.ts +20 -0
- package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
- package/templates/base/src/common/decorators/public.decorator.ts +4 -0
- package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
- package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
- package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
- package/templates/base/src/common/guards/auth.guard.ts +51 -0
- package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
- package/templates/base/src/common/guards/roles.guard.ts +45 -0
- package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
- package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
- package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
- package/templates/base/src/common/validators/password.validator.ts +37 -0
- package/templates/base/src/config/config.module.ts +14 -0
- package/templates/base/src/config/logger.config.ts +109 -0
- package/templates/base/src/main.ts +84 -0
- package/templates/base/src/modules/auth/auth.controller.ts +133 -0
- package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
- package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/base/src/modules/health/health.controller.ts +20 -0
- package/templates/base/src/modules/health/health.module.ts +7 -0
- package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
- package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/base/src/modules/users/users.controller.ts +65 -0
- package/templates/base/test/app.e2e-spec.ts +24 -0
- package/templates/base/test/jest-e2e.json +9 -0
- package/templates/base/tsconfig.build.json +4 -0
- package/templates/base/tsconfig.json +24 -0
- package/templates/base-crud/CRUD_README.md +385 -0
- package/templates/base-crud/src/common/base/base.controller.ts +321 -0
- package/templates/base-crud/src/common/base/base.service.ts +192 -0
- package/templates/base-crud/src/common/base/index.ts +20 -0
- package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
- package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
- package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
- package/templates/database/mongodb/.env.example +22 -0
- package/templates/database/mysql/.env.example +24 -0
- package/templates/database/mysql/drizzle.config.ts +13 -0
- package/templates/database/mysql/package.json +5 -0
- package/templates/database/mysql/prisma/schema.prisma +55 -0
- package/templates/database/mysql/src/database/drizzle.ts +13 -0
- package/templates/database/mysql/src/database/schema.ts +58 -0
- package/templates/database/postgres/.env.example +24 -0
- package/templates/database/postgres/drizzle.config.ts +13 -0
- package/templates/database/postgres/package.json +8 -0
- package/templates/database/postgres/prisma/schema.prisma +55 -0
- package/templates/database/sqlite/.env.example +24 -0
- package/templates/database/sqlite/drizzle.config.ts +13 -0
- package/templates/database/sqlite/package.json +8 -0
- package/templates/database/sqlite/prisma/schema.prisma +48 -0
- package/templates/database/sqlite/src/database/drizzle.ts +11 -0
- package/templates/database/sqlite/src/database/schema.ts +52 -0
- package/templates/orm/drizzle/drizzle.config.ts +13 -0
- package/templates/orm/drizzle/package.json +90 -0
- package/templates/orm/drizzle/src/app.module.ts +73 -0
- package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
- package/templates/orm/drizzle/src/database/database.module.ts +25 -0
- package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
- package/templates/orm/drizzle/src/database/schema.ts +60 -0
- package/templates/orm/drizzle/src/database/seed.ts +87 -0
- package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
- package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
- package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
- package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
- package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
- package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
- package/templates/orm/mongoose/.env.example +22 -0
- package/templates/orm/mongoose/package.json +86 -0
- package/templates/orm/mongoose/src/app.module.ts +73 -0
- package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
- package/templates/orm/mongoose/src/database/database.module.ts +19 -0
- package/templates/orm/mongoose/src/database/seed.ts +107 -0
- package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
- package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
- package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
- package/templates/orm/mongoose/src/schemas/index.ts +2 -0
- package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
- package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
- package/templates/orm/prisma/package.json +102 -0
- package/templates/orm/prisma/prisma/schema.prisma +60 -0
- package/templates/orm/prisma/prisma/seed.ts +69 -0
- package/templates/orm/prisma/src/app.module.ts +55 -0
- package/templates/orm/prisma/src/config/env.validation.ts +56 -0
- package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
- package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
- package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
- package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
- package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
- package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
- package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
- package/templates/orm/typeorm/package.json +100 -0
- package/templates/orm/typeorm/src/app.module.ts +55 -0
- package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
- package/templates/orm/typeorm/src/database/data-source.ts +19 -0
- package/templates/orm/typeorm/src/database/database.module.ts +27 -0
- package/templates/orm/typeorm/src/database/seed.ts +76 -0
- package/templates/orm/typeorm/src/entities/index.ts +2 -0
- package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
- package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
- package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
- package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
- package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
- package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
- package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
- package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
- package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
- package/templates/swagger/src/main.ts +116 -0
- package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
- package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
- package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
- package/templates/swagger/src/modules/users/users.controller.ts +214 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-user.dto.ts +23 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
3
|
+
import { Repository } from 'typeorm';
|
|
4
|
+
import { User } from 'src/entities/user.entity';
|
|
5
|
+
import { RefreshToken } from 'src/entities/refresh-token.entity';
|
|
6
|
+
import { UpdateProfileDto } from './dtos/update-profile.dto';
|
|
7
|
+
import { UpdateUserDto } from './dtos/update-user.dto';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class UsersService {
|
|
11
|
+
constructor(
|
|
12
|
+
@InjectRepository(User)
|
|
13
|
+
private userRepository: Repository<User>,
|
|
14
|
+
@InjectRepository(RefreshToken)
|
|
15
|
+
private refreshTokenRepository: Repository<RefreshToken>,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
async getProfile(userId: string) {
|
|
19
|
+
const user = await this.userRepository.findOne({
|
|
20
|
+
where: { id: userId },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return this.sanitizeUser(user);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async updateProfile(userId: string, updateProfileDto: UpdateProfileDto) {
|
|
27
|
+
await this.userRepository.update(userId, updateProfileDto);
|
|
28
|
+
|
|
29
|
+
const updatedUser = await this.userRepository.findOne({
|
|
30
|
+
where: { id: userId },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return this.sanitizeUser(updatedUser);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getAllUsers(page: number = 1, limit: number = 10) {
|
|
37
|
+
const skip = (page - 1) * limit;
|
|
38
|
+
|
|
39
|
+
const [users, total] = await this.userRepository.findAndCount({
|
|
40
|
+
where: { isActive: true },
|
|
41
|
+
skip,
|
|
42
|
+
take: limit,
|
|
43
|
+
order: { createdAt: 'DESC' },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const sanitizedUsers = users.map((user) => this.sanitizeUser(user));
|
|
47
|
+
const totalPages = Math.ceil(total / limit);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
data: sanitizedUsers,
|
|
51
|
+
meta: {
|
|
52
|
+
total,
|
|
53
|
+
page,
|
|
54
|
+
limit,
|
|
55
|
+
totalPages,
|
|
56
|
+
hasNext: page < totalPages,
|
|
57
|
+
hasPrevious: page > 1,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getUserById(userId: string) {
|
|
63
|
+
if (!userId) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const user = await this.userRepository.findOne({
|
|
68
|
+
where: { id: userId },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!user) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return this.sanitizeUser(user);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async updateUserById(userId: string, updateUserDto: UpdateUserDto) {
|
|
79
|
+
await this.userRepository.update(userId, updateUserDto);
|
|
80
|
+
|
|
81
|
+
const updatedUser = await this.userRepository.findOne({
|
|
82
|
+
where: { id: userId },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return this.sanitizeUser(updatedUser);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async deleteUserById(userId: string) {
|
|
89
|
+
// Soft delete: set isActive to false
|
|
90
|
+
await this.userRepository.update(userId, { isActive: false });
|
|
91
|
+
|
|
92
|
+
// Invalidate all refresh tokens for this user
|
|
93
|
+
await this.refreshTokenRepository.delete({ userId });
|
|
94
|
+
|
|
95
|
+
return { message: 'User deleted successfully' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Utils
|
|
99
|
+
|
|
100
|
+
sanitizeUser(user: User | null) {
|
|
101
|
+
if (!user) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { refreshToken, passwordHash, ...safeUser } = user;
|
|
106
|
+
return safeUser;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { IsOptional, IsInt, Min, Max } from 'class-validator';
|
|
3
|
+
import { Type } from 'class-transformer';
|
|
4
|
+
|
|
5
|
+
export class PaginationDto {
|
|
6
|
+
@ApiPropertyOptional({
|
|
7
|
+
description: 'Page number',
|
|
8
|
+
example: 1,
|
|
9
|
+
minimum: 1,
|
|
10
|
+
default: 1,
|
|
11
|
+
})
|
|
12
|
+
@IsOptional()
|
|
13
|
+
@Type(() => Number)
|
|
14
|
+
@IsInt()
|
|
15
|
+
@Min(1)
|
|
16
|
+
page?: number = 1;
|
|
17
|
+
|
|
18
|
+
@ApiPropertyOptional({
|
|
19
|
+
description: 'Number of items per page',
|
|
20
|
+
example: 10,
|
|
21
|
+
minimum: 1,
|
|
22
|
+
maximum: 100,
|
|
23
|
+
default: 10,
|
|
24
|
+
})
|
|
25
|
+
@IsOptional()
|
|
26
|
+
@Type(() => Number)
|
|
27
|
+
@IsInt()
|
|
28
|
+
@Min(1)
|
|
29
|
+
@Max(100)
|
|
30
|
+
limit?: number = 10;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PaginatedResponse<T> {
|
|
34
|
+
data: T[];
|
|
35
|
+
meta: {
|
|
36
|
+
total: number;
|
|
37
|
+
page: number;
|
|
38
|
+
limit: number;
|
|
39
|
+
totalPages: number;
|
|
40
|
+
hasNext: boolean;
|
|
41
|
+
hasPrevious: boolean;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { NestFactory } from '@nestjs/core';
|
|
2
|
+
import { AppModule } from './app.module';
|
|
3
|
+
import cookieParser from 'cookie-parser';
|
|
4
|
+
import helmet from 'helmet';
|
|
5
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
6
|
+
import { ConfigService } from '@nestjs/config';
|
|
7
|
+
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
|
8
|
+
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
|
9
|
+
import { Logger } from 'nestjs-pino';
|
|
10
|
+
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
11
|
+
|
|
12
|
+
async function bootstrap() {
|
|
13
|
+
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
14
|
+
|
|
15
|
+
// Use Pino logger
|
|
16
|
+
app.useLogger(app.get(Logger));
|
|
17
|
+
|
|
18
|
+
app.setGlobalPrefix('api/v1');
|
|
19
|
+
|
|
20
|
+
const configService = app.get(ConfigService);
|
|
21
|
+
|
|
22
|
+
const port = configService.get<number>('PORT');
|
|
23
|
+
const corsOrigins = configService.get<string>('CORS_ORIGIN')?.split(',');
|
|
24
|
+
|
|
25
|
+
app.enableCors({
|
|
26
|
+
origins: corsOrigins,
|
|
27
|
+
credentials: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.use(cookieParser());
|
|
31
|
+
|
|
32
|
+
// Configure Helmet with relaxed CSP to allow Swagger UI assets
|
|
33
|
+
app.use(
|
|
34
|
+
helmet({
|
|
35
|
+
contentSecurityPolicy: {
|
|
36
|
+
directives: {
|
|
37
|
+
defaultSrc: ["'self'"],
|
|
38
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
39
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
40
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
crossOriginEmbedderPolicy: false,
|
|
44
|
+
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
45
|
+
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
46
|
+
dnsPrefetchControl: { allow: false },
|
|
47
|
+
frameguard: { action: 'deny' },
|
|
48
|
+
hidePoweredBy: true,
|
|
49
|
+
hsts: {
|
|
50
|
+
maxAge: 31536000,
|
|
51
|
+
includeSubDomains: true,
|
|
52
|
+
preload: true,
|
|
53
|
+
},
|
|
54
|
+
ieNoOpen: true,
|
|
55
|
+
noSniff: true,
|
|
56
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
57
|
+
xssFilter: true,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
app.useGlobalPipes(
|
|
62
|
+
new ValidationPipe({
|
|
63
|
+
whitelist: true,
|
|
64
|
+
forbidNonWhitelisted: true,
|
|
65
|
+
transform: true,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Get logger instance
|
|
70
|
+
const logger = app.get(Logger);
|
|
71
|
+
|
|
72
|
+
// Global response interceptor for standard API responses
|
|
73
|
+
app.useGlobalInterceptors(new ResponseInterceptor());
|
|
74
|
+
|
|
75
|
+
// Global exception filter for standard error responses
|
|
76
|
+
app.useGlobalFilters(new HttpExceptionFilter(logger));
|
|
77
|
+
|
|
78
|
+
// Swagger API documentation
|
|
79
|
+
const swaggerConfig = new DocumentBuilder()
|
|
80
|
+
.setTitle('NestJS Auth API')
|
|
81
|
+
.setDescription(
|
|
82
|
+
'Production-ready NestJS authentication API with JWT, refresh tokens, and Role-Based Access Control (RBAC).',
|
|
83
|
+
)
|
|
84
|
+
.setVersion('1.0')
|
|
85
|
+
.addBearerAuth(
|
|
86
|
+
{
|
|
87
|
+
type: 'http',
|
|
88
|
+
scheme: 'bearer',
|
|
89
|
+
bearerFormat: 'JWT',
|
|
90
|
+
name: 'Authorization',
|
|
91
|
+
description: 'Enter your JWT access token',
|
|
92
|
+
in: 'header',
|
|
93
|
+
},
|
|
94
|
+
'bearer',
|
|
95
|
+
)
|
|
96
|
+
.build();
|
|
97
|
+
|
|
98
|
+
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
|
99
|
+
SwaggerModule.setup('api/docs', app, document, {
|
|
100
|
+
swaggerOptions: {
|
|
101
|
+
persistAuthorization: true,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await app.listen(port || 8080);
|
|
106
|
+
|
|
107
|
+
logger.log(
|
|
108
|
+
`🚀 Application is running on: http://localhost:${port || 8080}/api/v1`,
|
|
109
|
+
'Bootstrap',
|
|
110
|
+
);
|
|
111
|
+
logger.log(
|
|
112
|
+
`📄 Swagger docs available at: http://localhost:${port || 8080}/api/docs`,
|
|
113
|
+
'Bootstrap',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
void bootstrap();
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Get,
|
|
5
|
+
Post,
|
|
6
|
+
Req,
|
|
7
|
+
Res,
|
|
8
|
+
UseGuards,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
import {
|
|
11
|
+
ApiTags,
|
|
12
|
+
ApiOperation,
|
|
13
|
+
ApiResponse,
|
|
14
|
+
ApiBearerAuth,
|
|
15
|
+
} from '@nestjs/swagger';
|
|
16
|
+
import { AuthService } from './auth.service';
|
|
17
|
+
import { Request, Response } from 'express';
|
|
18
|
+
import { RefreshTokenGuard } from 'src/common/guards/refresh-token.guard';
|
|
19
|
+
import { GetUser } from 'src/common/decorators/get-user.decorator';
|
|
20
|
+
import { Public } from 'src/common/decorators/public.decorator';
|
|
21
|
+
import { SignupDto } from './dtos/signup.dto';
|
|
22
|
+
import { LoginDto } from './dtos/login.dto';
|
|
23
|
+
import { Throttle, SkipThrottle } from '@nestjs/throttler';
|
|
24
|
+
import { COOKIE_CONFIG } from 'src/common/constants/cookie.config';
|
|
25
|
+
import { CookieOptions } from 'express';
|
|
26
|
+
|
|
27
|
+
@ApiTags('Auth')
|
|
28
|
+
@Controller('auth')
|
|
29
|
+
export class AuthController {
|
|
30
|
+
constructor(private readonly authService: AuthService) {}
|
|
31
|
+
|
|
32
|
+
@ApiOperation({ summary: 'Register a new user account' })
|
|
33
|
+
@ApiResponse({
|
|
34
|
+
status: 201,
|
|
35
|
+
description: 'User successfully registered.',
|
|
36
|
+
schema: {
|
|
37
|
+
example: {
|
|
38
|
+
statusCode: 201,
|
|
39
|
+
message: 'User registered successfully',
|
|
40
|
+
data: {
|
|
41
|
+
id: 'clxyz...',
|
|
42
|
+
email: 'user@example.com',
|
|
43
|
+
fullName: 'John Doe',
|
|
44
|
+
role: 'USER',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
@ApiResponse({
|
|
50
|
+
status: 400,
|
|
51
|
+
description: 'Validation error - invalid input data.',
|
|
52
|
+
})
|
|
53
|
+
@ApiResponse({ status: 409, description: 'Conflict - email already exists.' })
|
|
54
|
+
@Throttle({ strict: { ttl: 60000, limit: 3 } })
|
|
55
|
+
@Public()
|
|
56
|
+
@Post('/signup')
|
|
57
|
+
async signup(@Body() signupDto: SignupDto) {
|
|
58
|
+
return this.authService.signup(signupDto);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@ApiOperation({ summary: 'Login with email and password' })
|
|
62
|
+
@ApiResponse({
|
|
63
|
+
status: 200,
|
|
64
|
+
description: 'User successfully logged in. Tokens set in cookies.',
|
|
65
|
+
schema: {
|
|
66
|
+
example: {
|
|
67
|
+
statusCode: 200,
|
|
68
|
+
message: 'Login successful',
|
|
69
|
+
data: {
|
|
70
|
+
user: {
|
|
71
|
+
id: 'clxyz...',
|
|
72
|
+
email: 'user@example.com',
|
|
73
|
+
fullName: 'John Doe',
|
|
74
|
+
role: 'USER',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
@ApiResponse({
|
|
81
|
+
status: 401,
|
|
82
|
+
description: 'Unauthorized - invalid credentials.',
|
|
83
|
+
})
|
|
84
|
+
@Throttle({ strict: { ttl: 60000, limit: 5 } })
|
|
85
|
+
@Public()
|
|
86
|
+
@Post('/login')
|
|
87
|
+
async login(
|
|
88
|
+
@Body() loginDto: LoginDto,
|
|
89
|
+
@Req() req: Request,
|
|
90
|
+
@Res({ passthrough: true }) res: Response,
|
|
91
|
+
) {
|
|
92
|
+
const deviceInfo = req.headers['user-agent'] || 'Unknown Device';
|
|
93
|
+
const ipAddress =
|
|
94
|
+
(req.headers['x-forwarded-for'] as string)?.split(',')[0] ||
|
|
95
|
+
req.ip ||
|
|
96
|
+
'Unknown IP';
|
|
97
|
+
|
|
98
|
+
const data = await this.authService.login(loginDto, deviceInfo, ipAddress);
|
|
99
|
+
|
|
100
|
+
res.cookie(
|
|
101
|
+
COOKIE_CONFIG.ACCESS_TOKEN.name,
|
|
102
|
+
data.accessToken,
|
|
103
|
+
COOKIE_CONFIG.ACCESS_TOKEN.options as CookieOptions,
|
|
104
|
+
);
|
|
105
|
+
res.cookie(
|
|
106
|
+
COOKIE_CONFIG.REFRESH_TOKEN.name,
|
|
107
|
+
data.refreshToken,
|
|
108
|
+
COOKIE_CONFIG.REFRESH_TOKEN.options as CookieOptions,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
user: data.user,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@ApiOperation({ summary: 'Refresh access and refresh tokens' })
|
|
117
|
+
@ApiResponse({
|
|
118
|
+
status: 200,
|
|
119
|
+
description: 'Tokens refreshed successfully.',
|
|
120
|
+
schema: {
|
|
121
|
+
example: {
|
|
122
|
+
statusCode: 200,
|
|
123
|
+
message: 'Tokens refreshed successfully',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
@ApiResponse({
|
|
128
|
+
status: 401,
|
|
129
|
+
description: 'Unauthorized - invalid or expired refresh token.',
|
|
130
|
+
})
|
|
131
|
+
@Throttle({ default: { ttl: 60000, limit: 10 } })
|
|
132
|
+
@Public()
|
|
133
|
+
@UseGuards(RefreshTokenGuard)
|
|
134
|
+
@Post('/refresh')
|
|
135
|
+
async refreshToken(
|
|
136
|
+
@GetUser('sub') userId: string,
|
|
137
|
+
@Req() req: Request,
|
|
138
|
+
@Res({ passthrough: true }) res: Response,
|
|
139
|
+
) {
|
|
140
|
+
const rt = req.cookies[COOKIE_CONFIG.REFRESH_TOKEN.name] as string;
|
|
141
|
+
const deviceInfo = req.headers['user-agent'] || 'Unknown Device';
|
|
142
|
+
const ipAddress =
|
|
143
|
+
(req.headers['x-forwarded-for'] as string)?.split(',')[0] ||
|
|
144
|
+
req.ip ||
|
|
145
|
+
'Unknown IP';
|
|
146
|
+
|
|
147
|
+
const { accessToken, refreshToken } = await this.authService.refreshToken(
|
|
148
|
+
userId,
|
|
149
|
+
rt,
|
|
150
|
+
deviceInfo,
|
|
151
|
+
ipAddress,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
res.cookie(
|
|
155
|
+
COOKIE_CONFIG.ACCESS_TOKEN.name,
|
|
156
|
+
accessToken,
|
|
157
|
+
COOKIE_CONFIG.ACCESS_TOKEN.options as CookieOptions,
|
|
158
|
+
);
|
|
159
|
+
res.cookie(
|
|
160
|
+
COOKIE_CONFIG.REFRESH_TOKEN.name,
|
|
161
|
+
refreshToken,
|
|
162
|
+
COOKIE_CONFIG.REFRESH_TOKEN.options as CookieOptions,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
message: 'Tokens refreshed successfully',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@ApiOperation({ summary: 'Logout and invalidate tokens' })
|
|
171
|
+
@ApiResponse({
|
|
172
|
+
status: 200,
|
|
173
|
+
description: 'User logged out successfully.',
|
|
174
|
+
schema: {
|
|
175
|
+
example: {
|
|
176
|
+
statusCode: 200,
|
|
177
|
+
message: 'Logged out successfully',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
@Public()
|
|
182
|
+
@UseGuards(RefreshTokenGuard)
|
|
183
|
+
@Post('/logout')
|
|
184
|
+
async logout(
|
|
185
|
+
@GetUser('sub') userId: string,
|
|
186
|
+
@Req() req: Request,
|
|
187
|
+
@Res({ passthrough: true }) res: Response,
|
|
188
|
+
) {
|
|
189
|
+
const rt = req.cookies[COOKIE_CONFIG.REFRESH_TOKEN.name] as
|
|
190
|
+
| string
|
|
191
|
+
| undefined;
|
|
192
|
+
await this.authService.logout(userId, rt);
|
|
193
|
+
|
|
194
|
+
res.clearCookie(
|
|
195
|
+
COOKIE_CONFIG.ACCESS_TOKEN.name,
|
|
196
|
+
COOKIE_CONFIG.ACCESS_TOKEN.options as CookieOptions | undefined,
|
|
197
|
+
);
|
|
198
|
+
res.clearCookie(
|
|
199
|
+
COOKIE_CONFIG.REFRESH_TOKEN.name,
|
|
200
|
+
COOKIE_CONFIG.REFRESH_TOKEN.options as CookieOptions | undefined,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
message: 'Logged out successfully',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@ApiBearerAuth('bearer')
|
|
209
|
+
@ApiOperation({ summary: 'Get current authenticated user profile' })
|
|
210
|
+
@ApiResponse({
|
|
211
|
+
status: 200,
|
|
212
|
+
description: 'Returns the current user profile.',
|
|
213
|
+
schema: {
|
|
214
|
+
example: {
|
|
215
|
+
statusCode: 200,
|
|
216
|
+
data: {
|
|
217
|
+
id: 'clxyz...',
|
|
218
|
+
email: 'user@example.com',
|
|
219
|
+
fullName: 'John Doe',
|
|
220
|
+
role: 'USER',
|
|
221
|
+
isActive: true,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
@ApiResponse({
|
|
227
|
+
status: 401,
|
|
228
|
+
description: 'Unauthorized - invalid or missing JWT token.',
|
|
229
|
+
})
|
|
230
|
+
@SkipThrottle()
|
|
231
|
+
@Get('/me')
|
|
232
|
+
async getMe(@GetUser('sub') userId: string) {
|
|
233
|
+
return this.authService.getMe(userId);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsEmail, IsString } from 'class-validator';
|
|
3
|
+
import { IsStrongPassword } from 'src/common/validators/password.validator';
|
|
4
|
+
|
|
5
|
+
export class LoginDto {
|
|
6
|
+
@ApiProperty({
|
|
7
|
+
description: 'User email address',
|
|
8
|
+
example: 'user@example.com',
|
|
9
|
+
})
|
|
10
|
+
@IsEmail()
|
|
11
|
+
email: string;
|
|
12
|
+
|
|
13
|
+
@ApiProperty({
|
|
14
|
+
description: 'User password',
|
|
15
|
+
example: 'StrongP@ss1',
|
|
16
|
+
})
|
|
17
|
+
@IsString()
|
|
18
|
+
@IsStrongPassword()
|
|
19
|
+
password: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsString, MinLength } from 'class-validator';
|
|
3
|
+
import { LoginDto } from './login.dto';
|
|
4
|
+
|
|
5
|
+
export class SignupDto extends LoginDto {
|
|
6
|
+
@ApiProperty({
|
|
7
|
+
description: 'User full name',
|
|
8
|
+
example: 'John Doe',
|
|
9
|
+
minLength: 2,
|
|
10
|
+
})
|
|
11
|
+
@IsString()
|
|
12
|
+
@MinLength(2)
|
|
13
|
+
fullName: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
export class UpdateProfileDto {
|
|
5
|
+
@ApiPropertyOptional({
|
|
6
|
+
description: 'User full name',
|
|
7
|
+
example: 'Jane Doe',
|
|
8
|
+
minLength: 2,
|
|
9
|
+
maxLength: 100,
|
|
10
|
+
})
|
|
11
|
+
@IsOptional()
|
|
12
|
+
@IsString()
|
|
13
|
+
@MinLength(2)
|
|
14
|
+
@MaxLength(100)
|
|
15
|
+
fullName?: string;
|
|
16
|
+
|
|
17
|
+
// Email update removed - requires separate verification flow for security
|
|
18
|
+
// Implement email change in a dedicated endpoint with verification
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { IsBoolean, IsEnum, IsOptional } from 'class-validator';
|
|
3
|
+
import { UpdateProfileDto } from './update-profile.dto';
|
|
4
|
+
import { UserRole } from 'src/common/guards/roles.guard';
|
|
5
|
+
|
|
6
|
+
export class UpdateUserDto extends UpdateProfileDto {
|
|
7
|
+
@ApiPropertyOptional({
|
|
8
|
+
description: 'User role',
|
|
9
|
+
enum: UserRole,
|
|
10
|
+
example: 'ADMIN',
|
|
11
|
+
})
|
|
12
|
+
@IsOptional()
|
|
13
|
+
@IsEnum(UserRole, { message: 'Role must be a valid UserRole' })
|
|
14
|
+
role?: UserRole;
|
|
15
|
+
|
|
16
|
+
@ApiPropertyOptional({
|
|
17
|
+
description: 'Whether the user account is active',
|
|
18
|
+
example: true,
|
|
19
|
+
})
|
|
20
|
+
@IsOptional()
|
|
21
|
+
@IsBoolean()
|
|
22
|
+
isActive?: boolean;
|
|
23
|
+
}
|