innovationhub-cli 1.1.0 → 2.0.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -86
  3. package/index.js +228 -29
  4. package/package.json +12 -3
  5. package/templates/nest/.env.example +41 -0
  6. package/templates/nest/.prettierrc +4 -0
  7. package/templates/nest/Dockerfile +17 -0
  8. package/templates/nest/README.md +224 -0
  9. package/templates/nest/addons/.github/dependabot.yml +11 -0
  10. package/templates/nest/addons/.github/labeler.yml +34 -0
  11. package/templates/nest/addons/.github/workflows/check-branch.yml +24 -0
  12. package/templates/nest/addons/.github/workflows/ci.yml +29 -0
  13. package/templates/nest/addons/.github/workflows/draft-release.yml +22 -0
  14. package/templates/nest/addons/.github/workflows/pr-labeler.yml +16 -0
  15. package/templates/nest/addons/.github/workflows/release.yml +33 -0
  16. package/templates/nest/addons/.github/workflows/security-audit.yml +17 -0
  17. package/templates/nest/addons/.github/workflows/semantic-pr.yml +31 -0
  18. package/templates/nest/addons/.github/workflows/stale.yml +24 -0
  19. package/templates/nest/addons/.husky/commit-msg +17 -0
  20. package/templates/nest/addons/.husky/pre-commit +1 -0
  21. package/templates/nest/addons/addons.json +17 -0
  22. package/templates/nest/addons/cloudinary/cloudinary.module.ts +11 -0
  23. package/templates/nest/addons/cloudinary/cloudinary.provider.ts +16 -0
  24. package/templates/nest/addons/cloudinary/cloudinary.service.ts +54 -0
  25. package/templates/nest/docker-compose.yml +58 -0
  26. package/templates/nest/eslint.config.mjs +34 -0
  27. package/templates/nest/jest.config.js +17 -0
  28. package/templates/nest/nest-cli.json +12 -0
  29. package/templates/nest/package.json +99 -0
  30. package/templates/nest/src/app.controller.ts +7 -0
  31. package/templates/nest/src/app.module.ts +69 -0
  32. package/templates/nest/src/app.service.ts +4 -0
  33. package/templates/nest/src/auth/auth.controller.ts +97 -0
  34. package/templates/nest/src/auth/auth.module.ts +46 -0
  35. package/templates/nest/src/auth/auth.service.ts +231 -0
  36. package/templates/nest/src/auth/decorators/roles.decorator.ts +5 -0
  37. package/templates/nest/src/auth/dto/change-password.dto.ts +21 -0
  38. package/templates/nest/src/auth/dto/login-response.dto.ts +25 -0
  39. package/templates/nest/src/auth/dto/login.dto.ts +15 -0
  40. package/templates/nest/src/auth/dto/refresh-token.dto.ts +12 -0
  41. package/templates/nest/src/auth/entities/refresh-token.entity.ts +18 -0
  42. package/templates/nest/src/auth/enums/role.enum.ts +4 -0
  43. package/templates/nest/src/auth/guards/jwt-auth.guard.ts +5 -0
  44. package/templates/nest/src/auth/guards/refresh-token.guard.ts +5 -0
  45. package/templates/nest/src/auth/guards/roles.guard.ts +23 -0
  46. package/templates/nest/src/auth/interfaces/jwt-payload.interface.ts +10 -0
  47. package/templates/nest/src/auth/strategies/jwt.strategy.ts +28 -0
  48. package/templates/nest/src/auth/strategies/local.strategy.ts +23 -0
  49. package/templates/nest/src/auth/strategies/refresh-token.strategy.ts +32 -0
  50. package/templates/nest/src/common/base.entity.ts +19 -0
  51. package/templates/nest/src/common/base.repository.ts +79 -0
  52. package/templates/nest/src/common/base.service.ts +28 -0
  53. package/templates/nest/src/common/constants/errors.constants.ts +33 -0
  54. package/templates/nest/src/common/decorators/user.decorator.ts +9 -0
  55. package/templates/nest/src/common/dto/base-query.dto.ts +56 -0
  56. package/templates/nest/src/common/irepository.ts +18 -0
  57. package/templates/nest/src/common/utils/duration.utils.ts +33 -0
  58. package/templates/nest/src/common/utils/pagination.utils.ts +35 -0
  59. package/templates/nest/src/common/utils/slug.utils.ts +14 -0
  60. package/templates/nest/src/common/utils/transform.utils.ts +62 -0
  61. package/templates/nest/src/common/validators/is-date-after.validator.ts +40 -0
  62. package/templates/nest/src/data-source.ts +23 -0
  63. package/templates/nest/src/main.ts +44 -0
  64. package/templates/nest/src/user/dto/create-user.dto.ts +50 -0
  65. package/templates/nest/src/user/dto/query-users.dto.ts +23 -0
  66. package/templates/nest/src/user/dto/update-user.dto.ts +15 -0
  67. package/templates/nest/src/user/entities/user.entity.ts +66 -0
  68. package/templates/nest/src/user/user.controller.ts +172 -0
  69. package/templates/nest/src/user/user.module.ts +15 -0
  70. package/templates/nest/src/user/user.repository.ts +61 -0
  71. package/templates/nest/src/user/user.service.ts +138 -0
  72. package/templates/nest/template.json +5 -0
  73. package/templates/nest/test/jest-e2e.json +12 -0
  74. package/templates/nest/tsconfig.build.json +4 -0
  75. package/templates/nest/tsconfig.json +25 -0
  76. package/templates/python/.env.example +37 -0
  77. package/templates/python/Dockerfile +18 -0
  78. package/templates/python/README.md +219 -0
  79. package/templates/python/addons/.github/dependabot.yml +11 -0
  80. package/templates/python/addons/.github/labeler.yml +29 -0
  81. package/templates/python/addons/.github/workflows/check-branch.yml +24 -0
  82. package/templates/python/addons/.github/workflows/ci.yml +30 -0
  83. package/templates/python/addons/.github/workflows/draft-release.yml +22 -0
  84. package/templates/python/addons/.github/workflows/pr-labeler.yml +16 -0
  85. package/templates/python/addons/.github/workflows/release.yml +30 -0
  86. package/templates/python/addons/.github/workflows/security-audit.yml +21 -0
  87. package/templates/python/addons/.github/workflows/semantic-pr.yml +31 -0
  88. package/templates/python/addons/.github/workflows/stale.yml +24 -0
  89. package/templates/python/addons/addons.json +17 -0
  90. package/templates/python/addons/cloudinary/service.py +67 -0
  91. package/templates/python/addons/pre-commit/.pre-commit-config.yaml +31 -0
  92. package/templates/python/alembic/env.py +56 -0
  93. package/templates/python/alembic/script.py.mako +26 -0
  94. package/templates/python/alembic/versions/.gitkeep +0 -0
  95. package/templates/python/alembic.ini +39 -0
  96. package/templates/python/app/__init__.py +0 -0
  97. package/templates/python/app/auth/__init__.py +5 -0
  98. package/templates/python/app/auth/dependencies.py +118 -0
  99. package/templates/python/app/auth/enums.py +6 -0
  100. package/templates/python/app/auth/models.py +18 -0
  101. package/templates/python/app/auth/router.py +68 -0
  102. package/templates/python/app/auth/schemas.py +58 -0
  103. package/templates/python/app/auth/service.py +180 -0
  104. package/templates/python/app/common/__init__.py +18 -0
  105. package/templates/python/app/common/base_model.py +26 -0
  106. package/templates/python/app/common/base_repository.py +83 -0
  107. package/templates/python/app/common/errors.py +35 -0
  108. package/templates/python/app/common/pagination.py +22 -0
  109. package/templates/python/app/common/schemas.py +20 -0
  110. package/templates/python/app/common/utils.py +15 -0
  111. package/templates/python/app/core/__init__.py +4 -0
  112. package/templates/python/app/core/config.py +55 -0
  113. package/templates/python/app/core/database.py +20 -0
  114. package/templates/python/app/main.py +33 -0
  115. package/templates/python/app/user/__init__.py +4 -0
  116. package/templates/python/app/user/models.py +26 -0
  117. package/templates/python/app/user/repository.py +84 -0
  118. package/templates/python/app/user/router.py +170 -0
  119. package/templates/python/app/user/schemas.py +60 -0
  120. package/templates/python/app/user/service.py +114 -0
  121. package/templates/python/docker-compose.yml +55 -0
  122. package/templates/python/pyproject.toml +46 -0
  123. package/templates/python/requirements-dev.txt +7 -0
  124. package/templates/python/requirements.txt +20 -0
  125. package/templates/python/template.json +5 -0
  126. package/utils/template.js +165 -0
  127. package/utils/git.js +0 -71
@@ -0,0 +1,17 @@
1
+ module.exports = {
2
+ moduleFileExtensions: [
3
+ 'js',
4
+ 'json',
5
+ 'ts',
6
+ ],
7
+ rootDir: 'src',
8
+ testRegex: '.*\\.spec\\.ts$',
9
+ transform: {
10
+ '^.+\.(t|j)s$': 'ts-jest',
11
+ },
12
+ collectCoverageFrom: [
13
+ '**/*.(t|j)s',
14
+ ],
15
+ coverageDirectory: '../coverage',
16
+ testEnvironment: 'node',
17
+ };
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true,
7
+ "assets": [
8
+ "templates/**/*"
9
+ ],
10
+ "watchAssets": true
11
+ }
12
+ }
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "site-ih-back",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "UNLICENSED",
8
+ "scripts": {
9
+ "build": "nest build",
10
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
+ "start": "npm run migration:run && nest start --watch",
12
+ "start:dev": "npm run migration:run && nest start --watch",
13
+ "start:debug": "npm run migration:run && nest start --debug --watch",
14
+ "start:prod": "npm run migration:run && node dist/main",
15
+ "heroku-postbuild": "npm run build",
16
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch",
19
+ "test:cov": "jest --coverage",
20
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21
+ "test:e2e": "jest --config ./test/jest-e2e.json",
22
+ "typeorm": "typeorm-ts-node-commonjs",
23
+ "migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts",
24
+ "migration:create": "npm run typeorm -- migration:create",
25
+ "migration:run": "npm run typeorm -- migration:run -d src/data-source.ts",
26
+ "migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts",
27
+ "migration:show": "npm run typeorm -- migration:show -d src/data-source.ts",
28
+ "prepare": "husky"
29
+ },
30
+ "dependencies": {
31
+ "@nestjs-modules/mailer": "^2.0.2",
32
+ "@nestjs/axios": "^4.0.1",
33
+ "@nestjs/common": "^11.1.9",
34
+ "@nestjs/config": "^4.0.2",
35
+ "@nestjs/core": "^11.1.9",
36
+ "@nestjs/jwt": "^11.0.0",
37
+ "@nestjs/passport": "^11.0.5",
38
+ "@nestjs/platform-express": "^11.0.1",
39
+ "@nestjs/swagger": "^11.2.0",
40
+ "@nestjs/typeorm": "^11.0.0",
41
+ "axios": "^1.13.2",
42
+ "bcryptjs": "^3.0.3",
43
+ "class-transformer": "^0.5.1",
44
+ "class-validator": "^0.14.2",
45
+ "cloudinary": "^2.9.0",
46
+ "handlebars": "^4.7.8",
47
+ "multer": "^2.0.2",
48
+ "multer-storage-cloudinary": "^4.0.0",
49
+ "nodemailer": "^8.0.1",
50
+ "passport": "^0.7.0",
51
+ "passport-jwt": "^4.0.1",
52
+ "passport-local": "^1.0.0",
53
+ "pg": "^8.16.3",
54
+ "reflect-metadata": "^0.2.2",
55
+ "rxjs": "^7.8.1",
56
+ "swagger-ui-express": "^5.0.1",
57
+ "typeorm": "^0.3.27",
58
+ "uuid": "^13.0.0"
59
+ },
60
+ "overrides": {
61
+ "glob": "^11.0.0",
62
+ "html-minifier": "^4.0.0",
63
+ "mjml": "^4.14.1",
64
+ "multer-storage-cloudinary": {
65
+ "cloudinary": "^2.9.0"
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@eslint/eslintrc": "^3.2.0",
70
+ "@eslint/js": "^10.0.1",
71
+ "@nestjs/cli": "^11.0.0",
72
+ "@nestjs/schematics": "^11.0.0",
73
+ "@nestjs/testing": "^11.0.1",
74
+ "@types/express": "^5.0.0",
75
+ "@types/jest": "^30.0.0",
76
+ "@types/multer": "^2.0.0",
77
+ "@types/node": "^25.2.3",
78
+ "@types/nodemailer": "^7.0.4",
79
+ "@types/passport-jwt": "^4.0.1",
80
+ "@types/passport-local": "^1.0.38",
81
+ "@types/supertest": "^6.0.2",
82
+ "@types/uuid": "^11.0.0",
83
+ "eslint": "^10.0.0",
84
+ "eslint-config-prettier": "^10.0.1",
85
+ "eslint-plugin-prettier": "^5.2.2",
86
+ "globals": "^17.3.0",
87
+ "husky": "^9.1.7",
88
+ "jest": "^30.0.0",
89
+ "prettier": "^3.4.2",
90
+ "source-map-support": "^0.5.21",
91
+ "supertest": "^7.0.0",
92
+ "ts-jest": "^29.2.5",
93
+ "ts-loader": "^9.5.2",
94
+ "ts-node": "^10.9.2",
95
+ "tsconfig-paths": "^4.2.0",
96
+ "typescript": "^5.7.3",
97
+ "typescript-eslint": "^8.20.0"
98
+ }
99
+ }
@@ -0,0 +1,7 @@
1
+ import { Controller } from '@nestjs/common';
2
+ import { AppService } from './app.service';
3
+
4
+ @Controller()
5
+ export class AppController {
6
+ constructor(private readonly appService: AppService) {}
7
+ }
@@ -0,0 +1,69 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
3
+ import { ConfigModule, ConfigService } from '@nestjs/config';
4
+ import { UserModule } from './user/user.module';
5
+ import { AuthModule } from './auth/auth.module';
6
+ import { AppController } from './app.controller';
7
+ import { AppService } from './app.service';
8
+ import { MailerModule } from '@nestjs-modules/mailer';
9
+ import { join } from 'path';
10
+ import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
11
+
12
+ @Module({
13
+ imports: [
14
+ UserModule,
15
+ AuthModule,
16
+
17
+ ConfigModule.forRoot({
18
+ isGlobal: true,
19
+ }),
20
+ TypeOrmModule.forRootAsync({
21
+ imports: [ConfigModule],
22
+ useFactory: (configService: ConfigService): TypeOrmModuleOptions => ({
23
+ type: configService.get<string>('DATABASE_TYPE') as 'postgres',
24
+ host: configService.get<string>('DATABASE_HOST'),
25
+ port: configService.get<number>('DATABASE_PORT'),
26
+ username: configService.get<string>('DATABASE_USERNAME'),
27
+ password: configService.get<string>('DATABASE_PASSWORD'),
28
+ database: configService.get<string>('DATABASE_NAME'),
29
+ entities: [__dirname + '/**/*.entity{.ts,.js}'],
30
+ synchronize: false,
31
+ ssl:
32
+ configService.get<string>('DATABASE_SSL') === 'true'
33
+ ? {
34
+ rejectUnauthorized: false,
35
+ }
36
+ : false,
37
+ }),
38
+ inject: [ConfigService],
39
+ }),
40
+ MailerModule.forRootAsync({
41
+ imports: [ConfigModule],
42
+ useFactory: (config: ConfigService) => ({
43
+ transport: {
44
+ host: config.get('MAIL_HOST'),
45
+ port: config.get('MAIL_PORT'),
46
+ secure: false,
47
+ auth: {
48
+ user: config.get('MAIL_USER'),
49
+ pass: config.get('MAIL_PASSWORD'),
50
+ },
51
+ },
52
+ defaults: {
53
+ from: config.get('MAIL_FROM'),
54
+ },
55
+ template: {
56
+ dir: join(process.cwd(), 'dist', 'templates'),
57
+ adapter: new HandlebarsAdapter(),
58
+ options: {
59
+ strict: true,
60
+ },
61
+ },
62
+ }),
63
+ inject: [ConfigService],
64
+ }),
65
+ ],
66
+ controllers: [AppController],
67
+ providers: [AppService],
68
+ })
69
+ export class AppModule {}
@@ -0,0 +1,4 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ @Injectable()
4
+ export class AppService {}
@@ -0,0 +1,97 @@
1
+ import {
2
+ Controller,
3
+ Post,
4
+ Body,
5
+ UseGuards,
6
+ Patch,
7
+ HttpCode,
8
+ HttpStatus,
9
+ } from '@nestjs/common';
10
+ import { User } from '../common/decorators/user.decorator';
11
+ import { UserEntity } from '../user/entities/user.entity';
12
+ import {
13
+ ApiTags,
14
+ ApiOperation,
15
+ ApiResponse,
16
+ ApiBody,
17
+ ApiBearerAuth,
18
+ } from '@nestjs/swagger';
19
+ import { AuthService } from './auth.service';
20
+ import { AuthGuard } from '@nestjs/passport';
21
+ import { LoginDto } from './dto/login.dto';
22
+ import { JwtAuthGuard } from './guards/jwt-auth.guard';
23
+ import { ChangePasswordDto } from './dto/change-password.dto';
24
+ import { RefreshTokenDto } from './dto/refresh-token.dto';
25
+ import { LoginResponseDto } from './dto/login-response.dto';
26
+ import { plainToInstance } from 'class-transformer';
27
+ import { RefreshTokenGuard } from './guards/refresh-token.guard';
28
+ import { RefreshTokenPayload } from './interfaces/jwt-payload.interface';
29
+
30
+ @ApiTags('auth')
31
+ @Controller('auth')
32
+ export class AuthController {
33
+ constructor(private readonly authService: AuthService) {}
34
+
35
+ @Post('login')
36
+ @UseGuards(AuthGuard('local'))
37
+ @ApiOperation({ summary: 'Autentica um usuário' })
38
+ @ApiBody({ type: LoginDto })
39
+ @ApiResponse({
40
+ status: 200,
41
+ description: 'Usuário autenticado com sucesso.',
42
+ type: LoginResponseDto,
43
+ })
44
+ async login(@User() user: UserEntity): Promise<LoginResponseDto> {
45
+ const result = await this.authService.login(user);
46
+ return plainToInstance(LoginResponseDto, result);
47
+ }
48
+
49
+ @Post('logout')
50
+ @UseGuards(RefreshTokenGuard)
51
+ @HttpCode(HttpStatus.OK)
52
+ @ApiOperation({ summary: 'Faz logout do usuário' })
53
+ @ApiBody({ type: RefreshTokenDto })
54
+ @ApiResponse({ status: 200, description: 'Logout realizado com sucesso.' })
55
+ @ApiResponse({ status: 401, description: 'Não autorizado.' })
56
+ logout(@User() user: RefreshTokenPayload & { refreshToken: string }) {
57
+ return this.authService.logout(user.jti);
58
+ }
59
+
60
+ @Post('refresh')
61
+ @UseGuards(RefreshTokenGuard)
62
+ @HttpCode(HttpStatus.OK)
63
+ @ApiOperation({ summary: 'Atualiza os tokens de acesso' })
64
+ @ApiBody({ type: RefreshTokenDto })
65
+ @ApiResponse({
66
+ status: 200,
67
+ description: 'Tokens atualizados com sucesso.',
68
+ type: LoginResponseDto,
69
+ })
70
+ @ApiResponse({
71
+ status: 401,
72
+ description: 'O refresh token é inválido ou expirou.',
73
+ })
74
+ async refreshTokens(
75
+ @User() user: RefreshTokenPayload & { refreshToken: string },
76
+ ): Promise<LoginResponseDto> {
77
+ const { sub, jti, refreshToken } = user;
78
+ const result = await this.authService.refreshTokens(sub, jti, refreshToken);
79
+ return plainToInstance(LoginResponseDto, result);
80
+ }
81
+
82
+ @Patch('change-password')
83
+ @ApiOperation({ summary: 'Altera a senha do usuário logado' })
84
+ @ApiBearerAuth()
85
+ @ApiResponse({ status: 200, description: 'Senha alterada com sucesso.' })
86
+ @ApiResponse({ status: 401, description: 'Não autorizado.' })
87
+ @ApiResponse({ status: 400, description: 'Requisição inválida.' })
88
+ @ApiBody({ type: ChangePasswordDto })
89
+ @UseGuards(JwtAuthGuard)
90
+ async changePassword(
91
+ @User() user: UserEntity,
92
+ @Body() changePasswordDto: ChangePasswordDto,
93
+ ): Promise<{ message: string }> {
94
+ const id = user.id;
95
+ return this.authService.changePassword(id, changePasswordDto);
96
+ }
97
+ }
@@ -0,0 +1,46 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { AuthController } from './auth.controller';
3
+ import { AuthService } from './auth.service';
4
+ import { PassportModule } from '@nestjs/passport';
5
+ import { ConfigModule, ConfigService } from '@nestjs/config';
6
+ import { UserModule } from '../user/user.module';
7
+ import { JwtModule } from '@nestjs/jwt';
8
+ import { JwtAuthGuard } from './guards/jwt-auth.guard';
9
+ import { JwtStrategy } from './strategies/jwt.strategy';
10
+ import { LocalStrategy } from './strategies/local.strategy';
11
+ import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';
12
+ import { RefreshTokenGuard } from './guards/refresh-token.guard';
13
+ import type { StringValue } from 'ms';
14
+ import { TypeOrmModule } from '@nestjs/typeorm';
15
+ import { RefreshTokenEntity } from './entities/refresh-token.entity';
16
+
17
+ @Module({
18
+ imports: [
19
+ UserModule,
20
+ PassportModule,
21
+ ConfigModule,
22
+ TypeOrmModule.forFeature([RefreshTokenEntity]),
23
+ JwtModule.registerAsync({
24
+ imports: [ConfigModule],
25
+ useFactory: (configService: ConfigService) => ({
26
+ secret: configService.getOrThrow<string>('JWT_SECRET'),
27
+ signOptions: {
28
+ expiresIn: (configService.get<string>('JWT_EXPIRATION_TIME') ||
29
+ '20m') as StringValue,
30
+ },
31
+ }),
32
+ inject: [ConfigService],
33
+ }),
34
+ ],
35
+ controllers: [AuthController],
36
+ providers: [
37
+ AuthService,
38
+ JwtAuthGuard,
39
+ JwtStrategy,
40
+ LocalStrategy,
41
+ RefreshTokenStrategy,
42
+ RefreshTokenGuard,
43
+ ],
44
+ exports: [AuthService, JwtAuthGuard],
45
+ })
46
+ export class AuthModule {}
@@ -0,0 +1,231 @@
1
+ import {
2
+ Injectable,
3
+ BadRequestException,
4
+ UnauthorizedException,
5
+ ForbiddenException,
6
+ } from '@nestjs/common';
7
+ import { ConfigService } from '@nestjs/config';
8
+ import { UserService } from '../user/user.service';
9
+ import { JwtService } from '@nestjs/jwt';
10
+ import { ERRORS } from '../common/constants/errors.constants';
11
+ import { UserEntity } from '../user/entities/user.entity';
12
+ import { ChangePasswordDto } from './dto/change-password.dto';
13
+ import * as bcrypt from 'bcryptjs';
14
+ import { UsersRepository } from '../user/user.repository';
15
+ import { InjectRepository } from '@nestjs/typeorm';
16
+ import { RefreshTokenEntity } from './entities/refresh-token.entity';
17
+ import { Repository } from 'typeorm';
18
+ import { v4 as uuidv4 } from 'uuid';
19
+ import { LoginResponseDto } from './dto/login-response.dto';
20
+ import { parseDurationToSeconds } from '../common/utils/duration.utils';
21
+
22
+ @Injectable()
23
+ export class AuthService {
24
+ constructor(
25
+ private userService: UserService,
26
+ private jwtService: JwtService,
27
+ private configService: ConfigService,
28
+ private usersRepository: UsersRepository,
29
+ @InjectRepository(RefreshTokenEntity)
30
+ private refreshTokenRepository: Repository<RefreshTokenEntity>,
31
+ ) {}
32
+
33
+ async validateUser(email: string, pass: string): Promise<UserEntity | null> {
34
+ const user = await this.userService.findOneByEmailWithPassword(email);
35
+
36
+ if (user && user.password) {
37
+ const isMatch = await this.userService.comparePassword(
38
+ pass,
39
+ user.password,
40
+ );
41
+
42
+ if (isMatch) {
43
+ if (user.deletedAt) {
44
+ throw new UnauthorizedException(ERRORS.AUTH.ACCOUNT_DELETED);
45
+ }
46
+ if (!user.isActive) {
47
+ throw new UnauthorizedException(ERRORS.AUTH.ACCOUNT_DISABLED);
48
+ }
49
+ return user;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ async login(user: UserEntity): Promise<LoginResponseDto> {
56
+ return this.generateAuthResponse(user);
57
+ }
58
+
59
+ async logout(jti: string): Promise<void> {
60
+ if (!jti) {
61
+ return;
62
+ }
63
+ const token = await this.refreshTokenRepository.findOne({
64
+ where: { jti },
65
+ });
66
+ if (token) {
67
+ token.isRevoked = true;
68
+ await this.refreshTokenRepository.save(token);
69
+ }
70
+ }
71
+
72
+ async refreshTokens(
73
+ id: string,
74
+ oldJti: string,
75
+ refreshToken: string,
76
+ ): Promise<LoginResponseDto> {
77
+ const tokenRecord = await this.refreshTokenRepository.findOne({
78
+ where: { jti: oldJti },
79
+ });
80
+
81
+ if (!tokenRecord || tokenRecord.isRevoked) {
82
+ throw new ForbiddenException(ERRORS.AUTH.ACCESS_DENIED);
83
+ }
84
+
85
+ const tokenMatches = await bcrypt.compare(
86
+ refreshToken,
87
+ tokenRecord.hashedToken,
88
+ );
89
+
90
+ if (!tokenMatches) {
91
+ throw new ForbiddenException(ERRORS.AUTH.ACCESS_DENIED);
92
+ }
93
+
94
+ tokenRecord.isRevoked = true;
95
+ await this.refreshTokenRepository.save(tokenRecord);
96
+
97
+ const user = await this.userService.findById(id);
98
+ if (!user) {
99
+ throw new UnauthorizedException(ERRORS.AUTH.NOT_FOUND);
100
+ }
101
+
102
+ return this.generateAuthResponse(user);
103
+ }
104
+
105
+ private async generateAuthResponse(
106
+ user: UserEntity,
107
+ ): Promise<LoginResponseDto> {
108
+ const jti = uuidv4();
109
+ const { accessTokenExpiresIn, refreshTokenExpiresIn } =
110
+ this.getExpirationTimes();
111
+
112
+ const tokens = await this.getTokens(
113
+ user,
114
+ jti,
115
+ accessTokenExpiresIn,
116
+ refreshTokenExpiresIn,
117
+ );
118
+
119
+ await this.createRefreshToken(user, jti, tokens.refreshToken);
120
+
121
+ return {
122
+ ...tokens,
123
+ expiresIn: accessTokenExpiresIn,
124
+ refreshExpiresIn: refreshTokenExpiresIn,
125
+ user,
126
+ };
127
+ }
128
+
129
+ private getExpirationTimes() {
130
+ const expiresInConfig = this.configService.get<string>(
131
+ 'JWT_EXPIRATION_TIME',
132
+ );
133
+ const refreshExpiresInConfig = this.configService.get<string>(
134
+ 'JWT_REFRESH_EXPIRATION_TIME',
135
+ );
136
+
137
+ return {
138
+ accessTokenExpiresIn: parseDurationToSeconds(expiresInConfig, 15 * 60),
139
+ refreshTokenExpiresIn: parseDurationToSeconds(
140
+ refreshExpiresInConfig,
141
+ 7 * 24 * 60 * 60,
142
+ ),
143
+ };
144
+ }
145
+
146
+ async createRefreshToken(
147
+ user: UserEntity,
148
+ jti: string,
149
+ token: string,
150
+ ): Promise<void> {
151
+ const hashedToken = await bcrypt.hash(token, 10);
152
+ const newRefreshToken = this.refreshTokenRepository.create({
153
+ user: user,
154
+ jti,
155
+ hashedToken,
156
+ isRevoked: false,
157
+ });
158
+ await this.refreshTokenRepository.save(newRefreshToken);
159
+ }
160
+
161
+ private async getTokens(
162
+ user: UserEntity,
163
+ jti: string,
164
+ accessTokenExpiresIn: number,
165
+ refreshTokenExpiresIn: number,
166
+ ): Promise<{ accessToken: string; refreshToken: string }> {
167
+ const [accessToken, refreshToken] = await Promise.all([
168
+ this.jwtService.signAsync(
169
+ {
170
+ sub: user.id,
171
+ email: user.email,
172
+ role: user.role,
173
+ },
174
+ {
175
+ secret: this.configService.get<string>('JWT_SECRET'),
176
+ expiresIn: accessTokenExpiresIn,
177
+ },
178
+ ),
179
+ this.jwtService.signAsync(
180
+ {
181
+ sub: user.id,
182
+ email: user.email,
183
+ role: user.role,
184
+ jti,
185
+ },
186
+ {
187
+ secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
188
+ expiresIn: refreshTokenExpiresIn,
189
+ },
190
+ ),
191
+ ]);
192
+
193
+ return {
194
+ accessToken,
195
+ refreshToken,
196
+ };
197
+ }
198
+
199
+ async changePassword(
200
+ id: string,
201
+ changePasswordDto: ChangePasswordDto,
202
+ ): Promise<{ message: string }> {
203
+ const user = await this.userService.findOneByIdWithPassword(id);
204
+
205
+ if (!user) {
206
+ throw new UnauthorizedException(ERRORS.AUTH.NOT_FOUND);
207
+ }
208
+
209
+ const isMatch = await this.userService.comparePassword(
210
+ changePasswordDto.oldPassword,
211
+ user.password,
212
+ );
213
+
214
+ if (!isMatch) {
215
+ throw new UnauthorizedException(ERRORS.AUTH.OLD_PASSWORD_INCORRECT);
216
+ }
217
+
218
+ if (changePasswordDto.oldPassword === changePasswordDto.newPassword) {
219
+ throw new BadRequestException(ERRORS.AUTH.PASSWORD_SAME_AS_OLD);
220
+ }
221
+
222
+ const hashedPassword = await bcrypt.hash(changePasswordDto.newPassword, 12);
223
+
224
+ await this.userService.updatePassword(id, hashedPassword);
225
+
226
+ if (user.mustChangePassword) {
227
+ await this.userService.disableMustChangePasswordFlag(id);
228
+ }
229
+ return { message: ERRORS.AUTH.PASSWORD_CHANGED };
230
+ }
231
+ }
@@ -0,0 +1,5 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+ import { Role } from '../enums/role.enum';
3
+
4
+ export const ROLES_KEY = 'roles';
5
+ export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
@@ -0,0 +1,21 @@
1
+ import { IsString, MinLength } from 'class-validator';
2
+ import { ApiProperty } from '@nestjs/swagger';
3
+ import { ERRORS } from '../../common/constants/errors.constants';
4
+
5
+ export class ChangePasswordDto {
6
+ @ApiProperty({
7
+ description: 'A senha atual (antiga) do usuário.',
8
+ example: 'MinhaSenhaAntiga123',
9
+ })
10
+ @IsString()
11
+ oldPassword!: string;
12
+
13
+ @ApiProperty({
14
+ description:
15
+ 'A nova senha que o usuário deseja definir. Deve ter no mínimo 8 caracteres.',
16
+ example: 'NovaSenhaForte@2025',
17
+ })
18
+ @IsString()
19
+ @MinLength(8, { message: ERRORS.AUTH.PASSWORD_MIN_LENGTH })
20
+ newPassword!: string;
21
+ }
@@ -0,0 +1,25 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { Expose } from 'class-transformer';
3
+ import { UserEntity } from '../../user/entities/user.entity';
4
+
5
+ export class LoginResponseDto {
6
+ @ApiProperty()
7
+ @Expose()
8
+ accessToken: string;
9
+
10
+ @ApiProperty()
11
+ @Expose()
12
+ refreshToken: string;
13
+
14
+ @ApiProperty({ type: 'number' })
15
+ @Expose()
16
+ expiresIn: number;
17
+
18
+ @ApiProperty({ type: 'number' })
19
+ @Expose()
20
+ refreshExpiresIn: number;
21
+
22
+ @ApiProperty({ type: () => UserEntity })
23
+ @Expose()
24
+ user: UserEntity;
25
+ }
@@ -0,0 +1,15 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsEmail, IsNotEmpty } from 'class-validator';
3
+
4
+ export class LoginDto {
5
+ @ApiProperty({
6
+ description: 'O endereço de e-mail do usuário',
7
+ example: 'joao.silva@exemplo.com',
8
+ })
9
+ @IsEmail()
10
+ email!: string;
11
+
12
+ @ApiProperty({ description: 'A senha do usuário' })
13
+ @IsNotEmpty()
14
+ password!: string;
15
+ }
@@ -0,0 +1,12 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsNotEmpty, IsString } from 'class-validator';
3
+
4
+ export class RefreshTokenDto {
5
+ @ApiProperty({
6
+ description: 'Refresh token recebido no login',
7
+ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
8
+ })
9
+ @IsNotEmpty()
10
+ @IsString()
11
+ refreshToken!: string;
12
+ }