add moodules

This commit is contained in:
2026-04-17 23:42:24 +01:00
parent 532458ecfa
commit a7fbb2c466
54 changed files with 3074 additions and 74 deletions
@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '@prisma/client';
export const CurrentDbUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): User => {
const req = ctx.switchToHttp().getRequest();
return req.dbUser as User;
},
);
@@ -0,0 +1,18 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export type KeycloakRequestUser = {
userId: string;
email?: string;
name?: string;
email_verified?: boolean;
picture?: string;
roles: string[];
raw?: unknown;
};
export const CurrentKeycloakUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): KeycloakRequestUser => {
const req = ctx.switchToHttp().getRequest();
return req.user as KeycloakRequestUser;
},
);
+6
View File
@@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '@prisma/client';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
+17
View File
@@ -0,0 +1,17 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, Max, Min } from 'class-validator';
export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 20;
}
+30
View File
@@ -0,0 +1,30 @@
import {
CanActivate,
ExecutionContext,
Injectable,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '@prisma/client';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!required?.length) {
return true;
}
const req = context.switchToHttp().getRequest();
const role = req.dbUser?.role as UserRole | undefined;
if (!role || !required.includes(role)) {
throw new ForbiddenException('Insufficient role');
}
return true;
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
+30
View File
@@ -0,0 +1,30 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '@prisma/client';
import { Pool } from 'pg';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
private readonly pool: Pool;
constructor(private readonly config: ConfigService) {
const connectionString = config.getOrThrow<string>('databaseUrl');
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
super({ adapter });
this.pool = pool;
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
await this.pool.end();
}
}
+10
View File
@@ -0,0 +1,10 @@
export function slugify(input: string): string {
return input
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 120) || 'item';
}