base con autenticacion, registro, modulo encuestas
This commit is contained in:
1
.commitlintrc.ts
Normal file
1
.commitlintrc.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default { extends: ['@commitlint/config-conventional'] };
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
.idea
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# Build Outputs
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
dist
|
||||
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
4
.husky/commit-msg
Normal file
4
.husky/commit-msg
Normal file
@@ -0,0 +1,4 @@
|
||||
# #!/usr/bin/env sh
|
||||
# . "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# npx commitlint --edit $1
|
||||
3
.lintstagedrc
Normal file
3
.lintstagedrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"*.{js,jsx,ts,tsx}": ["prettier . --write"]
|
||||
}
|
||||
7
.prettierignore
Normal file
7
.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
.gitignore
|
||||
.idea
|
||||
.turbo
|
||||
.changeset
|
||||
pnpm-workspace.yaml
|
||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"plugins": [
|
||||
"prettier-plugin-tailwindcss",
|
||||
"prettier-plugin-css-order",
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-packagejson"
|
||||
],
|
||||
"tailwindFunctions": ["clsx", "cn", "twMerge"]
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Aung Pyae Phyo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
88
README.md
Normal file
88
README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
|
||||
## NestJS & NextJS
|
||||
|
||||
Repositorio Sistema Base Fondemi.
|
||||
|
||||
### **Caracteristicas**
|
||||
|
||||
- Backend `NestJS (v11)`
|
||||
- Frontend `NextJS (v15)`
|
||||
- `SWC` para una transpilación rápida de TypeScript y JavaScript
|
||||
- `pnpm` para una gestión eficiente de dependencias
|
||||
- Autenticación con token de acceso y token de actualización `JWT` para un acceso seguro a la API
|
||||
- Base de datos `PostgreSQL` con Drizzle ORM
|
||||
- `Nodemailer` para servicios de correo electrónico
|
||||
- `Linting` y `Formatting` preconfigurados para la calidad del código
|
||||
- Compatibilidad con `Micro-Frontend` con Turborepo
|
||||
- Integración con `Shadcn/UI` para componentes con estilo
|
||||
- Integración con `Tailwindcss(v4)` en `@repo/shadcn`
|
||||
|
||||
### **Tabla de contenido**
|
||||
|
||||
- Installation
|
||||
- Getting Started
|
||||
- Project Structure
|
||||
- Scripts
|
||||
- Contributing
|
||||
- License
|
||||
|
||||
### **Installation**
|
||||
|
||||
Clona el repositorio:
|
||||
|
||||
```shell
|
||||
git clone https://git.fondemi.gob.ve/Fondemi/sistema_base.git
|
||||
```
|
||||
|
||||
Clona las variables de entorno y reemplaz la informacion:
|
||||
|
||||
```shell
|
||||
cp .env.exmple .env
|
||||
```
|
||||
|
||||
|
||||
Instala dependencias usando pnpm:
|
||||
|
||||
```shell
|
||||
pnpm install
|
||||
```
|
||||
|
||||
|
||||
Migra la base de datos:
|
||||
|
||||
```shell
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
Inicio
|
||||
Inicio el servidor en desarrollo, run:
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
|
||||
Estructura del proyecto
|
||||
El repositorio está organizado de la siguiente manera:
|
||||
|
||||
```yaml
|
||||
turborepo
|
||||
├── .husky # Git hooks
|
||||
├── apps
|
||||
│ ├── api # NestJS application
|
||||
│ └── web # NextJS application
|
||||
├── packages
|
||||
│ ├── shadcn # shadcn/UI component library
|
||||
│ ├── ts-config # Shared typescript configuration files
|
||||
│ ├── eslint-config # Shared eslint configuration files
|
||||
└── turbo.json # Turborepo configuration
|
||||
```
|
||||
|
||||
### Caracteristicas del sistema
|
||||
|
||||
- Administración de encuestas
|
||||
- Responder encuestas
|
||||
- Registro usuario
|
||||
- Login de usuario
|
||||
- Estadisticas de encuestas
|
||||
|
||||
19
apps/api/.env.example
Normal file
19
apps/api/.env.example
Normal 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
56
apps/api/.gitignore
vendored
Normal 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
13
apps/api/.swcrc
Normal 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
1
apps/api/README.md
Normal file
@@ -0,0 +1 @@
|
||||
### Backend
|
||||
4
apps/api/eslint.config.mjs
Normal file
4
apps/api/eslint.config.mjs
Normal 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
10
apps/api/nest-cli.json
Normal 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
87
apps/api/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
64
apps/api/src/app.module.ts
Normal file
64
apps/api/src/app.module.ts
Normal 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
35
apps/api/src/bootstrap.ts
Normal 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}`);
|
||||
});
|
||||
};
|
||||
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;
|
||||
};
|
||||
22
apps/api/src/database/drizzle-provider.ts
Normal file
22
apps/api/src/database/drizzle-provider.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
10
apps/api/src/database/drizzle.module.ts
Normal file
10
apps/api/src/database/drizzle.module.ts
Normal 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 {}
|
||||
5
apps/api/src/database/index.ts
Normal file
5
apps/api/src/database/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
export * from './schema/activity_logs';
|
||||
export * from './schema/auth';
|
||||
export * from './schema/general';
|
||||
export * from './schema/surveys'
|
||||
173
apps/api/src/database/migrations/0000_abnormal_lethal_legion.sql
Normal file
173
apps/api/src/database/migrations/0000_abnormal_lethal_legion.sql
Normal 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);
|
||||
9
apps/api/src/database/migrations/0001_massive_kylun.sql
Normal file
9
apps/api/src/database/migrations/0001_massive_kylun.sql
Normal 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);
|
||||
1306
apps/api/src/database/migrations/meta/0000_snapshot.json
Normal file
1306
apps/api/src/database/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1358
apps/api/src/database/migrations/meta/0001_snapshot.json
Normal file
1358
apps/api/src/database/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
20
apps/api/src/database/migrations/meta/_journal.json
Normal file
20
apps/api/src/database/migrations/meta/_journal.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
32
apps/api/src/database/schema/activity_logs.ts
Normal file
32
apps/api/src/database/schema/activity_logs.ts
Normal 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),
|
||||
}),
|
||||
);
|
||||
132
apps/api/src/database/schema/auth.ts
Normal file
132
apps/api/src/database/schema/auth.ts
Normal 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],
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
8
apps/api/src/database/schema/enum.ts
Normal file
8
apps/api/src/database/schema/enum.ts
Normal 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',
|
||||
]);
|
||||
114
apps/api/src/database/schema/general.ts
Normal file
114
apps/api/src/database/schema/general.ts
Normal 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"),
|
||||
// });
|
||||
4
apps/api/src/database/schema/schemas.ts
Normal file
4
apps/api/src/database/schema/schemas.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
//schemas
|
||||
import * as t from 'drizzle-orm/pg-core';
|
||||
export const authSchema = t.pgSchema('auth'); //autenticacion y sessiones usuarios
|
||||
|
||||
57
apps/api/src/database/schema/surveys.ts
Normal file
57
apps/api/src/database/schema/surveys.ts
Normal 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`);
|
||||
24
apps/api/src/database/seeds/admin-role.seed.ts
Normal file
24
apps/api/src/database/seeds/admin-role.seed.ts
Normal 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');
|
||||
}
|
||||
37
apps/api/src/database/seeds/index.ts
Normal file
37
apps/api/src/database/seeds/index.ts
Normal 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();
|
||||
25
apps/api/src/database/seeds/municipalities.seed.ts
Normal file
25
apps/api/src/database/seeds/municipalities.seed.ts
Normal 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');
|
||||
}
|
||||
25
apps/api/src/database/seeds/parishes.seed.ts
Normal file
25
apps/api/src/database/seeds/parishes.seed.ts
Normal 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');
|
||||
}
|
||||
24
apps/api/src/database/seeds/states.seed.ts
Normal file
24
apps/api/src/database/seeds/states.seed.ts
Normal 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');
|
||||
}
|
||||
39
apps/api/src/database/seeds/user-admin.seed.ts
Normal file
39
apps/api/src/database/seeds/user-admin.seed.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
8
apps/api/src/database/timestamps.ts
Normal file
8
apps/api/src/database/timestamps.ts
Normal 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()),
|
||||
};
|
||||
13
apps/api/src/drizzle.config.ts
Normal file
13
apps/api/src/drizzle.config.ts
Normal 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;
|
||||
61
apps/api/src/features/auth/auth.controller.ts
Normal file
61
apps/api/src/features/auth/auth.controller.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
12
apps/api/src/features/auth/auth.module.ts
Normal file
12
apps/api/src/features/auth/auth.module.ts
Normal 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 {}
|
||||
368
apps/api/src/features/auth/auth.service.ts
Normal file
368
apps/api/src/features/auth/auth.service.ts
Normal 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;
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
22
apps/api/src/features/auth/dto/change-password.dto.ts
Normal file
22
apps/api/src/features/auth/dto/change-password.dto.ts
Normal 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;
|
||||
}
|
||||
14
apps/api/src/features/auth/dto/confirm-email.dto.ts
Normal file
14
apps/api/src/features/auth/dto/confirm-email.dto.ts
Normal 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;
|
||||
}
|
||||
29
apps/api/src/features/auth/dto/create-user.dto.ts
Normal file
29
apps/api/src/features/auth/dto/create-user.dto.ts
Normal 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;
|
||||
}
|
||||
10
apps/api/src/features/auth/dto/forgot-password.dto.ts
Normal file
10
apps/api/src/features/auth/dto/forgot-password.dto.ts
Normal 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;
|
||||
}
|
||||
14
apps/api/src/features/auth/dto/refresh-token.dto.ts
Normal file
14
apps/api/src/features/auth/dto/refresh-token.dto.ts
Normal 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;
|
||||
}
|
||||
22
apps/api/src/features/auth/dto/reset-password.dto.ts
Normal file
22
apps/api/src/features/auth/dto/reset-password.dto.ts
Normal 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;
|
||||
}
|
||||
16
apps/api/src/features/auth/dto/signIn-user.dto.ts
Normal file
16
apps/api/src/features/auth/dto/signIn-user.dto.ts
Normal 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;
|
||||
}
|
||||
10
apps/api/src/features/auth/dto/signOut-user.dto.ts
Normal file
10
apps/api/src/features/auth/dto/signOut-user.dto.ts
Normal 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;
|
||||
}
|
||||
45
apps/api/src/features/auth/dto/signUp-user.dto.ts
Normal file
45
apps/api/src/features/auth/dto/signUp-user.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { User } from '@/features/users/entities/user.entity';
|
||||
|
||||
export class UpdateRefreshTokenDto {
|
||||
user: User;
|
||||
refresh_token: string;
|
||||
}
|
||||
16
apps/api/src/features/auth/dto/validate-user.dto.ts
Normal file
16
apps/api/src/features/auth/dto/validate-user.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
interface AuthTokensInterface {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export default AuthTokensInterface;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
interface RefreshTokenInterface {
|
||||
access_token: string;
|
||||
access_expire_in: number;
|
||||
refresh_token: string;
|
||||
refresh_expire_in: number;
|
||||
}
|
||||
|
||||
export default RefreshTokenInterface;
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Session {
|
||||
userId: string;
|
||||
sessionToken: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCategoryTypeDto } from './create-category-type.dto';
|
||||
|
||||
export class UpdateCategoryTypeDto extends PartialType(CreateCategoryTypeDto) {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateMunicipalityDto } from './create-municipality.dto';
|
||||
|
||||
export class UpdateMunicipalityDto extends PartialType(CreateMunicipalityDto) {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user