Adiciona controller do Ollama e reestrutura arquitetura
This commit is contained in:
18
src/modules/auth/auth.controller.spec.ts
Normal file
18
src/modules/auth/auth.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthController } from './auth.controller';
|
||||
|
||||
describe('AuthController', () => {
|
||||
let controller: AuthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AuthController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AuthController>(AuthController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
29
src/modules/auth/auth.controller.ts
Normal file
29
src/modules/auth/auth.controller.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
|
||||
import LoginResponseDto from './dto/loginResponse.dto';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto): Promise<LoginResponseDto> {
|
||||
return this.authService.login(loginDto);
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async logout(@Body() body: { refreshToken: string }): Promise<void> {
|
||||
return this.authService.logout(body.refreshToken);
|
||||
}
|
||||
|
||||
@Post('refresh-token')
|
||||
async refreshToken(
|
||||
@Body() body: { refreshToken: string },
|
||||
): Promise<LoginResponseDto> {
|
||||
return this.authService.refreshToken(body.refreshToken);
|
||||
}
|
||||
}
|
||||
11
src/modules/auth/auth.module.ts
Normal file
11
src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { KeycloakJwtStrategy } from './keycloak.strategy';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, KeycloakJwtStrategy],
|
||||
exports: [AuthService, KeycloakJwtStrategy],
|
||||
})
|
||||
export class AuthModule {}
|
||||
176
src/modules/auth/auth.service.spec.ts
Normal file
176
src/modules/auth/auth.service.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getUrl', () => {
|
||||
it('should return development URL when NODE_ENV is development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
expect(service['getUrl']()).toBe('https://auth.clipperia.com.br');
|
||||
});
|
||||
|
||||
it('should return production URL when NODE_ENV is not development', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
expect(service['getUrl']()).toBe('http://keycloak:8080');
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
const loginDto: LoginDto = {
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
};
|
||||
|
||||
const mockTokenResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
refresh_token: 'mock-refresh-token',
|
||||
expires_in: 300,
|
||||
refresh_expires_in: 1800,
|
||||
};
|
||||
|
||||
it('should login successfully', async () => {
|
||||
const mockPost = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ data: mockTokenResponse });
|
||||
mockedAxios.create.mockReturnValueOnce({
|
||||
post: mockPost,
|
||||
} as unknown as AxiosInstance);
|
||||
|
||||
const result = await service.login(loginDto);
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'/token',
|
||||
expect.any(URLSearchParams),
|
||||
);
|
||||
expect(result).toEqual(mockTokenResponse);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException when login fails', async () => {
|
||||
const mockError = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 401,
|
||||
data: { error: 'invalid_grant' },
|
||||
},
|
||||
};
|
||||
|
||||
const mockPost = jest.fn().mockRejectedValueOnce(mockError);
|
||||
|
||||
mockedAxios.create.mockReturnValueOnce({
|
||||
post: mockPost,
|
||||
} as unknown as AxiosInstance);
|
||||
|
||||
await expect(service.login(loginDto)).rejects.toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
const refreshToken = 'mock-refresh-token';
|
||||
|
||||
it('should logout successfully', async () => {
|
||||
const mockPost = jest.fn().mockResolvedValueOnce({});
|
||||
|
||||
mockedAxios.create.mockReturnValueOnce({
|
||||
post: mockPost,
|
||||
} as unknown as AxiosInstance);
|
||||
|
||||
await service.logout(refreshToken);
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'/logout',
|
||||
expect.any(URLSearchParams),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException when logout fails', async () => {
|
||||
const mockError = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 400,
|
||||
data: { error: 'invalid_token' },
|
||||
},
|
||||
};
|
||||
|
||||
const mockPost = jest.fn().mockRejectedValueOnce(mockError);
|
||||
|
||||
mockedAxios.create.mockReturnValueOnce({
|
||||
post: mockPost,
|
||||
} as unknown as AxiosInstance);
|
||||
|
||||
await expect(service.logout(refreshToken)).rejects.toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
const refreshToken = 'mock-refresh-token';
|
||||
const mockTokenResponse = {
|
||||
access_token: 'new-access-token',
|
||||
refresh_token: 'new-refresh-token',
|
||||
expires_in: 300,
|
||||
refresh_expires_in: 1800,
|
||||
};
|
||||
|
||||
it('should refresh token successfully', async () => {
|
||||
const mockPost = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ data: mockTokenResponse });
|
||||
|
||||
mockedAxios.create.mockReturnValueOnce({
|
||||
post: mockPost,
|
||||
} as unknown as AxiosInstance);
|
||||
|
||||
const result = await service.refreshToken(refreshToken);
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'/token',
|
||||
expect.any(URLSearchParams),
|
||||
);
|
||||
expect(result).toEqual(mockTokenResponse);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException when refresh token is invalid', async () => {
|
||||
const mockError = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 400,
|
||||
data: { error: 'invalid_grant' },
|
||||
},
|
||||
};
|
||||
|
||||
const mockPost = jest.fn().mockRejectedValueOnce(mockError);
|
||||
|
||||
mockedAxios.create.mockReturnValueOnce({
|
||||
post: mockPost,
|
||||
} as unknown as AxiosInstance);
|
||||
|
||||
await expect(service.refreshToken(refreshToken)).rejects.toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
93
src/modules/auth/auth.service.ts
Normal file
93
src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import axios, { isAxiosError } from 'axios';
|
||||
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
|
||||
import LoginResponseDto from './dto/loginResponse.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly keycloakUrl =
|
||||
this.getUrl() + '/realms/clipperia/protocol/openid-connect';
|
||||
|
||||
private readonly clientId = 'account';
|
||||
|
||||
private readonly keycloakApi = axios.create({
|
||||
baseURL: this.keycloakUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
getUrl() {
|
||||
return process.env.KEYCLOAK_URL ?? 'https://auth.clipperia.com.br';
|
||||
}
|
||||
|
||||
async login(loginDto: LoginDto) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.clientId,
|
||||
grant_type: 'password',
|
||||
username: loginDto.username,
|
||||
password: loginDto.password,
|
||||
});
|
||||
|
||||
const { data } = await this.keycloakApi.post<LoginResponseDto>(
|
||||
'/token',
|
||||
params,
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error)) {
|
||||
console.error(error.response?.data);
|
||||
throw new UnauthorizedException(error.response?.data);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException('Usuário ou senha inválidos');
|
||||
}
|
||||
}
|
||||
|
||||
async logout(refreshToken: string): Promise<void> {
|
||||
try {
|
||||
const data = new URLSearchParams({
|
||||
client_id: this.clientId,
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
await this.keycloakApi.post('/logout', data);
|
||||
} catch (error) {
|
||||
if (isAxiosError(error)) {
|
||||
console.error(error.response?.data);
|
||||
throw new UnauthorizedException(error.response?.data);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException('Erro ao deslogar usuário');
|
||||
}
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.clientId,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
const { data } = await this.keycloakApi.post<LoginResponseDto>(
|
||||
'/token',
|
||||
params,
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error)) {
|
||||
console.error(error.response?.data);
|
||||
throw new UnauthorizedException(error.response?.data);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException('Erro ao renovar token');
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/modules/auth/decorator/roles.decorator.ts
Normal file
4
src/modules/auth/decorator/roles.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
11
src/modules/auth/dto/login.dto.ts
Normal file
11
src/modules/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
username: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
8
src/modules/auth/dto/loginResponse.dto.ts
Normal file
8
src/modules/auth/dto/loginResponse.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default class LoginResponseDto {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_expires_in: number;
|
||||
scope: string;
|
||||
}
|
||||
26
src/modules/auth/keycloak-auth.guard.ts
Normal file
26
src/modules/auth/keycloak-auth.guard.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
import type { JwtPayload } from './keycloak.strategy';
|
||||
|
||||
@Injectable()
|
||||
export class KeycloakAuthGuard extends AuthGuard('jwt') {
|
||||
handleRequest<TUser = JwtPayload>(
|
||||
err: unknown,
|
||||
user: JwtPayload | false,
|
||||
info: unknown,
|
||||
context: import('@nestjs/common').ExecutionContext,
|
||||
): TUser {
|
||||
if (err || !user) {
|
||||
if (err instanceof UnauthorizedException) {
|
||||
throw err;
|
||||
}
|
||||
throw new UnauthorizedException('Usuário não autenticado');
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<{ user?: JwtPayload }>();
|
||||
request.user = user;
|
||||
|
||||
return user as TUser;
|
||||
}
|
||||
}
|
||||
71
src/modules/auth/keycloak.strategy.ts
Normal file
71
src/modules/auth/keycloak.strategy.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Injectable, UnauthorizedException, Logger } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
import * as jwksRsa from 'jwks-rsa';
|
||||
|
||||
export type JwtAudience = string | string[] | undefined;
|
||||
export interface JwtRealmAccess {
|
||||
roles: string[];
|
||||
}
|
||||
export interface JwtResourceAccessEntry {
|
||||
roles: string[];
|
||||
}
|
||||
export type JwtResourceAccess =
|
||||
| Record<string, JwtResourceAccessEntry>
|
||||
| undefined;
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
email?: string;
|
||||
preferred_username?: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
scope?: string;
|
||||
realm_access?: JwtRealmAccess;
|
||||
resource_access?: JwtResourceAccess;
|
||||
iat: number;
|
||||
exp: number;
|
||||
iss: string;
|
||||
aud?: JwtAudience;
|
||||
[claim: string]: unknown;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class KeycloakJwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
private readonly logger = new Logger(KeycloakJwtStrategy.name);
|
||||
|
||||
constructor() {
|
||||
const baseUrl = process.env.KEYCLOAK_URL ?? 'https://auth.clipperia.com.br';
|
||||
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKeyProvider: jwksRsa.passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
jwksUri: `${baseUrl}/realms/clipperia/protocol/openid-connect/certs`,
|
||||
}),
|
||||
algorithms: ['RS256'],
|
||||
issuer: 'https://auth.clipperia.com.br/realms/clipperia',
|
||||
ignoreExpiration: false,
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: JwtPayload) {
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (payload.exp < now) {
|
||||
throw new UnauthorizedException('Token expirado');
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.stack : String(error);
|
||||
|
||||
this.logger.error('JWT Validation Error:', errorMessage);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/modules/auth/roles.guard.ts
Normal file
64
src/modules/auth/roles.guard.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from './decorator/roles.decorator';
|
||||
|
||||
import type { JwtPayload } from './keycloak.strategy';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
private extractRoles(user: JwtPayload): string[] {
|
||||
const roles: string[] = [];
|
||||
|
||||
if (user.realm_access?.roles) {
|
||||
roles.push(...user.realm_access.roles);
|
||||
}
|
||||
|
||||
if (user.resource_access) {
|
||||
Object.entries(user.resource_access).forEach(([, resource]) => {
|
||||
if (resource?.roles) {
|
||||
roles.push(...resource.roles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const uniqueRoles = [...new Set(roles)];
|
||||
return uniqueRoles;
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
|
||||
ROLES_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
if (!requiredRoles || !requiredRoles.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<{ user: JwtPayload }>();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException('Usuário não autenticado');
|
||||
}
|
||||
|
||||
const userRoles = this.extractRoles(user);
|
||||
|
||||
const hasRole = requiredRoles.some((role) => userRoles.includes(role));
|
||||
|
||||
if (!hasRole) {
|
||||
throw new ForbiddenException(
|
||||
'Você não possui permissão para acessar este recurso',
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
10
src/modules/ollama/dto/chat.dto.ts
Normal file
10
src/modules/ollama/dto/chat.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ChatDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
message: string;
|
||||
|
||||
@IsString()
|
||||
model: string;
|
||||
}
|
||||
18
src/modules/ollama/ollama.controller.spec.ts
Normal file
18
src/modules/ollama/ollama.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OllamaController } from './ollama.controller';
|
||||
|
||||
describe('OllamaController', () => {
|
||||
let controller: OllamaController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [OllamaController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<OllamaController>(OllamaController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
44
src/modules/ollama/ollama.controller.ts
Normal file
44
src/modules/ollama/ollama.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||
import { OllamaService } from './ollama.service';
|
||||
import { ChatDto } from './dto/chat.dto';
|
||||
|
||||
@Controller('ollama')
|
||||
export class OllamaController {
|
||||
constructor(private readonly ollamaService: OllamaService) {}
|
||||
|
||||
@Get('models')
|
||||
public async getModels(
|
||||
@Query('onlyChats') { onlyChats }: { onlyChats: boolean },
|
||||
) {
|
||||
return await this.ollamaService.getModels({ onlyChats });
|
||||
}
|
||||
|
||||
@Post('chat')
|
||||
public async chat(@Body() body: ChatDto) {
|
||||
const { message, model } = body;
|
||||
|
||||
if (!message) {
|
||||
throw new Error('O atributo message é obrigatório');
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
const [defaultModel] = await this.ollamaService.getModels({
|
||||
onlyChats: true,
|
||||
});
|
||||
|
||||
if (!defaultModel) {
|
||||
throw new Error('Nenhum modelo encontrado');
|
||||
}
|
||||
|
||||
return await this.ollamaService.generateChat({
|
||||
model: defaultModel.name,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
return await this.ollamaService.generateChat({
|
||||
model,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
9
src/modules/ollama/ollama.module.ts
Normal file
9
src/modules/ollama/ollama.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OllamaController } from './ollama.controller';
|
||||
import { OllamaService } from './ollama.service';
|
||||
|
||||
@Module({
|
||||
controllers: [OllamaController],
|
||||
providers: [OllamaService],
|
||||
})
|
||||
export class OllamaModule {}
|
||||
18
src/modules/ollama/ollama.service.spec.ts
Normal file
18
src/modules/ollama/ollama.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OllamaService } from './ollama.service';
|
||||
|
||||
describe('OllamaService', () => {
|
||||
let service: OllamaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [OllamaService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<OllamaService>(OllamaService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
55
src/modules/ollama/ollama.service.ts
Normal file
55
src/modules/ollama/ollama.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Ollama } from 'ollama';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { ListResponse, ModelResponse } from 'ollama';
|
||||
|
||||
@Injectable()
|
||||
export class OllamaService {
|
||||
private readonly ollama: Ollama;
|
||||
|
||||
constructor() {
|
||||
this.ollama = new Ollama({ host: 'http://154.12.229.181:11434' });
|
||||
}
|
||||
|
||||
public async getModels({ onlyChats }: { onlyChats: boolean }) {
|
||||
try {
|
||||
const modelsData = await this.ollama.list();
|
||||
|
||||
if (onlyChats) {
|
||||
return this.getChatModels(modelsData);
|
||||
}
|
||||
|
||||
return modelsData.models;
|
||||
} catch (error) {
|
||||
throw new Error(error as string);
|
||||
}
|
||||
}
|
||||
|
||||
public getChatModels(modelsData: ListResponse): ModelResponse[] {
|
||||
const excludeFamilies = ['nomic-bert', 'embed', 'embedding'];
|
||||
|
||||
return modelsData.models.filter((model) => {
|
||||
const families = model.details?.families || [];
|
||||
|
||||
return !families.some((f) => excludeFamilies.includes(f.toLowerCase()));
|
||||
});
|
||||
}
|
||||
|
||||
public async generateChat({
|
||||
model,
|
||||
message,
|
||||
}: {
|
||||
model: string;
|
||||
message: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await this.ollama.chat({
|
||||
model,
|
||||
messages: [{ role: 'user', content: message }],
|
||||
});
|
||||
|
||||
return response.message.content;
|
||||
} catch (error) {
|
||||
throw new Error(error as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/modules/usuarios/dto/create-usuario-dto.ts
Normal file
19
src/modules/usuarios/dto/create-usuario-dto.ts
Normal 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;
|
||||
}
|
||||
53
src/modules/usuarios/dto/usuarios.response.ts
Normal file
53
src/modules/usuarios/dto/usuarios.response.ts
Normal 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;
|
||||
}
|
||||
47
src/modules/usuarios/usuarios.controller.ts
Normal file
47
src/modules/usuarios/usuarios.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
11
src/modules/usuarios/usuarios.module.ts
Normal file
11
src/modules/usuarios/usuarios.module.ts
Normal 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 {}
|
||||
141
src/modules/usuarios/usuarios.service.ts
Normal file
141
src/modules/usuarios/usuarios.service.ts
Normal 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,
|
||||
} as const;
|
||||
|
||||
@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,
|
||||
});
|
||||
}
|
||||
}
|
||||
59
src/modules/videos/dto/create-video-dto.ts
Normal file
59
src/modules/videos/dto/create-video-dto.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsISO8601,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { video_situation } from '@root/generated/prisma';
|
||||
|
||||
export class CreateVideoDto {
|
||||
@IsUrl()
|
||||
@MaxLength(244)
|
||||
url!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(video_situation)
|
||||
situation?: video_situation;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(244)
|
||||
error_message?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
clips_quantity?: number;
|
||||
|
||||
@IsOptional()
|
||||
times?: unknown;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(244)
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(244)
|
||||
filename?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(244)
|
||||
videoid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
datetime_download?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
datetime_convert?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
datetime_posted?: string;
|
||||
}
|
||||
40
src/modules/videos/dto/list-videos-query.dto.ts
Normal file
40
src/modules/videos/dto/list-videos-query.dto.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
} from 'class-validator';
|
||||
import { video_situation } from 'generated/prisma';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class ListVideosQueryDto {
|
||||
@IsEnum(video_situation)
|
||||
@IsOptional()
|
||||
@Transform(
|
||||
({ value }: { value: string }) => value?.toUpperCase() as video_situation,
|
||||
)
|
||||
situation?: video_situation;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
title?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value ? Number(value) : 1))
|
||||
page?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value ? Number(value) : 10))
|
||||
perPage?: number = 10;
|
||||
|
||||
@IsOptional()
|
||||
direction: 'asc' | 'desc' = 'desc';
|
||||
|
||||
@Transform(({ value }) => value !== 'false')
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
pageable: boolean = true;
|
||||
}
|
||||
41
src/modules/videos/dto/video-response.dto.ts
Normal file
41
src/modules/videos/dto/video-response.dto.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Expose, Transform, TransformFnParams } from 'class-transformer';
|
||||
import dayjs from 'dayjs';
|
||||
import { video_situation } from 'generated/prisma';
|
||||
|
||||
export class VideoResponseDto {
|
||||
@Expose()
|
||||
id!: number;
|
||||
|
||||
@Expose()
|
||||
title!: string | null;
|
||||
|
||||
@Expose()
|
||||
url!: string;
|
||||
|
||||
@Expose()
|
||||
situation!: video_situation;
|
||||
|
||||
@Expose()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
typeof value === 'number' ? value : 0,
|
||||
)
|
||||
clips_quantity!: number;
|
||||
|
||||
@Expose()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
typeof value === 'string' ? value : '',
|
||||
)
|
||||
videoid!: string;
|
||||
|
||||
@Expose()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
typeof value === 'string' ? value : '',
|
||||
)
|
||||
filename!: string;
|
||||
|
||||
@Expose()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
value ? dayjs(value as Date | string).format('DD/MM/YYYY HH:mm:ss') : '',
|
||||
)
|
||||
datetime_download!: string;
|
||||
}
|
||||
83
src/modules/videos/videos.controller.ts
Normal file
83
src/modules/videos/videos.controller.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
Patch,
|
||||
Body,
|
||||
Delete,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma, videos, video_situation } from 'generated/prisma';
|
||||
|
||||
import { VideosService } from './videos.service';
|
||||
import { VideoResponseDto } from './dto/video-response.dto';
|
||||
import { KeycloakAuthGuard } from '../auth/keycloak-auth.guard';
|
||||
import { Roles } from '../auth/decorator/roles.decorator';
|
||||
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
|
||||
import { VideoMetadataDto } from 'src/shared/dto/video-metadata';
|
||||
|
||||
type PaginatedResponse<T> = {
|
||||
content: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@Controller('videos')
|
||||
@UseGuards(KeycloakAuthGuard)
|
||||
export class VideosController {
|
||||
constructor(private readonly videosService: VideosService) {}
|
||||
|
||||
@Get('situacoes')
|
||||
@Roles('user', 'admin')
|
||||
getSituacao(): video_situation[] {
|
||||
return Object.values(video_situation) as video_situation[];
|
||||
}
|
||||
|
||||
@Get('search')
|
||||
@Roles('user', 'admin')
|
||||
getVideoMetadata(
|
||||
@Query() { url }: { url: string },
|
||||
): Promise<VideoMetadataDto> {
|
||||
return this.videosService.getVideoMetadata(url);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles('user', 'admin')
|
||||
async list(
|
||||
@Query() query: ListVideosQueryDto,
|
||||
): Promise<PaginatedResponse<VideoResponseDto> | VideoResponseDto[]> {
|
||||
if (query.pageable || query.page || query.perPage) {
|
||||
return this.videosService.listPaginated(query);
|
||||
}
|
||||
|
||||
return this.videosService.list({
|
||||
situation: query.situation,
|
||||
title: query.title,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async get(@Param('id') id: string): Promise<videos | null> {
|
||||
return this.videosService.get(Number(id));
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() body: Prisma.videosUpdateInput,
|
||||
): Promise<videos> {
|
||||
return this.videosService.update(Number(id), body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(@Param('id') id: string): Promise<videos> {
|
||||
return this.videosService.delete(Number(id));
|
||||
}
|
||||
}
|
||||
12
src/modules/videos/videos.module.ts
Normal file
12
src/modules/videos/videos.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { VideosService } from './videos.service';
|
||||
import { VideosController } from './videos.controller';
|
||||
import { PrismaModule } from '@prisma/prisma.module';
|
||||
import { KeycloakAuthGuard } from '@modules/auth/keycloak-auth.guard';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [VideosService, KeycloakAuthGuard],
|
||||
controllers: [VideosController],
|
||||
})
|
||||
export class VideosModule {}
|
||||
148
src/modules/videos/videos.service.ts
Normal file
148
src/modules/videos/videos.service.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import axios from 'axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
|
||||
import { Prisma, videos, video_situation } from 'generated/prisma';
|
||||
|
||||
import { PrismaService } from '@prisma/prisma.service';
|
||||
import { VideoResponseDto } from './dto/video-response.dto';
|
||||
import { PaginatedResponse } from '@shared/dto/paginated';
|
||||
import { ListVideosQueryDto } from './dto/list-videos-query.dto';
|
||||
import { VideoMetadataDto } from '@shared/dto/video-metadata';
|
||||
|
||||
@Injectable()
|
||||
export class VideosService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list({
|
||||
situation,
|
||||
title,
|
||||
}: {
|
||||
situation?: video_situation;
|
||||
title?: string;
|
||||
}): Promise<VideoResponseDto[]> {
|
||||
const where: Prisma.videosWhereInput = situation ? { situation } : {};
|
||||
|
||||
if (title) {
|
||||
where.title = {
|
||||
contains: title,
|
||||
};
|
||||
}
|
||||
const data = await this.prisma.videos.findMany({
|
||||
where,
|
||||
orderBy: { id: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
url: true,
|
||||
situation: true,
|
||||
clips_quantity: true,
|
||||
videoid: true,
|
||||
filename: true,
|
||||
datetime_download: true,
|
||||
},
|
||||
});
|
||||
|
||||
return plainToInstance(VideoResponseDto, data, {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
}
|
||||
|
||||
async listPaginated(
|
||||
query: ListVideosQueryDto,
|
||||
): Promise<PaginatedResponse<VideoResponseDto>> {
|
||||
const page = Number(query.page ?? 1);
|
||||
const perPage = Number(query.perPage ?? 1);
|
||||
const direction = query.direction ?? 'desc';
|
||||
|
||||
const skip = page > 0 ? (page - 1) * perPage : 0;
|
||||
const where: Prisma.videosWhereInput = query.situation
|
||||
? { situation: query.situation }
|
||||
: {};
|
||||
|
||||
if (query.title) {
|
||||
where.title = {
|
||||
contains: query.title,
|
||||
};
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.videos.findMany({
|
||||
where,
|
||||
orderBy: { id: direction },
|
||||
skip,
|
||||
take: perPage,
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
url: true,
|
||||
situation: true,
|
||||
clips_quantity: true,
|
||||
videoid: true,
|
||||
filename: true,
|
||||
datetime_download: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.videos.count({ where }),
|
||||
]);
|
||||
|
||||
const content: VideoResponseDto[] = plainToInstance(
|
||||
VideoResponseDto,
|
||||
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(id: number): Promise<videos | null> {
|
||||
return this.prisma.videos.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async getVideoMetadata(url: string): Promise<VideoMetadataDto> {
|
||||
try {
|
||||
const { data } = await axios.get<VideoMetadataDto>(
|
||||
`${process.env.YOUTUBE_API_URL}/get-video-metadata`,
|
||||
{
|
||||
params: {
|
||||
url,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return plainToInstance(VideoMetadataDto, data, {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
} catch {
|
||||
throw new Error('Erro ao obter metadados do vídeo');
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: Prisma.videosUpdateInput): Promise<videos> {
|
||||
return this.prisma.videos.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: number): Promise<videos> {
|
||||
return this.prisma.videos.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user