diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aebcb97 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.git +.gitignore +Dockerfile* +docker-compose*.yml +npm-debug.log +.env +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f569e51 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Copy this file to .env locally or to the server as /opt/exdev-api/.env. +# Never commit real credentials. +NODE_ENV=development +PORT=3001 +# For local `npm run start:dev`, use 127.0.0.1 or localhost. +# For Docker Compose on the VM, use host.docker.internal and PostgreSQL's VM port. +DATABASE_URL=postgresql://exdev_website:CHANGE_ME@127.0.0.1:5432/exdev_staging_db diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b1e79bc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @IanBattistoni @Im-Fran diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index faf3c2b..b474b74 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -1,9 +1,9 @@ -name: CI/CD – exdev-apply-api +name: CI/CD – exdev-api on: push: - branches: ["master"] # cámbialo si despliegas otra rama - workflow_dispatch: {} # permite correrlo manualmente + branches: [develop, master] + workflow_dispatch: {} permissions: contents: read @@ -14,12 +14,10 @@ concurrency: cancel-in-progress: true jobs: - build-and-deploy: + build-and-push: runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - name: Log in to GHCR uses: docker/login-action@v3 @@ -28,33 +26,63 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build & Push image + - name: Build and push image uses: docker/build-push-action@v6 with: context: . push: true - tags: ghcr.io/${{ github.repository }}:latest + tags: ghcr.io/exdevutem/exdev-api:${{ github.ref_name }} + + deploy-staging: + if: github.ref_name == 'develop' + needs: build-and-push + runs-on: [self-hosted, staging] + environment: staging + steps: + - uses: actions/checkout@v4 - - name: Deploy over SSH - uses: appleboy/ssh-action@v1.2.0 + - name: Log in to GHCR + uses: docker/login-action@v3 with: - host: ${{ secrets.SSH_HOST }} # ej: 18.231.166.7 - username: ${{ secrets.SSH_USER }} # ej: ubuntu o ec2-user - key: ${{ secrets.SSH_PRIVATE_KEY }} # llave privada que creaste - port: ${{ secrets.SSH_PORT || 22 }} - script: | - set -e - if [ ! -d /opt/exdev-apply-api ]; then - mkdir -p /opt/exdev-apply-api - cd /opt/exdev-apply-api - git clone https://github.com/${{ github.repository }} . - else - cd /opt/exdev-apply-api - git fetch --all - git checkout -B ${{ github.ref_name }} origin/${{ github.ref_name }} - fi - - docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - docker compose -f docker-compose.yml pull - docker compose -f docker-compose.yml up -d --remove-orphans - docker image prune -f + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy staging + shell: bash + run: | + set -euo pipefail + APP_DIR=/opt/exdev-api + mkdir -p "$APP_DIR" + cp docker-compose.yml "$APP_DIR/docker-compose.yml" + export IMAGE_TAG=develop + docker compose -f "$APP_DIR/docker-compose.yml" pull + docker compose -f "$APP_DIR/docker-compose.yml" up -d --remove-orphans + docker image prune -f + + deploy-production: + if: github.ref_name == 'master' + needs: build-and-push + runs-on: [self-hosted, production] + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy production + shell: bash + run: | + set -euo pipefail + APP_DIR=/opt/exdev-api + mkdir -p "$APP_DIR" + cp docker-compose.yml "$APP_DIR/docker-compose.yml" + export IMAGE_TAG=master + docker compose -f "$APP_DIR/docker-compose.yml" pull + docker compose -f "$APP_DIR/docker-compose.yml" up -d --remove-orphans + docker image prune -f diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bab0eff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# ---------- Dep stage ---------- +FROM node:24-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci + +# ---------- Build stage ---------- +FROM node:24-alpine AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# Si usas nest-cli.json/tsconfig*.json, se copian arriba con "COPY . ." +RUN npm run build + +# ---------- Prod deps (solo prod) ---------- +FROM node:24-alpine AS prod-deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev + +# ---------- Runner ---------- +FROM node:24-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +COPY --from=prod-deps /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist + + +EXPOSE 3000 +CMD ["node", "dist/main.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c8198fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.9" + +services: + exdev-api: + image: ghcr.io/exdevutem/exdev-api:${IMAGE_TAG:-latest} + container_name: exdev-api + restart: unless-stopped + ports: + - "3001:3000" + env_file: + - /opt/exdev-api/.env + extra_hosts: + - "host.docker.internal:host-gateway" + diff --git a/src/applications/applications.controller.ts b/src/applications/applications.controller.ts index c786f69..1623ca3 100644 --- a/src/applications/applications.controller.ts +++ b/src/applications/applications.controller.ts @@ -9,4 +9,8 @@ export class ApplicationsController { create(@Body() body: any){ return this.applicationsService.create(body); } + @Get() + findAll() { + return this.applicationsService.findAll(); + } } diff --git a/src/applications/applications.service.ts b/src/applications/applications.service.ts index e012969..58aa6ca 100644 --- a/src/applications/applications.service.ts +++ b/src/applications/applications.service.ts @@ -69,4 +69,51 @@ async create(body: any){ } } + async findAll() { + const countSql = `SELECT COUNT(*)::int AS total FROM postulaciones;`; + + const listSql = ` + SELECT + id, + nombre_completo, + rut, + edad, + correo_institucional, + campus, + carrera, + anio_ingreso, + anio_actual, + area_interes1, + area_interes2, + area_interes3, + ayudantias, + horas_disponibles_semanales, + motivo_postulacion, + proyecto_idea, + portafolio, + postulacion_conjunta, + pitch, + apodo, + created_at, + updated_at + FROM postulaciones + ORDER BY created_at DESC; + `; + + try { + const [countRes, listRes] = await Promise.all([ + this.pool.query(countSql), + this.pool.query(listSql), + ]); + + return { + totalPostulaciones: countRes.rows[0].total as number, + postulaciones: listRes.rows, + }; + } catch (err: any) { + const { status, body } = buildErrorExceptionPayload(err); + throw new HttpException(body, status, { cause: err }); + } + } + } diff --git a/src/main.ts b/src/main.ts index f8d5795..ea97106 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,42 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + //desactivar cache + const expressApp = app.getHttpAdapter().getInstance(); + expressApp.set('etag', false); + + const WEB_ORIGINS = [ + 'https://dev.exdev.cl', + 'https://exdev.cl', + 'https://www.exdev.cl', + 'http://localhost:5000', + 'http://localhost:3000', + 'https://tomas.exdev.cl', + 'https://www.tomas.exdev.cl' + ]; + app.use((req, res, next) => { + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + next(); + }); + app.enableCors({ + origin: (origin, cb) => { + + if (!origin) return cb(null, true); + cb(null, WEB_ORIGINS.includes(origin)); + }, + methods: ['GET','POST','OPTIONS'], + allowedHeaders: ['Content-Type','Authorization'], + credentials: false, + optionsSuccessStatus: 204, + }); + await app.listen(process.env.PORT ? Number(process.env.PORT) : 3000); } bootstrap(); +