full project: admin panel, backend modules, docs

This commit is contained in:
Lizandro Guarnizo
2026-06-03 22:11:01 -05:00
parent 1635723035
commit afc096d552
94 changed files with 15994 additions and 240 deletions
+47
View File
@@ -0,0 +1,47 @@
import { IsOptional, IsInt, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class PaginationDto {
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@ApiPropertyOptional({ default: 20 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 20;
}
export interface PaginatedResult<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
export function paginate<T>(
data: T[],
total: number,
page: number,
limit: number,
): PaginatedResult<T> {
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit) || 1,
},
};
}
+33
View File
@@ -0,0 +1,33 @@
import { Injectable, CanActivate, ExecutionContext, ForbiddenException, SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PrismaService } from '../prisma/prisma.service';
export const OWNERSHIP_KEY = 'ownership';
export const Ownership = (param: string, model: string, field: string) =>
SetMetadata(OWNERSHIP_KEY, { param, model, field });
@Injectable()
export class OwnershipGuard implements CanActivate {
constructor(
private reflector: Reflector,
private prisma: PrismaService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const metadata = this.reflector.get(OWNERSHIP_KEY, context.getHandler());
if (!metadata) return true;
const request = context.switchToHttp().getRequest();
const resourceId = request.params[metadata.param];
const userId = request.user.sub;
const record = await (this.prisma as any)[metadata.model].findUnique({
where: { id: resourceId },
});
if (!record || record[metadata.field] !== userId) {
throw new ForbiddenException('No tienes permiso para modificar este recurso');
}
return true;
}
}
+4
View File
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
+18
View File
@@ -0,0 +1,18 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user?.role === role);
}
}