base con autenticacion, registro, modulo encuestas

This commit is contained in:
2025-06-16 12:02:22 -04:00
commit 475e0754df
411 changed files with 26265 additions and 0 deletions

19
apps/api/.env.example Normal file
View File

@@ -0,0 +1,19 @@
# Server Configuration
HOST=localhost
PORT=8000
ALLOW_CORS_URL=http://localhost:3000
NODE_ENV='development' #development | production
#Jwt Securtiy
ACCESS_TOKEN_SECRET=bc63d848ca6e651b3b848bd96ef1ad1eb9b31afc9cad67ed5953efd023d02ffe
ACCESS_TOKEN_EXPIRATION=2h
REFRESH_TOKEN_SECRET=bc63d848ca6e651b3b848bd96ef1ad1eb9b31afc9cad67ed5953efd023d02ffe
REFRESH_TOKEN_EXPIRATION=30d
#Database Configuration
DATABASE_URL="postgresql://postgres:local**@localhost:5432/caja_ahorro" #url conexion a base de datos
#Mail Configuration
MAIL_HOST=gmail
MAIL_USERNAME=
MAIL_PASSWORD=

56
apps/api/.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

13
apps/api/.swcrc Normal file
View File

@@ -0,0 +1,13 @@
{
"$schema": "https://swc.rs/schema.json",
"sourceMaps": true,
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"baseUrl": "./"
},
"minify": false
}

1
apps/api/README.md Normal file
View File

@@ -0,0 +1 @@
### Backend

View File

@@ -0,0 +1,4 @@
import { nestJsConfig } from '@repo/eslint-config/nest-js';
/** @type {import("eslint").Linter.Config} */
export default nestJsConfig;

10
apps/api/nest-cli.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"builder": "swc",
"typeCheck": true
}
}

87
apps/api/package.json Normal file
View File

@@ -0,0 +1,87 @@
{
"name": "api",
"private": true,
"scripts": {
"build": "nest build",
"db:generate": "drizzle-kit generate --config ./src/drizzle.config.ts",
"db:migrate": "drizzle-kit migrate --config ./src/drizzle.config.ts",
"db:push": "drizzle-kit push --config ./src/drizzle.config.ts",
"db:seed": "ts-node -r tsconfig-paths/register ./src/database/seeds/index.ts",
"dev": "nest start -b swc -w",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"start": "node dist/main",
"start:debug": "nest start --debug --watch",
"start:dev": "nest start",
"test": "jest",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:watch": "jest --watch"
},
"jest": {
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testEnvironment": "node",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
},
"dependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"drizzle-orm": "^0.40.0",
"joi": "^17.13.3",
"moment": "^2.30.1",
"path-to-regexp": "^8.2.0",
"pg": "^8.13.3",
"pino-pretty": "^13.0.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/cli": "^11.0.0",
"@nestjs/config": "^4.0.0",
"@nestjs/jwt": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/swagger": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@nestjs/throttler": "^6.3.0",
"@repo/eslint-config": "workspace:*",
"@repo/ts-config": "workspace:*",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.14",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.12",
"@types/pg": "^8.11.11",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"bcryptjs": "^3.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"drizzle-kit": "^0.30.5",
"jest": "^29.5.0",
"nestjs-pino": "^4.1.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.19.3"
}
}

View File

@@ -0,0 +1,64 @@
import { JwtAuthGuard, RolesGuard } from '@/common/guards';
import {
LoggerModule,
NodeMailerModule,
ThrottleModule,
} from '@/common/modules';
import { UsersModule } from '@/features/users/users.module';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { JwtModule } from '@nestjs/jwt';
import { ThrottlerGuard } from '@nestjs/throttler';
import { DrizzleModule } from './database/drizzle.module';
import { AuthModule } from './features/auth/auth.module';
import { ConfigurationsModule } from './features/configurations/configurations.module';
import { LocationModule} from './features/location/location.module'
import { MailModule } from './features/mail/mail.module';
import { RolesModule } from './features/roles/roles.module';
import { UserRolesModule } from './features/user-roles/user-roles.module';
import { SurveysModule } from './features/surveys/surveys.module';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
// {
// provide: APP_GUARD,
// useClass: PermissionsGuard,
// },
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
imports: [
JwtModule.register({
global: true,
}),
ConfigModule.forRoot({
isGlobal: true,
//validate: validateEnv,
}),
NodeMailerModule,
LoggerModule,
ThrottleModule,
UsersModule,
AuthModule,
MailModule,
DrizzleModule,
RolesModule,
UserRolesModule,
ConfigurationsModule,
SurveysModule,
LocationModule
],
})
export class AppModule {}

35
apps/api/src/bootstrap.ts Normal file
View File

@@ -0,0 +1,35 @@
import { swagger } from '@/swagger';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { Logger } from 'nestjs-pino';
import { envs } from './common/config/envs';
export const bootstrap = async (app: NestExpressApplication) => {
const logger = app.get(Logger);
// app.setGlobalPrefix('api');
app.useStaticAssets('./uploads', {
prefix: '/assets',
});
app.enableCors({
credentials: true,
//origin: envs.allow_cors_url,
origin: ['*'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
});
app.useLogger(logger);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await swagger(app);
await app.listen(envs.port!, () => {
logger.log(`This application started at ${envs.host}:${envs.port}`);
});
};

View 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
};

View File

@@ -0,0 +1 @@
export * from './role';

View 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>;

View File

@@ -0,0 +1,3 @@
export * from './public.decorator';
export * from './roles.decorator';
export * from './user.decorator';

View File

@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: string[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);

View 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);

View 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;
},
);

View 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';
}

View File

@@ -0,0 +1,2 @@
export * from './jwt-auth.guard';
export * from './roles.guard';

View 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;
}
}

View 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;
}
}

View 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)
);
}
}

View File

@@ -0,0 +1 @@
export * from './req-log.interceptor';

View 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]),
),
),
);
}
}

View File

@@ -0,0 +1 @@
export * from './logger.middleware';

View 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();
}
}

View File

@@ -0,0 +1,3 @@
export * from './logger.module';
export * from './node-mailer.module';
export * from './throttle.module';

View 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 {}

View 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 {}

View 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 {}

View 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
);
}
}

View 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)
);
}
}

View File

@@ -0,0 +1,3 @@
export * from './file-size-validator.pipe';
export * from './file-type-validator.pipe';
export * from './zod-validator.pipe';

View 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;
}
}

View 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 };

View 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;
};

View 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';

View 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;
};

View File

@@ -0,0 +1,22 @@
import { Provider } from '@nestjs/common';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './index';
import { envs } from 'src/common/config/envs';
export const DRIZZLE_PROVIDER = 'DRIZZLE_PROVIDER';
export type DrizzleDatabase = NodePgDatabase<typeof schema>;
export const DrizzleProvider: Provider = {
provide: DRIZZLE_PROVIDER,
useFactory: () => {
const pool = new Pool({
connectionString: envs.dataBaseUrl,
ssl:
envs.node_env === 'production' ? { rejectUnauthorized: false } : false,
});
return drizzle(pool, { schema }) as DrizzleDatabase;
},
};

View File

@@ -0,0 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { DrizzleProvider } from './drizzle-provider';
@Global()
@Module({
imports: [],
providers: [DrizzleProvider],
exports: [DrizzleProvider],
})
export class DrizzleModule {}

View File

@@ -0,0 +1,5 @@
export * from './schema/activity_logs';
export * from './schema/auth';
export * from './schema/general';
export * from './schema/surveys'

View File

@@ -0,0 +1,173 @@
CREATE SCHEMA "auth";
--> statement-breakpoint
CREATE TYPE "auth"."gender" AS ENUM('FEMENINO', 'MASCULINO');--> statement-breakpoint
CREATE TYPE "public"."nationality" AS ENUM('VENEZOLANO', 'EXTRANJERO');--> statement-breakpoint
CREATE TYPE "auth"."status" AS ENUM('ACTIVE', 'INACTIVE');--> statement-breakpoint
CREATE TABLE "activity_logs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" integer,
"type" text NOT NULL,
"description" text NOT NULL,
"timestamp" timestamp DEFAULT now(),
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "auth"."roles" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "auth"."sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" integer NOT NULL,
"session_token" text NOT NULL,
"expires_at" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "auth"."users" (
"id" serial PRIMARY KEY NOT NULL,
"username" text NOT NULL,
"email" text NOT NULL,
"fullname" text NOT NULL,
"phone" text,
"password" text NOT NULL,
"is_two_factor_enabled" boolean DEFAULT false NOT NULL,
"two_factor_secret" text,
"is_email_verified" boolean DEFAULT false NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3),
CONSTRAINT "users_username_unique" UNIQUE("username"),
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "auth"."user_role" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer,
"role_id" integer,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "auth"."verificationToken" (
"identifier" text NOT NULL,
"token" text NOT NULL,
"code" integer,
"expires" timestamp NOT NULL,
"ip_address" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "category_type" (
"id" serial PRIMARY KEY NOT NULL,
"group" varchar(100) NOT NULL,
"description" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "localities" (
"id" serial PRIMARY KEY NOT NULL,
"state_id" integer NOT NULL,
"municipality_id" integer NOT NULL,
"parish_id" integer NOT NULL,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3),
CONSTRAINT "localities_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE "municipalities" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"state_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "parishes" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"municipality_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "states" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "answers_surveys" (
"id" serial PRIMARY KEY NOT NULL,
"survey_id" integer,
"user_id" integer,
"answers" jsonb NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE "surveys" (
"id" serial PRIMARY KEY NOT NULL,
"title" text NOT NULL,
"description" text NOT NULL,
"target_audience" varchar(50) NOT NULL,
"closing_date" date,
"published" boolean NOT NULL,
"questions" jsonb NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "auth"."sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "auth"."user_role" ADD CONSTRAINT "user_role_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "auth"."user_role" ADD CONSTRAINT "user_role_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "auth"."roles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "localities" ADD CONSTRAINT "localities_state_id_states_id_fk" FOREIGN KEY ("state_id") REFERENCES "public"."states"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "localities" ADD CONSTRAINT "localities_municipality_id_municipalities_id_fk" FOREIGN KEY ("municipality_id") REFERENCES "public"."municipalities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "localities" ADD CONSTRAINT "localities_parish_id_parishes_id_fk" FOREIGN KEY ("parish_id") REFERENCES "public"."parishes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "municipalities" ADD CONSTRAINT "municipalities_state_id_states_id_fk" FOREIGN KEY ("state_id") REFERENCES "public"."states"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "parishes" ADD CONSTRAINT "parishes_municipality_id_municipalities_id_fk" FOREIGN KEY ("municipality_id") REFERENCES "public"."municipalities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "answers_surveys" ADD CONSTRAINT "answers_surveys_survey_id_surveys_id_fk" FOREIGN KEY ("survey_id") REFERENCES "public"."surveys"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "answers_surveys" ADD CONSTRAINT "answers_surveys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "activityLogs_idx" ON "activity_logs" USING btree ("type");--> statement-breakpoint
CREATE INDEX "roles_idx" ON "auth"."roles" USING btree ("name");--> statement-breakpoint
CREATE INDEX "sessions_idx" ON "auth"."sessions" USING btree ("session_token");--> statement-breakpoint
CREATE INDEX "users_idx" ON "auth"."users" USING btree ("username");--> statement-breakpoint
CREATE INDEX "user_role_idx" ON "auth"."user_role" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "category_typeIx0" ON "category_type" USING btree ("group");--> statement-breakpoint
CREATE INDEX "category_typeIx1" ON "category_type" USING btree ("description");--> statement-breakpoint
CREATE UNIQUE INDEX "localities_index_03" ON "localities" USING btree ("state_id","municipality_id","parish_id");--> statement-breakpoint
CREATE INDEX "localities_index_00" ON "localities" USING btree ("state_id");--> statement-breakpoint
CREATE INDEX "localities_index_01" ON "localities" USING btree ("municipality_id");--> statement-breakpoint
CREATE INDEX "localities_index_02" ON "localities" USING btree ("parish_id");--> statement-breakpoint
CREATE INDEX "municipalities_index_00" ON "municipalities" USING btree ("id","name","state_id");--> statement-breakpoint
CREATE INDEX "parishes_index_00" ON "parishes" USING btree ("id","name","municipality_id");--> statement-breakpoint
CREATE INDEX "states_index_00" ON "states" USING btree ("id","name");--> statement-breakpoint
CREATE INDEX "answers_index_00" ON "answers_surveys" USING btree ("answers");--> statement-breakpoint
CREATE INDEX "answers_index_01" ON "answers_surveys" USING btree ("survey_id");--> statement-breakpoint
CREATE INDEX "answers_index_02" ON "answers_surveys" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "surveys_index_00" ON "surveys" USING btree ("title");--> statement-breakpoint
CREATE VIEW "auth"."user_access_view" AS (
SELECT
u.id AS user_id,
u.username,
u.email,
u.fullname,
r.id AS role_id,
r.name AS role_name
FROM
auth.users u
LEFT JOIN
auth.user_role ur ON u.id = ur.user_id
LEFT JOIN
auth.roles r ON ur.role_id = r.id);--> statement-breakpoint
CREATE VIEW "public"."v_surveys" AS (select s.id as survey_id, s.title, s.description, s.created_at, s.closing_date, s.target_audience, as2.user_id from surveys s
left join answers_surveys as2 on as2.survey_id = s.id
where s.published = true);

View File

@@ -0,0 +1,9 @@
DROP VIEW "public"."v_surveys";--> statement-breakpoint
ALTER TABLE "auth"."users" ADD COLUMN "state" integer;--> statement-breakpoint
ALTER TABLE "auth"."users" ADD COLUMN "municipality" integer;--> statement-breakpoint
ALTER TABLE "auth"."users" ADD COLUMN "parish" integer;--> statement-breakpoint
ALTER TABLE "auth"."users" ADD CONSTRAINT "users_state_states_id_fk" FOREIGN KEY ("state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "auth"."users" ADD CONSTRAINT "users_municipality_municipalities_id_fk" FOREIGN KEY ("municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "auth"."users" ADD CONSTRAINT "users_parish_parishes_id_fk" FOREIGN KEY ("parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE VIEW "public"."v_surveys" AS (select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
where published = true);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1743783835462,
"tag": "0000_abnormal_lethal_legion",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1747665408016,
"tag": "0001_massive_kylun",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,32 @@
import * as t from 'drizzle-orm/pg-core';
import { index, pgTable } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from './auth';
const timestamps = {
created_at: t.timestamp('created_at').defaultNow().notNull(),
updated_at: t
.timestamp('updated_at', { mode: 'date', precision: 3 })
.$onUpdate(() => new Date()),
};
// Tabla de Logs de Actividad
export const activityLogsSystem = pgTable(
'activity_logs',
{
id: t
.uuid('id')
.primaryKey()
.default(sql`gen_random_uuid()`),
userId: t
.integer('user_id')
.references(() => users.id, { onDelete: 'cascade' }),
type: t.text('type').notNull(), // login, failed
description: t.text('description').notNull(),
timestamp: t.timestamp('timestamp').defaultNow(),
...timestamps,
},
(activityLogs) => ({
activityLogsIdx: index('activityLogs_idx').on(activityLogs.type),
}),
);

View File

@@ -0,0 +1,132 @@
import * as t from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { authSchema } from './schemas';
import { timestamps } from '../timestamps';
import { states, municipalities, parishes } from './general';
// Tabla de Usuarios sistema
export const users = authSchema.table(
'users',
{
id: t.serial('id').primaryKey(),
username: t.text('username').unique().notNull(),
email: t.text('email').unique().notNull(),
fullname: t.text('fullname').notNull(),
phone: t.text('phone'),
password: t.text('password').notNull(),
state: t.integer('state').references(() => states.id, { onDelete: 'set null' }),
municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }),
parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }),
isTwoFactorEnabled: t
.boolean('is_two_factor_enabled')
.notNull()
.default(false),
twoFactorSecret: t.text('two_factor_secret'),
isEmailVerified: t.boolean('is_email_verified').notNull().default(false),
isActive: t.boolean('is_active').notNull().default(true),
...timestamps,
},
(users) => ({
usersIdx: t.index('users_idx').on(users.username),
}),
);
// Tabla de Roles
export const roles = authSchema.table(
'roles',
{
id: t.serial('id').primaryKey(),
name: t.text('name').notNull(),
...timestamps,
},
(roles) => ({
rolesIdx: t.index('roles_idx').on(roles.name),
}),
);
//tabla User_roles
export const usersRole = authSchema.table(
'user_role',
{
id: t.serial('id').primaryKey(),
userId: t
.integer('user_id')
.references(() => users.id, { onDelete: 'cascade' }),
roleId: t
.integer('role_id')
.references(() => roles.id, { onDelete: 'set null' }),
...timestamps,
},
(userRole) => ({
userRoleIdx: t.index('user_role_idx').on(userRole.userId),
}),
);
export const userAccessView = authSchema.view('user_access_view', {
userId: t.integer('userId').notNull(),
username: t.text('username').notNull(),
email: t.text('email').notNull(),
fullname: t.text('email').notNull(),
roleId: t.integer('role_id'),
roleName: t.text('role_name'),
}).as(sql`
SELECT
u.id AS user_id,
u.username,
u.email,
u.fullname,
r.id AS role_id,
r.name AS role_name
FROM
auth.users u
LEFT JOIN
auth.user_role ur ON u.id = ur.user_id
LEFT JOIN
auth.roles r ON ur.role_id = r.id`);
// Tabla de Sesiones
export const sessions = authSchema.table(
'sessions',
{
id: t
.uuid('id')
.primaryKey()
.default(sql`gen_random_uuid()`),
userId: t
.integer('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
sessionToken: t.text('session_token').notNull(),
expiresAt: t.integer('expires_at').notNull(),
...timestamps,
},
(sessions) => ({
sessionsIdx: t.index('sessions_idx').on(sessions.sessionToken),
}),
);
//tabla de tokens de verificación
export const verificationTokens = authSchema.table(
'verificationToken',
{
identifier: t.text('identifier').notNull(),
token: t.text('token').notNull(),
code: t.integer('code'),
expires: t.timestamp('expires', { mode: 'date' }).notNull(),
ipAddress: t.text('ip_address').notNull(),
},
(verificationToken) => [
{
compositePk: t.primaryKey({
columns: [verificationToken.identifier, verificationToken.token],
}),
},
],
);

View File

@@ -0,0 +1,8 @@
import { pgEnum } from 'drizzle-orm/pg-core';
import { authSchema } from './schemas';
export const statusEnum = authSchema.enum('status', ['ACTIVE', 'INACTIVE']);
export const genderEnum = authSchema.enum('gender', ['FEMENINO', 'MASCULINO']);
export const nationalityEnum = pgEnum('nationality', [
'VENEZOLANO',
'EXTRANJERO',
]);

View File

@@ -0,0 +1,114 @@
import * as t from 'drizzle-orm/pg-core';
import { timestamps } from '../timestamps';
//Tabla de Tipo de categorias
export const categoryType = t.pgTable(
'category_type',
{
id: t.serial('id').primaryKey(),
group: t.varchar('group', { length: 100 }).notNull(), // grupo pertenece
description: t.text('description').notNull(), // name
...timestamps,
},
(categoryType) => ({
categoryTypeIdx0: t.index('category_typeIx0').on(categoryType.group),
categoryTypeIdx1: t.index('category_typeIx1').on(categoryType.description),
}),
);
// Tabla States
export const states = t.pgTable(
'states',
{
id: t.serial('id').primaryKey(),
name: t.text('name').notNull(),
...timestamps,
},
(states) => ({
nameIndex: t.index('states_index_00').on(states.id, states.name),
}),
);
// Tabla Municipalities
export const municipalities = t.pgTable(
'municipalities',
{
id: t.serial('id').primaryKey(),
name: t.text('name').notNull(),
stateId: t
.integer('state_id')
.notNull()
.references(() => states.id, { onDelete: 'cascade' }),
...timestamps,
},
(municipalities) => ({
nameStateIndex: t
.index('municipalities_index_00')
.on(municipalities.id, municipalities.name, municipalities.stateId),
}),
);
// Tabla Parishes
export const parishes = t.pgTable(
'parishes',
{
id: t.serial('id').primaryKey(),
name: t.text('name').notNull(),
municipalityId: t
.integer('municipality_id')
.notNull()
.references(() => municipalities.id, { onDelete: 'cascade' }),
...timestamps,
},
(parishes) => ({
parishIndex: t
.index('parishes_index_00')
.on(parishes.id, parishes.name, parishes.municipalityId),
}),
);
// Tabla Localities
export const localities = t.pgTable(
'localities',
{
id: t.serial('id').primaryKey(),
stateId: t
.integer('state_id')
.notNull()
.references(() => states.id, { onDelete: 'cascade' }),
municipalityId: t
.integer('municipality_id')
.notNull()
.references(() => municipalities.id, { onDelete: 'cascade' }),
parishId: t
.integer('parish_id')
.notNull()
.references(() => parishes.id, { onDelete: 'cascade' }),
name: t.text('name').unique().notNull(),
...timestamps,
},
(localities) => ({
uniqueLocalityIndex: t
.uniqueIndex('localities_index_03')
.on(localities.stateId, localities.municipalityId, localities.parishId),
stateIndex: t.index('localities_index_00').on(localities.stateId),
municipalityIndex: t
.index('localities_index_01')
.on(localities.municipalityId),
parishIndex: t.index('localities_index_02').on(localities.parishId),
}),
);
// // Vista LocalitiesView
// export const localitiesView = t.pgView("localities_view", {
// id: t.integer("id").notNull(),
// stateId: t.integer("state_id"),
// state: t.text("state"),
// municipalityId: t.integer("municipality_id"),
// municipality: t.text("municipality"),
// parishId: t.integer("parish_id"),
// parish: t.text("parish"),
// fullLocation: t.text("full_location"),
// });

View File

@@ -0,0 +1,4 @@
//schemas
import * as t from 'drizzle-orm/pg-core';
export const authSchema = t.pgSchema('auth'); //autenticacion y sessiones usuarios

View File

@@ -0,0 +1,57 @@
import * as t from 'drizzle-orm/pg-core';
import { eq, lt, gte, ne, sql } from 'drizzle-orm';
import { timestamps } from '../timestamps';
import { users } from './auth';
// Tabla surveys
export const surveys = t.pgTable(
'surveys',
{
id: t.serial('id').primaryKey(),
title: t.text('title').notNull(),
description: t.text('description').notNull(),
targetAudience: t.varchar('target_audience', { length: 50 }).notNull(),
closingDate: t.date('closing_date'),
published: t.boolean('published').notNull(),
questions: t.jsonb('questions').notNull(),
...timestamps,
},
(surveys) => ({
surveysIndex: t
.index('surveys_index_00')
.on(surveys.title),
}),
);
export const answersSurveys = t.pgTable(
'answers_surveys',
{
id: t.serial('id').primaryKey(),
surveyId: t
.integer('survey_id')
.references(() => surveys.id, { onDelete: 'cascade' }),
userId: t
.integer('user_id')
.references(() => users.id, { onDelete: 'cascade' }),
answers: t.jsonb('answers').notNull(),
...timestamps,
},
(answers) => ({
answersIndex: t.index('answers_index_00').on(answers.answers),
answersIndex01: t.index('answers_index_01').on(answers.surveyId),
answersIndex02: t.index('answers_index_02').on(answers.userId),
}),
);
export const viewSurveys = t.pgView('v_surveys', {
surverId: t.integer('survey_id'),
title: t.text('title'),
description: t.text('description'),
created_at: t.timestamp('created_at'),
closingDate: t.date('closing_date'),
targetAudience: t.varchar('target_audience')
}).as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
where published = true`);

View File

@@ -0,0 +1,24 @@
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../index';
import { roles, } from '../index';
export async function seedAdminRole(db: NodePgDatabase<typeof schema>) {
console.log('Seeding admin role...');
// Insert roles
const roleNames = ['superadmin', 'admin', 'autoridad','manager','user','producers','organization'];
for (const roleName of roleNames) {
try {
await db.insert(roles).values({
name: roleName
}).onConflictDoNothing();
console.log(`Role '${roleName}' created or already exists`);
} catch (error) {
console.error(`Error creating role '${roleName}':`, error);
}
}
console.log('roles seeded successfully');
}

View File

@@ -0,0 +1,37 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { envs } from 'src/common/config/envs';
import * as schema from '../index';
import { seedAdminRole } from './admin-role.seed';
import { seedUserAdmin } from './user-admin.seed';
import { seedStates } from './states.seed';
import { seedMunicipalities } from './municipalities.seed';
import { seedParishes } from './parishes.seed';
async function main() {
const pool = new Pool({
connectionString: envs.dataBaseUrl,
ssl:
envs.node_env === 'production' ? { rejectUnauthorized: false } : false,
});
const db = drizzle(pool, { schema });
try {
// Run seeds in order
await seedStates(db);
await seedMunicipalities(db);
await seedParishes(db);
await seedAdminRole(db);
await seedUserAdmin(db)
console.log('All seeds completed successfully');
} catch (error) {
console.error('Error seeding database:', error);
} finally {
await pool.end();
}
}
main();

View File

@@ -0,0 +1,25 @@
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../index';
import { municipalities } from '../schema/general';
export async function seedMunicipalities(db: NodePgDatabase<typeof schema>) {
console.log('Seeding public municipalities...');
// Insert roles
const municipalitiesArray = [{name:'municipio1',stateId:1}, {name:'municipio2',stateId:1}, {name:'municipio3',stateId:2}];
for (const item of municipalitiesArray) {
try {
await db.insert(municipalities).values({
name: item.name,
stateId: item.stateId
}).onConflictDoNothing();
// console.log(`Municipality '${item}' created or already exists`);
} catch (error) {
console.error(`Error creating municipality '${item.name}':`, error);
}
}
console.log('All municipalities seeded successfully');
}

View File

@@ -0,0 +1,25 @@
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../index';
import { parishes } from '../schema/general';
export async function seedParishes(db: NodePgDatabase<typeof schema>) {
console.log('Seeding public parishes...');
// Insert roles
const parishesArray = [{name:'parroquia1',municipalityId:1}, {name:'parroquia2',municipalityId:1}, {name:'parroquia3',municipalityId:2}];
for (const item of parishesArray) {
try {
await db.insert(parishes).values({
name: item.name,
municipalityId: item.municipalityId
}).onConflictDoNothing();
// console.log(`Parish '${item}' created or already exists`);
} catch (error) {
console.error(`Error creating parish '${item.name}':`, error);
}
}
console.log('All parishes seeded successfully');
}

View File

@@ -0,0 +1,24 @@
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../index';
import { states } from '../schema/general';
export async function seedStates(db: NodePgDatabase<typeof schema>) {
console.log('Seeding public state...');
// Insert roles
const statesArray = ['estado1', 'estado2', 'estado3'];
for (const item of statesArray) {
try {
await db.insert(states).values({
name: item
}).onConflictDoNothing();
// console.log(`State '${item}' created or already exists`);
} catch (error) {
console.error(`Error creating state '${item}':`, error);
}
}
console.log('All states seeded successfully');
}

View File

@@ -0,0 +1,39 @@
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../index';
import { users, usersRole } from '../index';
export async function seedUserAdmin(db: NodePgDatabase<typeof schema>) {
// Insert admin user
try {
// Password is already hashed in your SQL, but in a real application you might want to hash it here
// const hashedPassword = await hash('your_password', 10);
const hashedPassword = '$2b$10$6esl7d/BOINamScuReRoPuYFC8iSJgpk61LHm2X3PCU5hu/St8vHW';
const [adminUser] = await db.insert(users).values({
username: 'superadmin',
email: 'admin@zonastart.com',
fullname: 'Super Administrador',
password: hashedPassword,
state: 1,
municipality: 1,
parish: 1,
isTwoFactorEnabled: false,
isEmailVerified: true,
isActive: true
}).returning({ id: users.id }).onConflictDoNothing();
if (adminUser) {
// Assign superadmin role to the user
await db.insert(usersRole).values({
roleId: 1, // Assuming 'superadmin' has ID 1 based on the insert order
userId: adminUser.id
}).onConflictDoNothing();
console.log('Admin user created and assigned superadmin role');
} else {
console.log('Admin user already exists, skipping');
}
} catch (error) {
console.error('Error creating admin user:', error);
}
}

View File

@@ -0,0 +1,8 @@
import * as t from 'drizzle-orm/pg-core';
export const timestamps = {
created_at: t.timestamp('created_at').defaultNow().notNull(),
updated_at: t
.timestamp('updated_at', { mode: 'date', precision: 3 })
.$onUpdate(() => new Date()),
};

View File

@@ -0,0 +1,13 @@
import type { Config } from 'drizzle-kit';
import { envs } from './common/config/envs';
export default {
schema: './src/database/schema/*', // Path to schema file
out: './src/database/migrations', // Path to output directory
dialect: 'postgresql', // Database dialect
schemaFilter: ["public", "auth"],
dbCredentials: {
url: envs.dataBaseUrl,
},
} satisfies Config;

View File

@@ -0,0 +1,61 @@
import { Public } from '@/common/decorators';
import { JwtRefreshGuard } from '@/common/guards/jwt-refresh.guard';
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
import {
Body,
Controller,
HttpCode,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@HttpCode(200)
@Post('sing-up')
// @ApiOperation({ summary: 'Create a new user' })
// @ApiResponse({ status: 201, description: 'User created successfully.' })
async singUp(@Body() payload: SingUpUserDto) {
const data = await this.authService.singUp(payload)
return { message: 'User created successfully', data};
// return { message: 'User created successfully', data };
}
@Public()
@HttpCode(200)
@Post('sign-in')
async signIn(@Body() signInUserDto: SignInUserDto) {
return await this.authService.signIn(signInUserDto);
}
@Post('sign-out')
//@RequirePermissions('auth:sign-out')
async signOut(@Body() signOutUserDto: SignOutUserDto) {
await this.authService.signOut(signOutUserDto);
return { message: 'User signed out successfully' };
}
// @Post('forgot-password')
// async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
// await this.authService.forgotPassword(forgotPasswordDto);
// return { message: 'Password reset link sent to your email' };
// }
@UseGuards(JwtRefreshGuard)
@Patch('refresh-token')
//@RequirePermissions('auth:refresh-token')
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return await this.authService.refreshToken(refreshTokenDto);
}
}

View File

@@ -0,0 +1,12 @@
import { MailModule } from '@/features/mail/mail.module';
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { DrizzleModule } from '@/database/drizzle.module';
@Module({
imports: [DrizzleModule, MailModule],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,368 @@
import { envs } from '@/common/config/envs';
import { Env, validateString } from '@/common/utils';
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
import {
LoginUserInterface,
Roles,
} from '@/features/auth/interfaces/login-user.interface';
import RefreshTokenInterface from '@/features/auth/interfaces/refresh-token.interface';
import { MailService } from '@/features/mail/mail.service';
import { User } from '@/features/users/entities/user.entity';
import {
HttpException,
HttpStatus,
Inject,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import crypto from 'crypto';
import { and, eq, or } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from 'src/database/index';
import { sessions, users, roles, usersRole } from 'src/database/index';
import { Session } from './interfaces/session.interface';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly config: ConfigService<Env>,
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private readonly mailService: MailService,
) {}
//Decode Tokens
// Método para decodificar el token y obtener los datos completos
private decodeToken(token: string): {
sub: number;
username?: string;
iat: number;
exp: number;
} {
try {
const decoded = this.jwtService.decode(token) as {
sub: number;
username?: string;
iat: number;
exp: number;
};
// Validar que contiene los datos esenciales
if (!decoded || !decoded.exp || !decoded.iat) {
throw new Error('Token lacks required fields');
}
return decoded;
} catch (error) {
// Manejo seguro del tipo unknown
let errorMessage = 'Failed to decode token';
if (error instanceof Error) {
errorMessage = error.message;
console.error('Error decoding token:', errorMessage);
} else {
console.error('Unknown error type:', error);
}
throw new HttpException(errorMessage, HttpStatus.UNAUTHORIZED);
}
}
//Generate Tokens
async generateTokens(user: User): Promise<AuthTokensInterface> {
const [access_token, refresh_token] = await Promise.all([
this.jwtService.signAsync(
{
sub: user.id,
username: user.username,
},
{
secret: envs.access_token_secret,
expiresIn: envs.access_token_expiration,
},
),
this.jwtService.signAsync(
{
sub: user.id,
username: user.username,
},
{
secret: envs.refresh_token_secret,
expiresIn: envs.refresh_token_expiration,
},
),
]);
return {
access_token,
refresh_token,
};
}
//Generate OTP Code For Email Confirmation
async generateOTP(length = 6): Promise<string> {
return crypto
.randomInt(0, 10 ** length)
.toString()
.padStart(length, '0');
}
// metodo para crear una session
private async createSession(sessionInput: Session): Promise<string> {
const { userId } = sessionInput;
const activeSessionsCount = await this.drizzle
.select()
.from(sessions)
.where(eq(sessions.userId, parseInt(userId)));
if (activeSessionsCount.length !== 0) {
// Elimina sessiones viejsas
await this.drizzle
.delete(sessions)
.where(eq(sessions.userId, parseInt(userId)));
}
const session = await this.drizzle.insert(sessions).values({
sessionToken: sessionInput.sessionToken,
userId: parseInt(userId),
expiresAt: sessionInput.expiresAt,
});
if (session.rowCount === 0) throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
return 'Session created successfully';
}
//Find User
async findUser(username: string): Promise<User | null> {
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.username, username));
return user[0];
}
//Find User
async findUserById(id: number): Promise<User | null> {
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.id, id));
return user[0];
}
//Check User Is Already Exists
async validateUser(dto: ValidateUserDto): Promise<User> {
const user = await this.findUser(dto.username);
if (!user) throw new NotFoundException('User not found');
const isValid = await validateString(
dto.password,
user?.password as string,
);
if (!isValid) throw new UnauthorizedException('Invalid credentials');
return user;
}
//Find rol user
async findUserRol(id: number): Promise<Roles[]> {
const roles = await this.drizzle
.select({
id: schema.roles.id,
role: schema.roles.name,
})
.from(schema.usersRole)
.leftJoin(schema.roles, eq(schema.roles.id, schema.usersRole.roleId))
.where(eq(schema.usersRole.userId, id));
if (roles.length === 0) {
throw new NotFoundException('User not found');
}
// Aseguramos que no haya valores nulos
return roles.map((role) => ({
id: role.id ?? 0, // Asignamos un valor por defecto (0) si es null
rol: role.role ?? '', // Asignamos un valor por defecto (cadena vacía) si es null
}));
}
//Sign In User Account
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
const user = await this.validateUser(dto);
const tokens = await this.generateTokens(user);
const decodeAccess = this.decodeToken(tokens.access_token);
const decodeRefresh = this.decodeToken(tokens.refresh_token);
const rol = await this.findUserRol(user?.id as number);
await this.createSession({
userId: String(user?.id), // Convert number to string
sessionToken: tokens.refresh_token,
expiresAt: decodeRefresh.exp,
});
return {
message: 'User signed in successfully',
user: {
id: user?.id as number,
username: user?.username,
fullname: user?.fullname,
email: user?.email,
rol: rol,
},
tokens: {
access_token: tokens.access_token,
access_expire_in: decodeAccess.exp,
refresh_token: tokens.refresh_token,
refresh_expire_in: decodeRefresh.exp,
},
};
}
// //Forgot Password
// async forgotPassword(dto: ForgotPasswordDto): Promise<void> {
// const user = await this.findUser(dto.username);
// if (!user) throw new NotFoundException('User not found');
// const passwordResetToken = await this.generateOTP();
// user.passwordResetToken = passwordResetToken;
// user.passwordResetTokenExpires = new Date(
// Date.now() + 1000 * 60 * 60 * 24, // 1 day
// );
// await this.UserRepository.save(user);
// await this.mailService.sendEmail({
// to: [user.email],
// subject: 'Reset Password',
// html: ForgotPasswordMail({
// name: user.name,
// code: passwordResetToken,
// }),
// });
// }
//Sign Out User Account
async signOut(dto: SignOutUserDto): Promise<void> {
const { user_id } = dto;
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.id, parseInt(user_id)));
if (!user) throw new NotFoundException('User not found');
await this.drizzle
.delete(sessions)
.where(eq(sessions.userId, parseInt(user_id)));
}
//Refresh User Access Token
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
const { user_id } = dto;
const session = await this.drizzle
.select()
.from(sessions)
.where(
and(
eq(sessions.userId, user_id) &&
eq(sessions.sessionToken, dto.refresh_token),
),
);
if (session.length === 0) throw new NotFoundException('session not found');
const user = await this.findUserById(dto.user_id);
if (!user) throw new NotFoundException('User not found');
const tokens = await this.generateTokens(user);
const decodeAccess = this.decodeToken(tokens.access_token);
const decodeRefresh = this.decodeToken(tokens.refresh_token);
await this.drizzle
.update(sessions)
.set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp })
.where(eq(sessions.userId, dto.user_id));
return {
access_token: tokens.access_token,
access_expire_in: decodeAccess.exp,
refresh_token: tokens.refresh_token,
refresh_expire_in: decodeRefresh.exp,
};
}
async singUp(createUserDto: SingUpUserDto): Promise<User> {
// Check if username or email exists
const data = await this.drizzle
.select({
id: users.id,
username: users.username,
email: users.email
})
.from(users)
.where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email)));
if (data.length > 0) {
if (data[0].username === createUserDto.username) {
throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST);
}
if (data[0].email === createUserDto.email) {
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
}
}
// Hash the password
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// Start a transaction
return await this.drizzle.transaction(async (tx) => {
// Create the user
const [newUser] = await tx
.insert(users)
.values({
username: createUserDto.username,
email: createUserDto.email,
password: hashedPassword,
fullname: createUserDto.fullname,
isActive: true,
state: createUserDto.state,
municipality: createUserDto.municipality,
parish: createUserDto.parish,
phone: createUserDto.phone,
isEmailVerified: false,
isTwoFactorEnabled: false,
})
.returning();
// check if user role is admin
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
// Assign role to user
await tx.insert(usersRole).values({
userId: newUser.id,
roleId: role,
});
// Return the created user with role
const [userWithRole] = await tx
.select({
id: users.id,
username: users.username,
email: users.email,
fullname: users.fullname,
phone: users.phone,
isActive: users.isActive,
role: roles.name,
})
.from(users)
.leftJoin(usersRole, eq(usersRole.userId, users.id))
.leftJoin(roles, eq(roles.id, usersRole.roleId))
.where(eq(users.id, newUser.id));
return userWithRole;
})
}
}

View File

@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ChangePasswordDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
@ApiProperty()
@IsString({
message: 'New password must be a string',
})
newPassword: string;
}

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator';
export class ConfirmEmailDto {
@ApiProperty()
@IsString()
@MaxLength(6)
@MinLength(6)
code: string;
@ApiProperty()
@IsEmail()
email: string;
}

View File

@@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsOptional, IsString } from 'class-validator';
export class CreateUserDto {
@ApiProperty()
@IsEmail()
username: string;
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsEmail()
fullname: string;
@ApiProperty()
@IsString({
message: 'Phone must be a string',
})
@IsOptional()
phone: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ForgotPasswordDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
}

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class RefreshTokenDto {
@ApiProperty()
@IsString({
message: 'Refresh token must be a string',
})
refresh_token: string;
@ApiProperty()
@IsNumber()
user_id: number;
}

View File

@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ResetPasswordDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Reset Token must be a string',
})
resetToken: string;
@ApiProperty()
@IsString({
message: 'New password must be a string',
})
newPassword: string;
}

View File

@@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class SignInUserDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class SignOutUserDto {
@ApiProperty()
@IsString({
message: 'User Id must be a string',
})
user_id: string;
}

View File

@@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsInt, IsOptional, IsString } from 'class-validator';
export class SingUpUserDto {
@ApiProperty()
@IsString()
username: string;
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsString()
fullname: string;
@ApiProperty()
@IsString({
message: 'Phone must be a string',
})
@IsOptional()
phone: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
@ApiProperty()
@IsInt()
state: number;
@ApiProperty()
@IsInt()
municipality: number;
@ApiProperty()
@IsInt()
parish: number;
@ApiProperty()
@IsInt()
role: number;
}

View File

@@ -0,0 +1,6 @@
import { User } from '@/features/users/entities/user.entity';
export class UpdateRefreshTokenDto {
user: User;
refresh_token: string;
}

View File

@@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ValidateUserDto {
@ApiProperty()
@IsString({
message: 'First name must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
}

View File

@@ -0,0 +1,6 @@
interface AuthTokensInterface {
access_token: string;
refresh_token: string;
}
export default AuthTokensInterface;

View File

@@ -0,0 +1,25 @@
export interface LoginUserInterface {
message: string;
user: User;
tokens: Tokens;
}
interface User {
id: number;
username: string;
fullname: string;
email?: string;
rol: Roles[];
}
export interface Roles {
id: number;
rol: string;
}
interface Tokens {
access_token: string;
access_expire_in: number;
refresh_token: string;
refresh_expire_in: number;
}

View File

@@ -0,0 +1,8 @@
interface RefreshTokenInterface {
access_token: string;
access_expire_in: number;
refresh_token: string;
refresh_expire_in: number;
}
export default RefreshTokenInterface;

View File

@@ -0,0 +1,5 @@
export interface Session {
userId: string;
sessionToken: string;
expiresAt: number;
}

View File

@@ -0,0 +1,96 @@
import { Roles } from '@/common/decorators';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CategoryTypesService } from './category-types.service';
import { CreateCategoryTypeDto } from './dto/create-category-type.dto';
import { UpdateCategoryTypeDto } from './dto/update-category-type.dto';
import { CategoryType } from './entities/category-type.entity';
@ApiTags('Category Types')
@Controller('configurations/category-types')
export class CategoryTypesController {
constructor(private readonly categoryTypesService: CategoryTypesService) {}
@Get()
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get all category types' })
@ApiResponse({
status: 200,
description: 'Return all category types',
type: [CategoryType],
})
findAll() {
return this.categoryTypesService.findAll();
}
@Get(':id')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get a category type by id' })
@ApiResponse({
status: 200,
description: 'Return a category type',
type: CategoryType,
})
@ApiResponse({ status: 404, description: 'Category type not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.categoryTypesService.findOne(id);
}
@Get('group/:group')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get category types by group' })
@ApiResponse({
status: 200,
description: 'Return category types by group',
type: [CategoryType],
})
findByGroup(@Param('group') group: string) {
return this.categoryTypesService.findByGroup(group);
}
@Post()
@Roles('admin')
@ApiOperation({ summary: 'Create a new category type' })
@ApiResponse({
status: 201,
description: 'Category type created',
type: CategoryType,
})
create(@Body() createCategoryTypeDto: CreateCategoryTypeDto) {
return this.categoryTypesService.create(createCategoryTypeDto);
}
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: 'Update a category type' })
@ApiResponse({
status: 200,
description: 'Category type updated',
type: CategoryType,
})
@ApiResponse({ status: 404, description: 'Category type not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateCategoryTypeDto: UpdateCategoryTypeDto,
) {
return this.categoryTypesService.update(id, updateCategoryTypeDto);
}
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: 'Delete a category type' })
@ApiResponse({ status: 200, description: 'Category type deleted' })
@ApiResponse({ status: 404, description: 'Category type not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.categoryTypesService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { DrizzleModule } from '@/database/drizzle.module';
import { Module } from '@nestjs/common';
import { CategoryTypesController } from './category-types.controller';
import { CategoryTypesService } from './category-types.service';
@Module({
imports: [DrizzleModule],
controllers: [CategoryTypesController],
providers: [CategoryTypesService],
exports: [CategoryTypesService],
})
export class CategoryTypesModule {}

View File

@@ -0,0 +1,81 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import * as schema from '@/database/index';
import { categoryType } from '@/database/schema/general';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { CreateCategoryTypeDto } from './dto/create-category-type.dto';
import { UpdateCategoryTypeDto } from './dto/update-category-type.dto';
import { CategoryType } from './entities/category-type.entity';
@Injectable()
export class CategoryTypesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) {}
async findAll(): Promise<CategoryType[]> {
return await this.drizzle.select().from(categoryType);
}
async findOne(id: number): Promise<CategoryType> {
const category = await this.drizzle
.select()
.from(categoryType)
.where(eq(categoryType.id, id));
if (category.length === 0) {
throw new HttpException('Category type not found', HttpStatus.NOT_FOUND);
}
return category[0];
}
async findByGroup(group: string): Promise<CategoryType[]> {
return await this.drizzle
.select()
.from(categoryType)
.where(eq(categoryType.group, group));
}
async create(
createCategoryTypeDto: CreateCategoryTypeDto,
): Promise<CategoryType> {
const [category] = await this.drizzle
.insert(categoryType)
.values({
group: createCategoryTypeDto.group,
description: createCategoryTypeDto.description,
})
.returning();
return category;
}
async update(
id: number,
updateCategoryTypeDto: UpdateCategoryTypeDto,
): Promise<CategoryType> {
// Check if category type exists
await this.findOne(id);
await this.drizzle
.update(categoryType)
.set({
group: updateCategoryTypeDto.group,
description: updateCategoryTypeDto.description,
})
.where(eq(categoryType.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if category type exists
await this.findOne(id);
await this.drizzle.delete(categoryType).where(eq(categoryType.id, id));
return { message: 'Category type deleted successfully' };
}
}

View File

@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
export class CreateCategoryTypeDto {
@ApiProperty({
description: 'The group of the category type',
example: 'PAYROLL_TYPE',
})
@IsNotEmpty()
@IsString()
group: string;
@ApiProperty({
description: 'The description of the category type',
example: 'Quincenal',
})
@IsNotEmpty()
@IsString()
description: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCategoryTypeDto } from './create-category-type.dto';
export class UpdateCategoryTypeDto extends PartialType(CreateCategoryTypeDto) {}

View File

@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';
export class CategoryType {
@ApiProperty({
description: 'The unique identifier of the category type',
example: 1,
})
id: number;
@ApiProperty({
description: 'The group of the category type',
example: 'PAYROLL_TYPE',
})
group: string;
@ApiProperty({
description: 'The description of the category type',
example: 'Quincenal',
})
description: string;
@ApiProperty({
description: 'The date when the category type was created',
example: '2023-01-01T00:00:00.000Z',
})
created_at?: Date;
@ApiProperty({
description: 'The date when the category type was last updated',
example: '2023-01-01T00:00:00.000Z',
})
updated_at?: Date | null;
}

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { CategoryTypesModule } from './category-types/category-types.module';
import { MunicipalitiesModule } from './municipalities/municipalities.module';
import { ParishesModule } from './parishes/parishes.module';
import { StatesModule } from './states/states.module';
@Module({
imports: [
StatesModule,
MunicipalitiesModule,
ParishesModule,
CategoryTypesModule,
],
})
export class ConfigurationsModule {}

View File

@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreateMunicipalityDto {
@ApiProperty({
description: 'The name of the municipality',
example: 'Los Angeles',
})
@IsNotEmpty()
@IsString()
name: string;
@ApiProperty({
description: 'The ID of the state this municipality belongs to',
example: 1,
})
@IsNotEmpty()
@IsNumber()
stateId: number;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateMunicipalityDto } from './create-municipality.dto';
export class UpdateMunicipalityDto extends PartialType(CreateMunicipalityDto) {}

View File

@@ -0,0 +1,39 @@
import { ApiProperty } from '@nestjs/swagger';
export class Municipality {
@ApiProperty({
description: 'The unique identifier of the municipality',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the municipality',
example: 'Los Angeles',
})
name: string;
@ApiProperty({
description: 'The ID of the state this municipality belongs to',
example: 1,
})
stateId: number;
@ApiProperty({
description: 'The name of the state this municipality belongs to',
example: 'California',
})
stateName?: string | null;
@ApiProperty({
description: 'The date when the municipality was created',
example: '2023-01-01T00:00:00.000Z',
})
created_at?: Date | null;
@ApiProperty({
description: 'The date when the municipality was last updated',
example: '2023-01-01T00:00:00.000Z',
})
updated_at?: Date | null;
}

View File

@@ -0,0 +1,97 @@
import { Roles } from '@/common/decorators';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateMunicipalityDto } from './dto/create-municipality.dto';
import { UpdateMunicipalityDto } from './dto/update-municipality.dto';
import { Municipality } from './entities/municipality.entity';
import { MunicipalitiesService } from './municipalities.service';
@ApiTags('Municipalities')
@Controller('configurations/municipalities')
export class MunicipalitiesController {
constructor(private readonly municipalitiesService: MunicipalitiesService) {}
@Get()
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get all municipalities' })
@ApiResponse({
status: 200,
description: 'Return all municipalities',
type: [Municipality],
})
findAll() {
return this.municipalitiesService.findAll();
}
@Get(':id')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get a municipality by id' })
@ApiResponse({
status: 200,
description: 'Return a municipality',
type: Municipality,
})
@ApiResponse({ status: 404, description: 'Municipality not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.municipalitiesService.findOne(id);
}
@Get('state/:stateId')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get municipalities by state id' })
@ApiResponse({
status: 200,
description: 'Return municipalities by state',
type: [Municipality],
})
@ApiResponse({ status: 404, description: 'State not found' })
findByState(@Param('stateId', ParseIntPipe) stateId: number) {
return this.municipalitiesService.findByState(stateId);
}
@Post()
@Roles('ADMIN')
@ApiOperation({ summary: 'Create a new municipality' })
@ApiResponse({
status: 201,
description: 'Municipality created',
type: Municipality,
})
create(@Body() createMunicipalityDto: CreateMunicipalityDto) {
return this.municipalitiesService.create(createMunicipalityDto);
}
@Patch(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Update a municipality' })
@ApiResponse({
status: 200,
description: 'Municipality updated',
type: Municipality,
})
@ApiResponse({ status: 404, description: 'Municipality not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateMunicipalityDto: UpdateMunicipalityDto,
) {
return this.municipalitiesService.update(id, updateMunicipalityDto);
}
@Delete(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Delete a municipality' })
@ApiResponse({ status: 200, description: 'Municipality deleted' })
@ApiResponse({ status: 404, description: 'Municipality not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.municipalitiesService.remove(id);
}
}

View File

@@ -0,0 +1,13 @@
import { DrizzleModule } from '@/database/drizzle.module';
import { Module } from '@nestjs/common';
import { StatesModule } from '../states/states.module';
import { MunicipalitiesController } from './municipalities.controller';
import { MunicipalitiesService } from './municipalities.service';
@Module({
imports: [DrizzleModule, StatesModule],
controllers: [MunicipalitiesController],
providers: [MunicipalitiesService],
exports: [MunicipalitiesService],
})
export class MunicipalitiesModule {}

View File

@@ -0,0 +1,120 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import * as schema from '@/database/index';
import { municipalities, states } from '@/database/schema/general';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { StatesService } from '../states/states.service';
import { CreateMunicipalityDto } from './dto/create-municipality.dto';
import { UpdateMunicipalityDto } from './dto/update-municipality.dto';
import { Municipality } from './entities/municipality.entity';
@Injectable()
export class MunicipalitiesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private statesService: StatesService,
) {}
async findAll(): Promise<Municipality[]> {
return await this.drizzle
.select({
id: municipalities.id,
name: municipalities.name,
stateId: municipalities.stateId,
stateName: states.name,
created_at: municipalities.created_at,
updated_at: municipalities.updated_at,
})
.from(municipalities)
.leftJoin(states, eq(municipalities.stateId, states.id));
}
async findOne(id: number): Promise<Municipality> {
const municipality = await this.drizzle
.select({
id: municipalities.id,
name: municipalities.name,
stateId: municipalities.stateId,
stateName: states.name,
created_at: municipalities.created_at,
updated_at: municipalities.updated_at,
})
.from(municipalities)
.leftJoin(states, eq(municipalities.stateId, states.id))
.where(eq(municipalities.id, id));
if (municipality.length === 0) {
throw new HttpException('Municipality not found', HttpStatus.NOT_FOUND);
}
return municipality[0];
}
async findByState(stateId: number): Promise<Municipality[]> {
// Verify state exists
await this.statesService.findOne(stateId);
return await this.drizzle
.select({
id: municipalities.id,
name: municipalities.name,
stateId: municipalities.stateId,
stateName: states.name,
created_at: municipalities.created_at,
updated_at: municipalities.updated_at,
})
.from(municipalities)
.leftJoin(states, eq(municipalities.stateId, states.id))
.where(eq(municipalities.stateId, stateId));
}
async create(
createMunicipalityDto: CreateMunicipalityDto,
): Promise<Municipality> {
// Verify state exists
await this.statesService.findOne(createMunicipalityDto.stateId);
const [municipality] = await this.drizzle
.insert(municipalities)
.values({
name: createMunicipalityDto.name,
stateId: createMunicipalityDto.stateId,
})
.returning();
return this.findOne(municipality.id);
}
async update(
id: number,
updateMunicipalityDto: UpdateMunicipalityDto,
): Promise<Municipality> {
// Check if municipality exists
await this.findOne(id);
// If stateId is provided, verify it exists
if (updateMunicipalityDto.stateId) {
await this.statesService.findOne(updateMunicipalityDto.stateId);
}
await this.drizzle
.update(municipalities)
.set({
name: updateMunicipalityDto.name,
stateId: updateMunicipalityDto.stateId,
})
.where(eq(municipalities.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if municipality exists
await this.findOne(id);
await this.drizzle.delete(municipalities).where(eq(municipalities.id, id));
return { message: 'Municipality deleted successfully' };
}
}

View File

@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreateParishDto {
@ApiProperty({
description: 'The name of the parish',
example: 'Downtown',
})
@IsNotEmpty()
@IsString()
name: string;
@ApiProperty({
description: 'The ID of the municipality this parish belongs to',
example: 1,
})
@IsNotEmpty()
@IsNumber()
municipalityId: number;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateParishDto } from './create-parish.dto';
export class UpdateParishDto extends PartialType(CreateParishDto) {}

View File

@@ -0,0 +1,39 @@
import { ApiProperty } from '@nestjs/swagger';
export class Parish {
@ApiProperty({
description: 'The unique identifier of the parish',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the parish',
example: 'Downtown',
})
name: string;
@ApiProperty({
description: 'The ID of the municipality this parish belongs to',
example: 1,
})
municipalityId: number;
@ApiProperty({
description: 'The name of the municipality this parish belongs to',
example: 'Los Angeles',
})
municipalityName?: string | null;
@ApiProperty({
description: 'The date when the parish was created',
example: '2023-01-01T00:00:00.000Z',
})
created_at?: Date | null;
@ApiProperty({
description: 'The date when the parish was last updated',
example: '2023-01-01T00:00:00.000Z',
})
updated_at: Date | null;
}

View File

@@ -0,0 +1,99 @@
import { Roles } from '@/common/decorators';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateParishDto } from './dto/create-parish.dto';
import { UpdateParishDto } from './dto/update-parish.dto';
import { Parish } from './entities/parish.entity';
import { ParishesService } from './parishes.service';
@ApiTags('Parishes')
@Controller('configurations/parishes')
export class ParishesController {
constructor(private readonly parishesService: ParishesService) {}
@Get()
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get all parishes' })
@ApiResponse({
status: 200,
description: 'Return all parishes',
type: [Parish],
})
findAll() {
return this.parishesService.findAll();
}
@Get(':id')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get a parish by id' })
@ApiResponse({
status: 200,
description: 'Return a parish',
type: Parish,
})
@ApiResponse({ status: 404, description: 'Parish not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.parishesService.findOne(id);
}
@Get('municipality/:municipalityId')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get parishes by municipality id' })
@ApiResponse({
status: 200,
description: 'Return parishes by municipality',
type: [Parish],
})
@ApiResponse({ status: 404, description: 'Municipality not found' })
findByMunicipality(
@Param('municipalityId', ParseIntPipe) municipalityId: number,
) {
return this.parishesService.findByMunicipality(municipalityId);
}
@Post()
@Roles('ADMIN')
@ApiOperation({ summary: 'Create a new parish' })
@ApiResponse({
status: 201,
description: 'Parish created',
type: Parish,
})
create(@Body() createParishDto: CreateParishDto) {
return this.parishesService.create(createParishDto);
}
@Patch(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Update a parish' })
@ApiResponse({
status: 200,
description: 'Parish updated',
type: Parish,
})
@ApiResponse({ status: 404, description: 'Parish not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateParishDto: UpdateParishDto,
) {
return this.parishesService.update(id, updateParishDto);
}
@Delete(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Delete a parish' })
@ApiResponse({ status: 200, description: 'Parish deleted' })
@ApiResponse({ status: 404, description: 'Parish not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.parishesService.remove(id);
}
}

View File

@@ -0,0 +1,13 @@
import { DrizzleModule } from '@/database/drizzle.module';
import { Module } from '@nestjs/common';
import { MunicipalitiesModule } from '../municipalities/municipalities.module';
import { ParishesController } from './parishes.controller';
import { ParishesService } from './parishes.service';
@Module({
imports: [DrizzleModule, MunicipalitiesModule],
controllers: [ParishesController],
providers: [ParishesService],
exports: [ParishesService],
})
export class ParishesModule {}

View File

@@ -0,0 +1,115 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import * as schema from '@/database/index';
import { municipalities, parishes } from '@/database/schema/general';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { MunicipalitiesService } from '../municipalities/municipalities.service';
import { CreateParishDto } from './dto/create-parish.dto';
import { UpdateParishDto } from './dto/update-parish.dto';
import { Parish } from './entities/parish.entity';
@Injectable()
export class ParishesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private municipalitiesService: MunicipalitiesService,
) {}
async findAll(): Promise<Parish[]> {
return await this.drizzle
.select({
id: parishes.id,
name: parishes.name,
municipalityId: parishes.municipalityId,
municipalityName: municipalities.name,
created_at: parishes.created_at,
updated_at: parishes.updated_at,
})
.from(parishes)
.leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id));
}
async findOne(id: number): Promise<Parish> {
const parish = await this.drizzle
.select({
id: parishes.id,
name: parishes.name,
municipalityId: parishes.municipalityId,
municipalityName: municipalities.name,
created_at: parishes.created_at,
updated_at: parishes.updated_at,
})
.from(parishes)
.leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id))
.where(eq(parishes.id, id));
if (parish.length === 0) {
throw new HttpException('Parish not found', HttpStatus.NOT_FOUND);
}
return parish[0];
}
async findByMunicipality(municipalityId: number): Promise<Parish[]> {
// Verify municipality exists
await this.municipalitiesService.findOne(municipalityId);
return await this.drizzle
.select({
id: parishes.id,
name: parishes.name,
municipalityId: parishes.municipalityId,
municipalityName: municipalities.name,
created_at: parishes.created_at,
updated_at: parishes.updated_at,
})
.from(parishes)
.leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id))
.where(eq(parishes.municipalityId, municipalityId));
}
async create(createParishDto: CreateParishDto): Promise<Parish> {
// Verify municipality exists
await this.municipalitiesService.findOne(createParishDto.municipalityId);
const [parish] = await this.drizzle
.insert(parishes)
.values({
name: createParishDto.name,
municipalityId: createParishDto.municipalityId,
})
.returning();
return this.findOne(parish.id);
}
async update(id: number, updateParishDto: UpdateParishDto): Promise<Parish> {
// Check if parish exists
await this.findOne(id);
// If municipalityId is provided, verify it exists
if (updateParishDto.municipalityId) {
await this.municipalitiesService.findOne(updateParishDto.municipalityId);
}
await this.drizzle
.update(parishes)
.set({
name: updateParishDto.name,
municipalityId: updateParishDto.municipalityId,
})
.where(eq(parishes.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if parish exists
await this.findOne(id);
await this.drizzle.delete(parishes).where(eq(parishes.id, id));
return { message: 'Parish deleted successfully' };
}
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateStateDto {
@ApiProperty({
description: 'The name of the state',
example: 'California',
})
@IsNotEmpty()
@IsString()
name: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateStateDto } from './create-state.dto';
export class UpdateStateDto extends PartialType(CreateStateDto) {}

Some files were not shown because too many files have changed in this diff Show More