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,18 @@
1
+ import { BaseEntity } from '../../common/base.entity';
2
+ import { UserEntity } from '../../user/entities/user.entity';
3
+ import { Entity, Column, ManyToOne } from 'typeorm';
4
+
5
+ @Entity('refresh_tokens')
6
+ export class RefreshTokenEntity extends BaseEntity {
7
+ @Column({ unique: true })
8
+ public jti: string;
9
+
10
+ @Column()
11
+ public hashedToken: string;
12
+
13
+ @ManyToOne(() => UserEntity, (user) => user.refreshTokens)
14
+ public user: UserEntity;
15
+
16
+ @Column({ default: false })
17
+ public isRevoked: boolean;
18
+ }
@@ -0,0 +1,4 @@
1
+ export enum Role {
2
+ User = 'user',
3
+ Admin = 'admin',
4
+ }
@@ -0,0 +1,5 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AuthGuard } from '@nestjs/passport';
3
+
4
+ @Injectable()
5
+ export class JwtAuthGuard extends AuthGuard('jwt') {}
@@ -0,0 +1,5 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AuthGuard } from '@nestjs/passport';
3
+
4
+ @Injectable()
5
+ export class RefreshTokenGuard extends AuthGuard('jwt-refresh') {}
@@ -0,0 +1,23 @@
1
+ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { Role } from '../enums/role.enum';
4
+ import { ROLES_KEY } from '../decorators/roles.decorator';
5
+
6
+ @Injectable()
7
+ export class RolesGuard implements CanActivate {
8
+ constructor(private reflector: Reflector) {}
9
+
10
+ canActivate(context: ExecutionContext): boolean {
11
+ const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
12
+ context.getHandler(),
13
+ context.getClass(),
14
+ ]);
15
+ if (!requiredRoles) {
16
+ return true;
17
+ }
18
+ const { user }: { user: { role: Role } } = context
19
+ .switchToHttp()
20
+ .getRequest();
21
+ return requiredRoles.some((role) => user?.role === role);
22
+ }
23
+ }
@@ -0,0 +1,10 @@
1
+ export interface JwtPayload {
2
+ sub: string;
3
+ email: string;
4
+ role: string;
5
+ jti?: string;
6
+ }
7
+
8
+ export interface RefreshTokenPayload extends JwtPayload {
9
+ jti: string;
10
+ }
@@ -0,0 +1,28 @@
1
+ import { Injectable, UnauthorizedException } from '@nestjs/common';
2
+ import { ERRORS } from '../../common/constants/errors.constants';
3
+ import { PassportStrategy } from '@nestjs/passport';
4
+ import { Strategy, ExtractJwt } from 'passport-jwt';
5
+ import { ConfigService } from '@nestjs/config';
6
+ import { UserService } from '../../user/user.service';
7
+
8
+ @Injectable()
9
+ export class JwtStrategy extends PassportStrategy(Strategy) {
10
+ constructor(
11
+ private configService: ConfigService,
12
+ private userService: UserService,
13
+ ) {
14
+ super({
15
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
16
+ ignoreExpiration: false,
17
+ secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
18
+ });
19
+ }
20
+ async validate(payload: { sub: string; email: string }) {
21
+ const user = await this.userService.findById(payload.sub);
22
+
23
+ if (!user) {
24
+ throw new UnauthorizedException(ERRORS.AUTH.INVALID_TOKEN);
25
+ }
26
+ return user;
27
+ }
28
+ }
@@ -0,0 +1,23 @@
1
+ import { Injectable, UnauthorizedException } from '@nestjs/common';
2
+ import { ERRORS } from '../../common/constants/errors.constants';
3
+ import { PassportStrategy } from '@nestjs/passport';
4
+ import { Strategy } from 'passport-local';
5
+ import { AuthService } from '../auth.service';
6
+ import { UserEntity } from '../../user/entities/user.entity';
7
+
8
+ @Injectable()
9
+ export class LocalStrategy extends PassportStrategy(Strategy) {
10
+ constructor(private authService: AuthService) {
11
+ super({
12
+ usernameField: 'email',
13
+ });
14
+ }
15
+ async validate(email: string, password: string): Promise<UserEntity> {
16
+ const user = await this.authService.validateUser(email, password);
17
+
18
+ if (!user) {
19
+ throw new UnauthorizedException(ERRORS.AUTH.INVALID_CREDENTIALS);
20
+ }
21
+ return user;
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { PassportStrategy } from '@nestjs/passport';
3
+ import { ExtractJwt, Strategy } from 'passport-jwt';
4
+ import { Request } from 'express';
5
+ import { ConfigService } from '@nestjs/config';
6
+ import {
7
+ JwtPayload,
8
+ RefreshTokenPayload,
9
+ } from '../interfaces/jwt-payload.interface';
10
+
11
+ @Injectable()
12
+ export class RefreshTokenStrategy extends PassportStrategy(
13
+ Strategy,
14
+ 'jwt-refresh',
15
+ ) {
16
+ constructor(private readonly configService: ConfigService) {
17
+ super({
18
+ jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'),
19
+ secretOrKey: configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
20
+ passReqToCallback: true,
21
+ });
22
+ }
23
+
24
+ validate(
25
+ req: Request,
26
+ payload: JwtPayload,
27
+ ): RefreshTokenPayload & { refreshToken: string } {
28
+ const body = req.body as { refreshToken: string };
29
+ const refreshToken = body.refreshToken;
30
+ return { ...payload, refreshToken, jti: payload.jti! };
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import {
2
+ BaseEntity as TypeOrmBaseEntity,
3
+ PrimaryGeneratedColumn,
4
+ CreateDateColumn,
5
+ UpdateDateColumn,
6
+ } from 'typeorm';
7
+ import { Expose } from 'class-transformer';
8
+
9
+ export abstract class BaseEntity extends TypeOrmBaseEntity {
10
+ @Expose()
11
+ @PrimaryGeneratedColumn('uuid')
12
+ id: string;
13
+
14
+ @CreateDateColumn()
15
+ createdAt: Date;
16
+
17
+ @UpdateDateColumn()
18
+ updatedAt: Date;
19
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ DeepPartial,
3
+ FindOptionsOrder,
4
+ Repository,
5
+ FindManyOptions,
6
+ FindOptionsWhere,
7
+ } from 'typeorm';
8
+ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
9
+ import { BaseEntity } from './base.entity';
10
+ import { IRepository } from './irepository';
11
+ import { buildPaginationMeta, PaginatedResult } from './utils/pagination.utils';
12
+
13
+ export abstract class BaseRepository<
14
+ T extends BaseEntity,
15
+ > implements IRepository<T> {
16
+ constructor(protected readonly repository: Repository<T>) {}
17
+
18
+ public get repo(): Repository<T> {
19
+ return this.repository;
20
+ }
21
+
22
+ async findAll(): Promise<T[]> {
23
+ return this.repository.find({
24
+ order: { createdAt: 'DESC' } as FindOptionsOrder<T>,
25
+ });
26
+ }
27
+
28
+ async findById(id: string): Promise<T | null> {
29
+ if (!id) {
30
+ return null;
31
+ }
32
+ return this.repository.findOneBy({ id } as FindOptionsWhere<T>);
33
+ }
34
+
35
+ async save(entity: T): Promise<T> {
36
+ return this.repository.save(entity);
37
+ }
38
+
39
+ async create(data: DeepPartial<T>): Promise<T> {
40
+ const entity = this.repository.create(data);
41
+ return this.repository.save(entity);
42
+ }
43
+
44
+ createInstance(data: DeepPartial<T>): T {
45
+ return this.repository.create(data);
46
+ }
47
+
48
+ async update(id: string, data: QueryDeepPartialEntity<T>): Promise<T | null> {
49
+ if (!id) {
50
+ return null;
51
+ }
52
+ await this.repository.update(id, data);
53
+ return this.findById(id);
54
+ }
55
+
56
+ async delete(id: string): Promise<void> {
57
+ if (!id) {
58
+ return;
59
+ }
60
+ await this.repository.delete(id);
61
+ }
62
+
63
+ async findAllPaginated(
64
+ options: FindManyOptions<T>,
65
+ meta: { page: number; limit: number },
66
+ ): Promise<PaginatedResult<T>> {
67
+ const { page, limit } = meta;
68
+ const [data, totalItems] = await this.repository.findAndCount({
69
+ ...options,
70
+ skip: (page - 1) * limit,
71
+ take: limit,
72
+ });
73
+
74
+ return {
75
+ data,
76
+ meta: buildPaginationMeta(totalItems, page, limit, data.length),
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,28 @@
1
+ import { DeepPartial } from 'typeorm';
2
+ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
3
+ import { BaseEntity } from './base.entity';
4
+ import { IRepository } from './irepository';
5
+
6
+ export abstract class BaseService<T extends BaseEntity> {
7
+ constructor(private readonly repository: IRepository<T>) {}
8
+
9
+ async findAll(): Promise<T[]> {
10
+ return this.repository.findAll();
11
+ }
12
+
13
+ async findById(id: string): Promise<T | null> {
14
+ return this.repository.findById(id);
15
+ }
16
+
17
+ async create(data: DeepPartial<T>): Promise<T> {
18
+ return this.repository.create(data);
19
+ }
20
+
21
+ async update(id: string, data: QueryDeepPartialEntity<T>): Promise<T | null> {
22
+ return this.repository.update(id, data);
23
+ }
24
+
25
+ async delete(id: string): Promise<void> {
26
+ return this.repository.delete(id);
27
+ }
28
+ }
@@ -0,0 +1,33 @@
1
+ export const ERRORS = {
2
+ AUTH: {
3
+ NOT_FOUND: 'Usuário não encontrado.',
4
+ INVALID_TOKEN: 'Token inválido.',
5
+ ACCESS_DENIED: 'Acesso negado',
6
+ EMAIL_IN_USE: 'O e-mail já está em uso.',
7
+ INVALID_CREDENTIALS:
8
+ 'E-mail ou senha inválidos. Por favor, verifique seus dados e tente novamente.',
9
+ ACCOUNT_DELETED:
10
+ 'Sua conta foi excluída. Por favor, entre em contato com o suporte.',
11
+ ACCOUNT_DISABLED:
12
+ 'Sua conta está desativada. Por favor, entre em contato com o suporte.',
13
+ OLD_PASSWORD_INCORRECT: 'A senha antiga está incorreta. Tente novamente.',
14
+ PASSWORD_SAME_AS_OLD:
15
+ 'A nova senha deve ser diferente da senha antiga. Por favor, escolha uma nova senha.',
16
+ PASSWORD_MIN_LENGTH: 'A nova senha deve ter no mínimo 8 caracteres.',
17
+ PASSWORD_CHANGED: 'Senha alterada com sucesso.',
18
+ },
19
+ USER: {
20
+ NOT_FOUND: 'Usuário não encontrado.',
21
+ EMAIL_IN_USE: 'O e-mail já está em uso.',
22
+ DEFAULT_PASSWORD_NOT_SET:
23
+ 'Variável de ambiente DEFAULT_PASSWORD não configurada.',
24
+ },
25
+ IMAGE: {
26
+ REQUIRED: 'O arquivo de imagem é obrigatório.',
27
+ },
28
+ COMMON: {
29
+ NOT_FOUND: 'Recurso não encontrado.',
30
+ BAD_REQUEST: 'Requisição inválida.',
31
+ INVALID_ARRAY_FORMAT: 'Formato de array inválido.',
32
+ },
33
+ };
@@ -0,0 +1,9 @@
1
+ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2
+ import { Request } from 'express';
3
+
4
+ export const User = createParamDecorator(
5
+ (data: unknown, ctx: ExecutionContext) => {
6
+ const request = ctx.switchToHttp().getRequest<Request>();
7
+ return request.user;
8
+ },
9
+ );
@@ -0,0 +1,56 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { Transform, Type } from 'class-transformer';
3
+ import { IsInt, IsOptional, IsString, Max, Min, IsIn } from 'class-validator';
4
+
5
+ export class BaseQueryDto {
6
+ @ApiPropertyOptional({
7
+ description: 'Número da página que deseja buscar.',
8
+ default: 1,
9
+ minimum: 1,
10
+ })
11
+ @IsOptional()
12
+ @Type(() => Number)
13
+ @IsInt()
14
+ @Min(1)
15
+ page: number = 1;
16
+
17
+ @ApiPropertyOptional({
18
+ description: 'Quantidade de itens por página.',
19
+ default: 10,
20
+ minimum: 1,
21
+ maximum: 100,
22
+ })
23
+ @IsOptional()
24
+ @Type(() => Number)
25
+ @IsInt()
26
+ @Min(1)
27
+ @Max(100)
28
+ limit: number = 10;
29
+
30
+ @ApiPropertyOptional({
31
+ description: 'Termo de busca.',
32
+ example: 'termo',
33
+ })
34
+ @IsOptional()
35
+ @IsString()
36
+ search?: string;
37
+
38
+ @ApiPropertyOptional({
39
+ description: 'Coluna pela qual os resultados serão ordenados.',
40
+ default: 'createdAt',
41
+ })
42
+ @IsOptional()
43
+ @IsString()
44
+ sortBy?: string = 'createdAt';
45
+
46
+ @ApiPropertyOptional({
47
+ description: 'Ordem da classificação (ascendente ou descendente).',
48
+ default: 'DESC',
49
+ enum: ['ASC', 'DESC'],
50
+ })
51
+ @IsOptional()
52
+ @IsString()
53
+ @Transform(({ value }: { value: string }) => value.toUpperCase())
54
+ @IsIn(['ASC', 'DESC'])
55
+ sortOrder?: 'ASC' | 'DESC' = 'DESC';
56
+ }
@@ -0,0 +1,18 @@
1
+ import { DeepPartial, FindManyOptions } from 'typeorm';
2
+ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
3
+ import { BaseEntity } from './base.entity';
4
+ import { PaginatedResult } from './utils/pagination.utils';
5
+
6
+ export interface IRepository<T extends BaseEntity> {
7
+ findAll(): Promise<T[]>;
8
+ findById(id: string): Promise<T | null>;
9
+ save(entity: T): Promise<T>;
10
+ create(data: DeepPartial<T>): Promise<T>;
11
+ createInstance(data: DeepPartial<T>): T;
12
+ update(id: string, data: QueryDeepPartialEntity<T>): Promise<T | null>;
13
+ delete(id: string): Promise<void>;
14
+ findAllPaginated(
15
+ options: FindManyOptions<T>,
16
+ meta: { page: number; limit: number },
17
+ ): Promise<PaginatedResult<T>>;
18
+ }
@@ -0,0 +1,33 @@
1
+ export function parseDurationToSeconds(
2
+ value: string | number | undefined,
3
+ defaultSeconds: number,
4
+ ): number {
5
+ if (value === undefined || value === null) {
6
+ return defaultSeconds;
7
+ }
8
+ if (typeof value === 'number') {
9
+ return value;
10
+ }
11
+
12
+ const trimmed = (value || '').toString().trim();
13
+ const match = /^(\d+)\s*(s|m|h|d)?$/i.exec(trimmed);
14
+ if (!match) {
15
+ return defaultSeconds;
16
+ }
17
+
18
+ const amount = parseInt(match[1], 10);
19
+ const unit = (match[2] || 's').toLowerCase();
20
+
21
+ switch (unit) {
22
+ case 's':
23
+ return amount;
24
+ case 'm':
25
+ return amount * 60;
26
+ case 'h':
27
+ return amount * 60 * 60;
28
+ case 'd':
29
+ return amount * 60 * 60 * 24;
30
+ default:
31
+ return defaultSeconds;
32
+ }
33
+ }
@@ -0,0 +1,35 @@
1
+ export interface PaginationMeta {
2
+ totalItems: number;
3
+ itemCount: number;
4
+ itemsPerPage: number;
5
+ totalPages: number;
6
+ currentPage: number;
7
+ }
8
+
9
+ export interface PaginatedResult<T> {
10
+ data: T[];
11
+ meta: PaginationMeta;
12
+ }
13
+
14
+ /**
15
+ * Constrói o objeto de metadados de paginação.
16
+ *
17
+ * @param totalItems - Total de registros no banco
18
+ * @param page - Página atual
19
+ * @param limit - Itens por página
20
+ * @param itemCount - Quantidade de itens retornados nesta página
21
+ */
22
+ export function buildPaginationMeta(
23
+ totalItems: number,
24
+ page: number,
25
+ limit: number,
26
+ itemCount: number,
27
+ ): PaginationMeta {
28
+ return {
29
+ totalItems,
30
+ itemCount,
31
+ itemsPerPage: limit,
32
+ totalPages: Math.ceil(totalItems / limit),
33
+ currentPage: page,
34
+ };
35
+ }
@@ -0,0 +1,14 @@
1
+ export function createSlug(title: string): string {
2
+ let slug = title
3
+ .toLowerCase()
4
+ .normalize('NFD')
5
+ .replace(/[\u0300-\u036f]/g, '')
6
+ .replace(/[^a-z0-9]+/g, '-')
7
+ .replace(/(^-|-$)+/g, '');
8
+
9
+ if (slug.length > 50) {
10
+ slug = slug.substring(0, 50).replace(/-$/, '');
11
+ }
12
+
13
+ return slug;
14
+ }
@@ -0,0 +1,62 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ import { ERRORS } from '../constants/errors.constants';
3
+
4
+ export function parseJSON<T = unknown>(value: unknown): T {
5
+ if (typeof value === 'string') {
6
+ if (value.trim() === '') {
7
+ return undefined as T;
8
+ }
9
+
10
+ try {
11
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
12
+ return JSON.parse(value);
13
+ } catch {
14
+ throw new BadRequestException(ERRORS.COMMON.BAD_REQUEST);
15
+ }
16
+ }
17
+ return value as T;
18
+ }
19
+
20
+ export function parseBoolean(value: unknown): boolean {
21
+ if (value === 'false' || value === false) return false;
22
+ return true;
23
+ }
24
+
25
+ export function parseStringArray(value: unknown): string[] {
26
+ if (value === undefined || value === null || value === '') return [];
27
+
28
+ if (Array.isArray(value)) {
29
+ return value.map(String).filter((s) => s.trim().length > 0);
30
+ }
31
+
32
+ if (typeof value === 'number') {
33
+ return [String(value)];
34
+ }
35
+
36
+ if (typeof value === 'string') {
37
+ const trimmed = value.trim();
38
+ if (!trimmed) return [];
39
+
40
+ if (trimmed.startsWith('[')) {
41
+ try {
42
+ const parsed = JSON.parse(trimmed) as unknown;
43
+ if (Array.isArray(parsed)) {
44
+ return parsed.map(String).filter((s) => s.trim().length > 0);
45
+ }
46
+ } catch {
47
+ throw new BadRequestException(ERRORS.COMMON.INVALID_ARRAY_FORMAT);
48
+ }
49
+ }
50
+
51
+ if (trimmed.includes(',')) {
52
+ return trimmed
53
+ .split(',')
54
+ .map((s) => s.trim())
55
+ .filter((s) => s.length > 0);
56
+ }
57
+
58
+ return [trimmed];
59
+ }
60
+
61
+ return [];
62
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ registerDecorator,
3
+ ValidationOptions,
4
+ ValidationArguments,
5
+ } from 'class-validator';
6
+
7
+ export function IsDateAfter(
8
+ property: string,
9
+ validationOptions?: ValidationOptions,
10
+ ) {
11
+ return function (object: object, propertyName: string) {
12
+ registerDecorator({
13
+ name: 'isDateAfter',
14
+ target: object.constructor,
15
+ propertyName: propertyName,
16
+ constraints: [property],
17
+ options: validationOptions,
18
+ validator: {
19
+ validate(value: any, args: ValidationArguments) {
20
+ const [relatedPropertyName] = args.constraints as string[];
21
+ const relatedValue = (args.object as Record<string, unknown>)[
22
+ relatedPropertyName
23
+ ];
24
+
25
+ if (!value || !relatedValue) {
26
+ return true;
27
+ }
28
+
29
+ const startDate = new Date(relatedValue as string | number | Date);
30
+ const endDate = new Date(value as string | number | Date);
31
+
32
+ return endDate >= startDate;
33
+ },
34
+ defaultMessage() {
35
+ return `A data de término deve ser maior ou igual à data de início`;
36
+ },
37
+ },
38
+ });
39
+ };
40
+ }
@@ -0,0 +1,23 @@
1
+ import { DataSource } from 'typeorm';
2
+ import * as dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ export default new DataSource({
7
+ type: 'postgres',
8
+ host: process.env.DATABASE_HOST,
9
+ port: Number(process.env.DATABASE_PORT),
10
+ username: process.env.DATABASE_USERNAME,
11
+ password: process.env.DATABASE_PASSWORD,
12
+ database: process.env.DATABASE_NAME,
13
+ entities: [__dirname + '/**/*.entity{.ts,.js}'],
14
+ migrations: ['src/migrations/*.ts'],
15
+ migrationsTableName: 'migrations',
16
+ synchronize: false,
17
+ ssl:
18
+ process.env.DATABASE_SSL === 'true'
19
+ ? {
20
+ rejectUnauthorized: false,
21
+ }
22
+ : false,
23
+ });
@@ -0,0 +1,44 @@
1
+ import { NestFactory, Reflector } from '@nestjs/core';
2
+ import { AppModule } from './app.module';
3
+ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
6
+ import { useContainer } from 'class-validator';
7
+
8
+ async function bootstrap() {
9
+ const app = await NestFactory.create(AppModule);
10
+ const configService = app.get(ConfigService);
11
+
12
+ app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
13
+ app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
14
+
15
+ useContainer(app.select(AppModule), { fallbackOnErrors: true });
16
+
17
+ const corsOrigin = configService.get<string>('CORS_ORIGIN');
18
+ const origins = corsOrigin ? corsOrigin.split(',') : [];
19
+
20
+ app.enableCors({
21
+ origin: origins,
22
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
23
+ allowedHeaders: ['Content-Type', 'Authorization'],
24
+ credentials: true,
25
+ });
26
+
27
+ const config = new DocumentBuilder()
28
+ .setTitle('API da Landing Page/Blog')
29
+ .setDescription(
30
+ 'Documentação da API do backend (NestJS) para Autenticação e Blog.',
31
+ )
32
+ .setVersion('1.0')
33
+ .addSecurity('bearer', {
34
+ type: 'http',
35
+ scheme: 'bearer',
36
+ })
37
+ .addSecurityRequirements('bearer')
38
+ .build();
39
+
40
+ const document = SwaggerModule.createDocument(app, config);
41
+ SwaggerModule.setup('api/docs', app, document);
42
+ await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
43
+ }
44
+ void bootstrap();