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.
- package/LICENSE +21 -0
- package/README.md +148 -86
- package/index.js +228 -29
- package/package.json +12 -3
- package/templates/nest/.env.example +41 -0
- package/templates/nest/.prettierrc +4 -0
- package/templates/nest/Dockerfile +17 -0
- package/templates/nest/README.md +224 -0
- package/templates/nest/addons/.github/dependabot.yml +11 -0
- package/templates/nest/addons/.github/labeler.yml +34 -0
- package/templates/nest/addons/.github/workflows/check-branch.yml +24 -0
- package/templates/nest/addons/.github/workflows/ci.yml +29 -0
- package/templates/nest/addons/.github/workflows/draft-release.yml +22 -0
- package/templates/nest/addons/.github/workflows/pr-labeler.yml +16 -0
- package/templates/nest/addons/.github/workflows/release.yml +33 -0
- package/templates/nest/addons/.github/workflows/security-audit.yml +17 -0
- package/templates/nest/addons/.github/workflows/semantic-pr.yml +31 -0
- package/templates/nest/addons/.github/workflows/stale.yml +24 -0
- package/templates/nest/addons/.husky/commit-msg +17 -0
- package/templates/nest/addons/.husky/pre-commit +1 -0
- package/templates/nest/addons/addons.json +17 -0
- package/templates/nest/addons/cloudinary/cloudinary.module.ts +11 -0
- package/templates/nest/addons/cloudinary/cloudinary.provider.ts +16 -0
- package/templates/nest/addons/cloudinary/cloudinary.service.ts +54 -0
- package/templates/nest/docker-compose.yml +58 -0
- package/templates/nest/eslint.config.mjs +34 -0
- package/templates/nest/jest.config.js +17 -0
- package/templates/nest/nest-cli.json +12 -0
- package/templates/nest/package.json +99 -0
- package/templates/nest/src/app.controller.ts +7 -0
- package/templates/nest/src/app.module.ts +69 -0
- package/templates/nest/src/app.service.ts +4 -0
- package/templates/nest/src/auth/auth.controller.ts +97 -0
- package/templates/nest/src/auth/auth.module.ts +46 -0
- package/templates/nest/src/auth/auth.service.ts +231 -0
- package/templates/nest/src/auth/decorators/roles.decorator.ts +5 -0
- package/templates/nest/src/auth/dto/change-password.dto.ts +21 -0
- package/templates/nest/src/auth/dto/login-response.dto.ts +25 -0
- package/templates/nest/src/auth/dto/login.dto.ts +15 -0
- package/templates/nest/src/auth/dto/refresh-token.dto.ts +12 -0
- package/templates/nest/src/auth/entities/refresh-token.entity.ts +18 -0
- package/templates/nest/src/auth/enums/role.enum.ts +4 -0
- package/templates/nest/src/auth/guards/jwt-auth.guard.ts +5 -0
- package/templates/nest/src/auth/guards/refresh-token.guard.ts +5 -0
- package/templates/nest/src/auth/guards/roles.guard.ts +23 -0
- package/templates/nest/src/auth/interfaces/jwt-payload.interface.ts +10 -0
- package/templates/nest/src/auth/strategies/jwt.strategy.ts +28 -0
- package/templates/nest/src/auth/strategies/local.strategy.ts +23 -0
- package/templates/nest/src/auth/strategies/refresh-token.strategy.ts +32 -0
- package/templates/nest/src/common/base.entity.ts +19 -0
- package/templates/nest/src/common/base.repository.ts +79 -0
- package/templates/nest/src/common/base.service.ts +28 -0
- package/templates/nest/src/common/constants/errors.constants.ts +33 -0
- package/templates/nest/src/common/decorators/user.decorator.ts +9 -0
- package/templates/nest/src/common/dto/base-query.dto.ts +56 -0
- package/templates/nest/src/common/irepository.ts +18 -0
- package/templates/nest/src/common/utils/duration.utils.ts +33 -0
- package/templates/nest/src/common/utils/pagination.utils.ts +35 -0
- package/templates/nest/src/common/utils/slug.utils.ts +14 -0
- package/templates/nest/src/common/utils/transform.utils.ts +62 -0
- package/templates/nest/src/common/validators/is-date-after.validator.ts +40 -0
- package/templates/nest/src/data-source.ts +23 -0
- package/templates/nest/src/main.ts +44 -0
- package/templates/nest/src/user/dto/create-user.dto.ts +50 -0
- package/templates/nest/src/user/dto/query-users.dto.ts +23 -0
- package/templates/nest/src/user/dto/update-user.dto.ts +15 -0
- package/templates/nest/src/user/entities/user.entity.ts +66 -0
- package/templates/nest/src/user/user.controller.ts +172 -0
- package/templates/nest/src/user/user.module.ts +15 -0
- package/templates/nest/src/user/user.repository.ts +61 -0
- package/templates/nest/src/user/user.service.ts +138 -0
- package/templates/nest/template.json +5 -0
- package/templates/nest/test/jest-e2e.json +12 -0
- package/templates/nest/tsconfig.build.json +4 -0
- package/templates/nest/tsconfig.json +25 -0
- package/templates/python/.env.example +37 -0
- package/templates/python/Dockerfile +18 -0
- package/templates/python/README.md +219 -0
- package/templates/python/addons/.github/dependabot.yml +11 -0
- package/templates/python/addons/.github/labeler.yml +29 -0
- package/templates/python/addons/.github/workflows/check-branch.yml +24 -0
- package/templates/python/addons/.github/workflows/ci.yml +30 -0
- package/templates/python/addons/.github/workflows/draft-release.yml +22 -0
- package/templates/python/addons/.github/workflows/pr-labeler.yml +16 -0
- package/templates/python/addons/.github/workflows/release.yml +30 -0
- package/templates/python/addons/.github/workflows/security-audit.yml +21 -0
- package/templates/python/addons/.github/workflows/semantic-pr.yml +31 -0
- package/templates/python/addons/.github/workflows/stale.yml +24 -0
- package/templates/python/addons/addons.json +17 -0
- package/templates/python/addons/cloudinary/service.py +67 -0
- package/templates/python/addons/pre-commit/.pre-commit-config.yaml +31 -0
- package/templates/python/alembic/env.py +56 -0
- package/templates/python/alembic/script.py.mako +26 -0
- package/templates/python/alembic/versions/.gitkeep +0 -0
- package/templates/python/alembic.ini +39 -0
- package/templates/python/app/__init__.py +0 -0
- package/templates/python/app/auth/__init__.py +5 -0
- package/templates/python/app/auth/dependencies.py +118 -0
- package/templates/python/app/auth/enums.py +6 -0
- package/templates/python/app/auth/models.py +18 -0
- package/templates/python/app/auth/router.py +68 -0
- package/templates/python/app/auth/schemas.py +58 -0
- package/templates/python/app/auth/service.py +180 -0
- package/templates/python/app/common/__init__.py +18 -0
- package/templates/python/app/common/base_model.py +26 -0
- package/templates/python/app/common/base_repository.py +83 -0
- package/templates/python/app/common/errors.py +35 -0
- package/templates/python/app/common/pagination.py +22 -0
- package/templates/python/app/common/schemas.py +20 -0
- package/templates/python/app/common/utils.py +15 -0
- package/templates/python/app/core/__init__.py +4 -0
- package/templates/python/app/core/config.py +55 -0
- package/templates/python/app/core/database.py +20 -0
- package/templates/python/app/main.py +33 -0
- package/templates/python/app/user/__init__.py +4 -0
- package/templates/python/app/user/models.py +26 -0
- package/templates/python/app/user/repository.py +84 -0
- package/templates/python/app/user/router.py +170 -0
- package/templates/python/app/user/schemas.py +60 -0
- package/templates/python/app/user/service.py +114 -0
- package/templates/python/docker-compose.yml +55 -0
- package/templates/python/pyproject.toml +46 -0
- package/templates/python/requirements-dev.txt +7 -0
- package/templates/python/requirements.txt +20 -0
- package/templates/python/template.json +5 -0
- package/utils/template.js +165 -0
- 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,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,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();
|