Add modulos de usuario

This commit is contained in:
LeoMortari
2025-08-29 01:02:24 -03:00
parent eaa188fac0
commit 85aac808e9
18 changed files with 348 additions and 62 deletions

View File

@@ -1,2 +1,3 @@
DATABASE_URL="postgresql://username:password@ip_server:port/database?schema=public"
REDIS_URL="redis://username:password@ip_server:port"
REDIS_URL="redis://username:password@ip_server:port"
SESSION_TTL_SECONDS=3600

View File

@@ -1,4 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
}

View File

@@ -24,6 +24,7 @@
"@nestjs/core": "11.0.1",
"@nestjs/platform-express": "11.0.1",
"@prisma/client": "6.14.0",
"bcrypt": "6.0.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",
"dayjs": "1.11.13",
@@ -36,6 +37,7 @@
"@nestjs/cli": "11.0.0",
"@nestjs/schematics": "11.0.0",
"@nestjs/testing": "11.0.1",
"@types/bcrypt": "6.0.0",
"@types/express": "5.0.0",
"@types/jest": "30.0.0",
"@types/node": "22.10.7",

View File

@@ -29,6 +29,11 @@ enum status_usuario {
EXCLUIDO
}
enum eboolean {
V
F
}
model videos {
id Int @id @default(autoincrement())
uuid String @default(dbgenerated("gen_random_uuid()")) @db.Uuid
@@ -54,7 +59,7 @@ model Usuario {
id Int @id @default(autoincrement())
uuid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
email String @unique @db.VarChar(255)
email_verificado Boolean @default(false)
email_verificado eboolean @default(F)
password String @db.VarChar(255)
nome String? @db.VarChar(100)
sobrenome String? @db.VarChar(100)

View File

@@ -1,22 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -1,11 +1,22 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { VideosModule } from './videos/videos.module';
import { UsuariosModule } from './usuarios/usuarios.module';
import { RedisModule } from './redis/redis.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [PrismaModule, VideosModule],
imports: [
ConfigModule.forRoot({ isGlobal: true }),
PrismaModule,
RedisModule,
AuthModule,
VideosModule,
UsuariosModule,
],
controllers: [AppController],
providers: [AppService],
})

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

View File

@@ -0,0 +1,19 @@
import { IsString, MaxLength } from 'class-validator';
export class CreateUsuarioDto {
@IsString()
@MaxLength(244)
nome!: string;
@IsString()
@MaxLength(244)
sobrenome!: string;
@IsString()
@MaxLength(244)
email!: string;
@IsString()
@MaxLength(244)
password!: string;
}

View File

@@ -0,0 +1,53 @@
import dayjs from 'dayjs';
import { Expose, Transform, TransformFnParams } from 'class-transformer';
import { papel_usuario, status_usuario, eboolean } from 'generated/prisma';
export class UsuariosResponseDto {
@Expose()
id!: number;
@Expose({ name: 'nome' })
nome!: string | null;
@Expose()
sobrenome!: string | null;
@Expose()
uuid!: string | null;
@Expose()
email!: string | null;
@Expose({ name: 'email_verificado' })
email_verificado!: eboolean;
@Expose()
papel!: papel_usuario;
@Expose()
status!: status_usuario;
@Expose({ name: 'criado_em' })
@Transform(
({ value }: TransformFnParams) =>
value ? dayjs(value as Date | string).format('DD/MM/YYYY HH:mm:ss') : '',
{ toPlainOnly: true },
)
criado_em!: string;
@Expose({ name: 'atualizado_em' })
@Transform(
({ value }: TransformFnParams) =>
value ? dayjs(value as Date | string).format('DD/MM/YYYY HH:mm:ss') : '',
{ toPlainOnly: true },
)
atualizado_em!: string;
@Expose({ name: 'bloqueado_ate' })
@Transform(
({ value }: TransformFnParams) =>
value ? dayjs(value as Date | string).format('DD/MM/YYYY HH:mm:ss') : '',
{ toPlainOnly: true },
)
bloqueado_ate!: string;
}

View File

@@ -0,0 +1,47 @@
import {
Controller,
Get,
Param,
Post,
Body,
UsePipes,
ValidationPipe,
Patch,
} from '@nestjs/common';
import { UsuariosService } from './usuarios.service';
import { UsuariosResponseDto } from './dto/usuarios.response';
import { CreateUsuarioDto } from './dto/create-usuario-dto';
@Controller('usuarios')
export class UsuariosController {
constructor(private readonly usuariosService: UsuariosService) {}
@Get()
async list(): Promise<UsuariosResponseDto[]> {
return this.usuariosService.list();
}
@Get(':uuid')
async get(@Param('uuid') uuid: string): Promise<UsuariosResponseDto> {
return this.usuariosService.get(uuid);
}
@Post()
@UsePipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
async create(@Body() body: CreateUsuarioDto): Promise<UsuariosResponseDto> {
return this.usuariosService.create(body);
}
@Patch(':uuid/email-verificado')
async emailVerificado(
@Param('uuid') uuid: string,
): Promise<UsuariosResponseDto> {
return this.usuariosService.emailVerificado(uuid);
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UsuariosService } from './usuarios.service';
import { UsuariosController } from './usuarios.controller';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [UsuariosService],
controllers: [UsuariosController],
})
export class UsuariosModule {}

View File

@@ -0,0 +1,141 @@
import bcrypt from 'bcrypt';
import { Injectable, NotFoundException } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { Prisma, Usuario } from 'generated/prisma';
import { PrismaService } from '../prisma/prisma.service';
import { PaginatedResponse } from '../shared/dto/paginated';
import { UsuariosResponseDto } from './dto/usuarios.response';
import { CreateUsuarioDto } from './dto/create-usuario-dto';
const SELECT = {
id: true,
uuid: true,
email: true,
email_verificado: true,
nome: true,
sobrenome: true,
papel: true,
status: true,
ultimo_login_em: true,
ultimo_login_ip: true,
tentativas_login_falhadas: true,
bloqueado_ate: true,
criado_em: true,
atualizado_em: true,
};
@Injectable()
export class UsuariosService {
constructor(private readonly prisma: PrismaService) {}
async list(): Promise<UsuariosResponseDto[]> {
const data = await this.prisma.usuario.findMany({
where: { email: { not: 'admin@clipperia.com' } },
orderBy: { id: 'desc' },
select: SELECT,
});
return plainToInstance(UsuariosResponseDto, data, {
excludeExtraneousValues: true,
});
}
async listPaginated(
page: number,
perPage: number,
direction: 'asc' | 'desc' = 'desc',
): Promise<PaginatedResponse<UsuariosResponseDto>> {
const skip = page >= 1 ? page * perPage : 0;
const [rows, total] = await Promise.all([
this.prisma.usuario.findMany({
orderBy: { id: direction },
skip,
take: perPage ?? 1,
select: SELECT,
}),
this.prisma.usuario.count(),
]);
const content: UsuariosResponseDto[] = plainToInstance(
UsuariosResponseDto,
rows,
{ excludeExtraneousValues: true },
);
const totalPages = Math.max(1, Math.ceil(total / perPage));
return {
content,
pagination: {
page,
direction,
perPage,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
};
}
async get(uuid: string): Promise<UsuariosResponseDto> {
const row = await this.prisma.usuario.findUnique({
where: { uuid },
select: SELECT,
});
if (!row) {
throw new NotFoundException('Usuário não encontrado');
}
return plainToInstance(UsuariosResponseDto, row, {
excludeExtraneousValues: true,
});
}
async update(id: number, data: Prisma.UsuarioUpdateInput): Promise<Usuario> {
return this.prisma.usuario.update({
where: { id },
data,
});
}
async create(dto: CreateUsuarioDto): Promise<UsuariosResponseDto> {
const { email, password, nome, sobrenome } = dto;
const senhaCriptografada = await bcrypt.hash(password, 10);
const usuario = await this.prisma.usuario.create({
data: {
email,
password: senhaCriptografada,
nome,
sobrenome,
},
});
return plainToInstance(UsuariosResponseDto, usuario, {
excludeExtraneousValues: true,
});
}
async delete(id: number): Promise<Usuario> {
return this.prisma.usuario.delete({
where: { id },
});
}
async emailVerificado(uuid: string): Promise<UsuariosResponseDto> {
const usuario = await this.prisma.usuario.update({
where: { uuid },
data: { email_verificado: 'V' },
});
return plainToInstance(UsuariosResponseDto, usuario, {
excludeExtraneousValues: true,
});
}
}

View File

@@ -11,8 +11,8 @@ import { videos, Prisma, video_situation } from 'generated/prisma';
import { VideosService } from './videos.service';
import { VideoResponseDto } from './dto/video-response.dto';
import { PaginatedQueryDto, PaginatedResponse } from './dto/paginated.dto';
import { EBooleanPipe } from './videos.pipe';
import { PaginatedQueryDto, PaginatedResponse } from '../shared/dto/paginated';
import { EBooleanPipe } from '../shared/pipe';
@Controller('videos')
export class VideosController {

View File

@@ -5,7 +5,7 @@ import { Prisma, videos, video_situation } from 'generated/prisma';
import { PrismaService } from '../prisma/prisma.service';
import { VideoResponseDto } from './dto/video-response.dto';
import { PaginatedResponse } from './dto/paginated.dto';
import { PaginatedResponse } from '../shared/dto/paginated';
@Injectable()
export class VideosService {

View File

@@ -1,25 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () =>
request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'));
});