base con autenticacion, registro, modulo encuestas
This commit is contained in:
58
apps/api/src/common/config/envs.ts
Normal file
58
apps/api/src/common/config/envs.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'dotenv/config';
|
||||
import * as joi from 'joi';
|
||||
|
||||
interface EnvVars {
|
||||
HOST: string;
|
||||
ALLOW_CORS_URL: string;
|
||||
PORT: number;
|
||||
NODE_ENV: string;
|
||||
DATABASE_URL: string;
|
||||
ACCESS_TOKEN_SECRET: string;
|
||||
ACCESS_TOKEN_EXPIRATION: string;
|
||||
REFRESH_TOKEN_SECRET: string;
|
||||
REFRESH_TOKEN_EXPIRATION: string;
|
||||
MAIL_HOST: string;
|
||||
MAIL_USERNAME: string;
|
||||
MAIL_PASSWORD: string;
|
||||
|
||||
}
|
||||
|
||||
const envsSchema = joi
|
||||
.object({
|
||||
HOST: joi.string().required(),
|
||||
ALLOW_CORS_URL: joi.string().required(),
|
||||
PORT: joi.number().required(),
|
||||
NODE_ENV: joi.string().required(),
|
||||
DATABASE_URL: joi.string().required(),
|
||||
ACCESS_TOKEN_SECRET: joi.string().required(),
|
||||
ACCESS_TOKEN_EXPIRATION: joi.string().required(),
|
||||
REFRESH_TOKEN_SECRET: joi.string().required(),
|
||||
REFRESH_TOKEN_EXPIRATION: joi.string().required(),
|
||||
MAIL_HOST: joi.string(),
|
||||
MAIL_USERNAME: joi.string(),
|
||||
MAIL_PASSWORD: joi.string(),
|
||||
})
|
||||
.unknown(true);
|
||||
|
||||
const { error, value } = envsSchema.validate(process.env);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Config validation error: ${error.message}`);
|
||||
}
|
||||
|
||||
const envVars: EnvVars = value;
|
||||
|
||||
export const envs = {
|
||||
port: envVars.PORT,
|
||||
dataBaseUrl: envVars.DATABASE_URL,
|
||||
node_env: envVars.NODE_ENV,
|
||||
host: envVars.HOST,
|
||||
allow_cors_url: envVars.ALLOW_CORS_URL,
|
||||
access_token_secret: envVars.ACCESS_TOKEN_SECRET,
|
||||
access_token_expiration: envVars.ACCESS_TOKEN_EXPIRATION,
|
||||
refresh_token_secret: envVars.REFRESH_TOKEN_SECRET,
|
||||
refresh_token_expiration: envVars.REFRESH_TOKEN_EXPIRATION,
|
||||
mail_host: envVars.MAIL_HOST,
|
||||
mail_username: envVars.MAIL_USERNAME,
|
||||
mail_password: envVars.MAIL_PASSWORD
|
||||
};
|
||||
1
apps/api/src/common/constants/index.ts
Normal file
1
apps/api/src/common/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './role';
|
||||
6
apps/api/src/common/constants/role.ts
Normal file
6
apps/api/src/common/constants/role.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Cambiamos de un enum estático a un tipo string para soportar roles dinámicos
|
||||
export const roleSchema = z.string();
|
||||
|
||||
export type Role = z.infer<typeof roleSchema>;
|
||||
3
apps/api/src/common/decorators/index.ts
Normal file
3
apps/api/src/common/decorators/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './public.decorator';
|
||||
export * from './roles.decorator';
|
||||
export * from './user.decorator';
|
||||
5
apps/api/src/common/decorators/permissions.decorator.ts
Normal file
5
apps/api/src/common/decorators/permissions.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
export const RequirePermissions = (...permissions: string[]) =>
|
||||
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
4
apps/api/src/common/decorators/public.decorator.ts
Normal file
4
apps/api/src/common/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
export const RequirePermissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
5
apps/api/src/common/decorators/roles.decorator.ts
Normal file
5
apps/api/src/common/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Role } from '../constants';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||
8
apps/api/src/common/decorators/user.decorator.ts
Normal file
8
apps/api/src/common/decorators/user.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const User = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
35
apps/api/src/common/dto/pagination.dto.ts
Normal file
35
apps/api/src/common/dto/pagination.dto.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsInt, Min, IsString, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class PaginationDto {
|
||||
@ApiPropertyOptional({ default: 1, description: 'Page number' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ default: 10, description: 'Items per page' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
limit?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Search term' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: 'id', description: 'Field to sort by' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sortBy?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: 'asc', enum: ['asc', 'desc'], description: 'Sort order' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['asc', 'desc'])
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
2
apps/api/src/common/guards/index.ts
Normal file
2
apps/api/src/common/guards/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './jwt-auth.guard';
|
||||
export * from './roles.guard';
|
||||
81
apps/api/src/common/guards/jwt-auth.guard.ts
Normal file
81
apps/api/src/common/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { IS_PUBLIC_KEY } from 'src/common/decorators';
|
||||
import { envs } from '../config/envs';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { roles, usersRole } from 'src/database/schema/auth';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private reflector: Reflector,
|
||||
@Inject(DRIZZLE_PROVIDER)
|
||||
private readonly drizzle: NodePgDatabase,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: envs.access_token_secret,
|
||||
});
|
||||
|
||||
// Asegurarse de que el payload contiene el ID del usuario
|
||||
if (!payload.sub && !payload.id) {
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
|
||||
const userId = payload.sub || payload.id;
|
||||
|
||||
// Obtener los roles del usuario desde la base de datos
|
||||
const userRoles = await this.drizzle
|
||||
.select({ name: roles.name })
|
||||
.from(roles)
|
||||
.innerJoin(usersRole, eq(usersRole.roleId, roles.id))
|
||||
.where(eq(usersRole.userId, userId));
|
||||
|
||||
// Verificar si el usuario tiene el rol SUPERADMIN
|
||||
const isSuperAdmin = userRoles.some(role => role.name === 'superadmin');
|
||||
|
||||
// Adjuntar el usuario a la solicitud con el ID correcto y sus roles
|
||||
request.user = {
|
||||
id: userId,
|
||||
username: payload.username,
|
||||
email: payload.email,
|
||||
roles: userRoles.map(role => role.name),
|
||||
isSuperAdmin, // Añadir flag para indicar si es SUPERADMIN
|
||||
};
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid Access Token');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
49
apps/api/src/common/guards/jwt-refresh.guard.ts
Normal file
49
apps/api/src/common/guards/jwt-refresh.guard.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Inject,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { Request } from 'express';
|
||||
import * as schema from 'src/database/index';
|
||||
import { envs } from '../config/envs';
|
||||
|
||||
@Injectable()
|
||||
export class JwtRefreshGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
request.user = await this.jwtService.verifyAsync(token, {
|
||||
secret: envs.refresh_token_secret,
|
||||
});
|
||||
} catch {
|
||||
const session = await this.drizzle
|
||||
.select()
|
||||
.from(schema.sessions)
|
||||
.where(eq(schema.sessions, token));
|
||||
if (session.length === 0) {
|
||||
throw new UnauthorizedException('Invalid Refresh Token');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
47
apps/api/src/common/guards/roles.guard.ts
Normal file
47
apps/api/src/common/guards/roles.guard.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
import { IS_PUBLIC_KEY } from '../decorators';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si el usuario es SUPERADMIN, permitir acceso sin verificar más
|
||||
if (user.isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene alguno de los roles requeridos
|
||||
return requiredRoles.some(role =>
|
||||
user.roles.includes(role)
|
||||
);
|
||||
}
|
||||
}
|
||||
1
apps/api/src/common/interceptors/index.ts
Normal file
1
apps/api/src/common/interceptors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './req-log.interceptor';
|
||||
36
apps/api/src/common/interceptors/req-log.interceptor.ts
Normal file
36
apps/api/src/common/interceptors/req-log.interceptor.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { concatStr } from '@/common/utils';
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
Logger,
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ReqLogInterceptor implements NestInterceptor {
|
||||
private readonly logger: Logger;
|
||||
constructor() {
|
||||
this.logger = new Logger('REQUEST INTERCEPTOR', { timestamp: true });
|
||||
}
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const res = context.switchToHttp().getResponse();
|
||||
/* *
|
||||
* Before the request is handled, log the request details
|
||||
* */
|
||||
this.logger.log(concatStr([req.method, req.originalUrl]));
|
||||
return next.handle().pipe(
|
||||
tap(() =>
|
||||
/* *
|
||||
* After the request is handled, log the response details
|
||||
* */
|
||||
this.logger.log(
|
||||
concatStr([req.method, req.originalUrl, res.statusCode]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
apps/api/src/common/middlewares/index.ts
Normal file
1
apps/api/src/common/middlewares/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './logger.middleware';
|
||||
15
apps/api/src/common/middlewares/logger.middleware.ts
Normal file
15
apps/api/src/common/middlewares/logger.middleware.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { concatStr } from '@/common/utils';
|
||||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class LoggerMiddleware implements NestMiddleware {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
this.logger.log(
|
||||
concatStr([req.method, req.originalUrl, res.statusCode]),
|
||||
'Request',
|
||||
);
|
||||
next();
|
||||
}
|
||||
}
|
||||
3
apps/api/src/common/modules/index.ts
Normal file
3
apps/api/src/common/modules/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './logger.module';
|
||||
export * from './node-mailer.module';
|
||||
export * from './throttle.module';
|
||||
24
apps/api/src/common/modules/logger.module.ts
Normal file
24
apps/api/src/common/modules/logger.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Env } from '@/common/utils';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { LoggerModule as PinoModule } from 'nestjs-pino';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PinoModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService<Env>) => ({
|
||||
pinoHttp: {
|
||||
quietReqLogger: false,
|
||||
quietResLogger: false,
|
||||
transport: {
|
||||
target:
|
||||
config.get('NODE_ENV') !== 'production' ? 'pino-pretty' : '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class LoggerModule {}
|
||||
23
apps/api/src/common/modules/node-mailer.module.ts
Normal file
23
apps/api/src/common/modules/node-mailer.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Env } from '@/common/utils';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MailerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService<Env>) => ({
|
||||
transport: {
|
||||
service: config.get('MAIL_HOST'),
|
||||
auth: {
|
||||
user: config.get('MAIL_USERNAME'),
|
||||
pass: config.get('MAIL_PASSWORD'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class NodeMailerModule {}
|
||||
28
apps/api/src/common/modules/throttle.module.ts
Normal file
28
apps/api/src/common/modules/throttle.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ThrottlerModule.forRoot({
|
||||
throttlers: [
|
||||
{
|
||||
name: 'short',
|
||||
ttl: 1000, // 1 sec
|
||||
limit: 2,
|
||||
},
|
||||
{
|
||||
name: 'medium',
|
||||
ttl: 10000, // 10 sec
|
||||
limit: 4,
|
||||
},
|
||||
{
|
||||
name: 'long',
|
||||
ttl: 60000, // 1 min
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
errorMessage: 'Too many requests, please try again later.',
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ThrottleModule {}
|
||||
38
apps/api/src/common/pipes/file-size-validator.pipe.ts
Normal file
38
apps/api/src/common/pipes/file-size-validator.pipe.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { FileValidator } from '@nestjs/common';
|
||||
import { IFile } from '@nestjs/common/pipes/file/interfaces';
|
||||
|
||||
export interface FileSizeValidatorOptions {
|
||||
fileSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the built-in FileType File Validator. It validates incoming files mime-type
|
||||
* matching a string or a regular expression. Note that this validator uses a naive strategy
|
||||
* to check the mime-type and could be fooled if the client provided a file with renamed extension.
|
||||
* (for instance, renaming a 'malicious.bat' to 'malicious.jpeg'). To handle such security issues
|
||||
* with more reliability, consider checking against the file's [magic-numbers](https://en.wikipedia.org/wiki/Magic_number_%28programming%29)
|
||||
*
|
||||
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export class FileSizeValidatorPipe extends FileValidator<
|
||||
FileSizeValidatorOptions,
|
||||
IFile
|
||||
> {
|
||||
buildErrorMessage(): string {
|
||||
return `Max file size is ${(this.validationOptions.fileSize * 0.000001).toFixed()} Mb`;
|
||||
}
|
||||
|
||||
isValid(file?: IFile): boolean {
|
||||
if (!this.validationOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!!file &&
|
||||
'mimetype' in file &&
|
||||
+file.size < this.validationOptions.fileSize
|
||||
);
|
||||
}
|
||||
}
|
||||
38
apps/api/src/common/pipes/file-type-validator.pipe.ts
Normal file
38
apps/api/src/common/pipes/file-type-validator.pipe.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { FileValidator } from '@nestjs/common';
|
||||
import { IFile } from '@nestjs/common/pipes/file/interfaces';
|
||||
|
||||
export interface FileTypeValidatorOptions {
|
||||
fileType: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the built-in FileType File Validator. It validates incoming files mime-type
|
||||
* matching a string or a regular expression. Note that this validator uses a naive strategy
|
||||
* to check the mime-type and could be fooled if the client provided a file with renamed extension.
|
||||
* (for instance, renaming a 'malicious.bat' to 'malicious.jpeg'). To handle such security issues
|
||||
* with more reliability, consider checking against the file's [magic-numbers](https://en.wikipedia.org/wiki/Magic_number_%28programming%29)
|
||||
*
|
||||
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export class FileTypeValidatorPipe extends FileValidator<
|
||||
FileTypeValidatorOptions,
|
||||
IFile
|
||||
> {
|
||||
buildErrorMessage(): string {
|
||||
return `File must be ${this.validationOptions.fileType}`;
|
||||
}
|
||||
|
||||
isValid(file?: IFile): boolean {
|
||||
if (!this.validationOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!!file &&
|
||||
'mimetype' in file &&
|
||||
this.validationOptions.fileType.includes(file.mimetype)
|
||||
);
|
||||
}
|
||||
}
|
||||
3
apps/api/src/common/pipes/index.ts
Normal file
3
apps/api/src/common/pipes/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './file-size-validator.pipe';
|
||||
export * from './file-type-validator.pipe';
|
||||
export * from './zod-validator.pipe';
|
||||
17
apps/api/src/common/pipes/zod-validator.pipe.ts
Normal file
17
apps/api/src/common/pipes/zod-validator.pipe.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BadRequestException, PipeTransform } from '@nestjs/common';
|
||||
import { ZodSchema, z } from 'zod';
|
||||
|
||||
export class ZodValidatorPipe implements PipeTransform {
|
||||
constructor(private schema: ZodSchema) {}
|
||||
transform(
|
||||
value: unknown,
|
||||
// metadata: ArgumentMetadata,
|
||||
): z.infer<typeof this.schema> {
|
||||
const validateFields = this.schema.safeParse(value);
|
||||
if (!validateFields.success)
|
||||
throw new BadRequestException({
|
||||
errors: validateFields.error.flatten().fieldErrors,
|
||||
});
|
||||
return validateFields.data;
|
||||
}
|
||||
}
|
||||
16
apps/api/src/common/utils/bcrypt.ts
Normal file
16
apps/api/src/common/utils/bcrypt.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { compare, hash } from 'bcryptjs';
|
||||
|
||||
const hashString = async (password: string): Promise<string> => {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
return hash(password, salt);
|
||||
};
|
||||
|
||||
const validateString = async (
|
||||
plainPassword: string,
|
||||
hashedPassword: string,
|
||||
): Promise<boolean> => {
|
||||
return await compare(plainPassword, hashedPassword);
|
||||
};
|
||||
|
||||
export { hashString, validateString };
|
||||
28
apps/api/src/common/utils/dateTimeUtility.ts
Normal file
28
apps/api/src/common/utils/dateTimeUtility.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
import * as moment from 'moment';
|
||||
|
||||
export const getExpiry = (cant: number) => {
|
||||
const createdAt = new Date();
|
||||
const expiresAt = moment(createdAt).add(cant, 'days').toDate();
|
||||
return expiresAt;
|
||||
};
|
||||
|
||||
export const getExpiryCode = (cant: number) => {
|
||||
const createdAt = new Date();
|
||||
const expiresAt = moment(createdAt).add(cant, 'seconds').toDate();
|
||||
return expiresAt;
|
||||
};
|
||||
|
||||
export function isDateExpired(expiry: Date): boolean {
|
||||
const expirationDate = new Date(expiry);
|
||||
const currentDate = new Date();
|
||||
return expirationDate.getTime() <= currentDate.getTime();
|
||||
}
|
||||
|
||||
export const expirationTimeInSeconds = (cant: number) => {
|
||||
const currentTimeInMillis = Date.now();
|
||||
const iat = Math.floor(currentTimeInMillis / 1000);
|
||||
const expirationTimeInSeconds = cant * 24 * 60 * 60;
|
||||
const exp = iat + expirationTimeInSeconds;
|
||||
return exp;
|
||||
};
|
||||
16
apps/api/src/common/utils/index.ts
Normal file
16
apps/api/src/common/utils/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const isEmptyObj = (obj: object) =>
|
||||
Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
|
||||
export const concatStr = (
|
||||
strings: (number | string)[],
|
||||
divider?: string,
|
||||
): string => strings.join(divider ?? ' ');
|
||||
|
||||
export const getRandomInt = (min: number, max: number) => {
|
||||
const minCelled = Math.ceil(min),
|
||||
maxFloored = Math.floor(max);
|
||||
return Math.floor(Math.random() * (maxFloored - minCelled) + minCelled); // The maximum is exclusive and the minimum is inclusive
|
||||
};
|
||||
|
||||
export * from './bcrypt';
|
||||
export * from './validateEnv';
|
||||
36
apps/api/src/common/utils/validateEnv.ts
Normal file
36
apps/api/src/common/utils/validateEnv.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const EnvSchema = z.object({
|
||||
HOST: z.string(),
|
||||
NODE_ENV: z
|
||||
.enum(['development', 'production', 'test', 'provision'])
|
||||
.default('development'),
|
||||
PORT: z
|
||||
.string()
|
||||
.default('8000')
|
||||
.transform((data: any) => +data),
|
||||
ALLOW_CORS_URL: z.string().url(),
|
||||
ACCESS_TOKEN_SECRET: z.string().min(10).max(128),
|
||||
ACCESS_TOKEN_EXPIRATION: z.string().min(1).max(60),
|
||||
REFRESH_TOKEN_SECRET: z.string().min(10).max(128),
|
||||
REFRESH_TOKEN_EXPIRATION: z.string().min(1).max(365),
|
||||
DB_HOST: z.string(),
|
||||
DB_PORT: z.string(),
|
||||
DB_USERNAME: z.string(),
|
||||
DB_PASSWORD: z.string(),
|
||||
DB_NAME: z.string(),
|
||||
MAIL_HOST: z.string(),
|
||||
MAIL_USERNAME: z.string(),
|
||||
MAIL_PASSWORD: z.string(),
|
||||
DATABASE_URL: z.string(),
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof EnvSchema>;
|
||||
|
||||
export const validateEnv = (config: Record<string, unknown>): Env => {
|
||||
const validate = EnvSchema.safeParse(config);
|
||||
if (!validate.success) {
|
||||
throw new Error(validate.error.message);
|
||||
}
|
||||
return validate.data;
|
||||
};
|
||||
Reference in New Issue
Block a user