diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..d57f215 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,362 @@ +pipeline { + agent any + + options { + disableConcurrentBuilds() + timestamps() + skipDefaultCheckout(true) + } + + environment { + REGISTRY = "localhost:5000" + SERVER_IP = "192.168.1.95" + + BACKEND_IMAGE = "${REGISTRY}/the-tip-top/backend" + FRONTEND_IMAGE = "${REGISTRY}/the-tip-top/frontend" + + CI_NETWORK = "tiptop-ci-${JOB_BASE_NAME}-${BUILD_NUMBER}" + CI_DATABASE = "tiptop-ci-db-${JOB_BASE_NAME}-${BUILD_NUMBER}" + + DATABASE_URL = "postgresql://tiptop_ci:tiptop_ci_password@${CI_DATABASE}:5432/the_tip_top_ci" + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Préparer environnement') { + steps { + script { + env.IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}" + + /* + * Stratégie de branches : + * + * feature/* -> CI uniquement + * develop -> DEV automatique + * release-* -> PREPROD automatique + * main -> PROD avec validation manuelle + */ + + if (env.BRANCH_NAME == 'develop') { + env.DEPLOY_ENABLED = 'true' + env.DEPLOY_ENV = 'DEV' + + env.COMPOSE_FILE = 'deploy/dev/docker-compose.yml' + env.COMPOSE_PROJECT = 'tiptop-dev' + env.DEPLOY_NETWORK = 'tiptop-dev' + + env.FRONTEND_API_URL = "http://${SERVER_IP}:5001" + + } else if (env.BRANCH_NAME.startsWith('release-')) { + env.DEPLOY_ENABLED = 'true' + env.DEPLOY_ENV = 'PREPROD' + + env.COMPOSE_FILE = 'deploy/preprod/docker-compose.yml' + env.COMPOSE_PROJECT = 'tiptop-preprod' + env.DEPLOY_NETWORK = 'tiptop-preprod' + + env.FRONTEND_API_URL = "http://${SERVER_IP}:5002" + + } else if (env.BRANCH_NAME == 'main') { + env.DEPLOY_ENABLED = 'true' + env.DEPLOY_ENV = 'PROD' + + env.COMPOSE_FILE = 'deploy/prod/docker-compose.yml' + env.COMPOSE_PROJECT = 'tiptop-prod' + env.DEPLOY_NETWORK = 'tiptop-prod' + + env.FRONTEND_API_URL = "http://${SERVER_IP}:5003" + + } else { + env.DEPLOY_ENABLED = 'false' + env.DEPLOY_ENV = 'CI uniquement' + + env.COMPOSE_FILE = '' + env.COMPOSE_PROJECT = '' + env.DEPLOY_NETWORK = '' + env.FRONTEND_API_URL = '' + } + + echo "Branche : ${env.BRANCH_NAME}" + echo "Tag Docker : ${env.IMAGE_TAG}" + echo "Environnement : ${env.DEPLOY_ENV}" + echo "Déploiement activé : ${env.DEPLOY_ENABLED}" + echo "Serveur : ${env.SERVER_IP}" + echo "API frontend : ${env.FRONTEND_API_URL}" + } + } + } + + stage('Vérification du workspace') { + steps { + sh ''' + test -f backend/package.json + test -f backend/package-lock.json + test -f frontend/package.json + test -f frontend/package-lock.json + + if [ "${DEPLOY_ENABLED}" = "true" ]; then + test -f "${COMPOSE_FILE}" + fi + ''' + } + } + + stage('Backend - Tests') { + steps { + sh ''' + docker network create "${CI_NETWORK}" + + docker run -d \ + --name "${CI_DATABASE}" \ + --network "${CI_NETWORK}" \ + -e POSTGRES_USER=tiptop_ci \ + -e POSTGRES_PASSWORD=tiptop_ci_password \ + -e POSTGRES_DB=the_tip_top_ci \ + postgres:16-alpine + + echo "Attente de PostgreSQL CI..." + + for i in $(seq 1 30); do + if docker exec "${CI_DATABASE}" \ + pg_isready \ + -U tiptop_ci \ + -d the_tip_top_ci; then + + echo "PostgreSQL CI est prêt." + break + fi + + if [ "$i" -eq 30 ]; then + echo "PostgreSQL CI ne répond pas." + docker logs "${CI_DATABASE}" + exit 1 + fi + + sleep 2 + done + + docker run --rm \ + --network "${CI_NETWORK}" \ + --volumes-from furious-jenkins \ + -v tiptop-npm-cache-backend:/npm-cache \ + --user "$(id -u):$(id -g)" \ + -e HOME=/tmp \ + -e NODE_OPTIONS=--dns-result-order=ipv4first \ + -e NPM_CONFIG_CACHE=/npm-cache \ + -e NPM_CONFIG_FETCH_RETRIES=5 \ + -e NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=20000 \ + -e NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=120000 \ + -e NPM_CONFIG_FETCH_TIMEOUT=600000 \ + -e DATABASE_URL="${DATABASE_URL}" \ + -e JWT_SECRET=jenkins_ci_secret \ + -e NODE_ENV=test \ + -w "${WORKSPACE}/backend" \ + node:22-alpine \ + sh -c " + npm ci --prefer-offline --no-audit --no-fund && + npx prisma generate && + npx prisma migrate deploy && + npm test + " + ''' + } + + post { + always { + junit( + testResults: 'backend/test-results/*.xml', + allowEmptyResults: true + ) + + archiveArtifacts( + artifacts: 'backend/coverage/**', + fingerprint: true, + allowEmptyArchive: true + ) + } + } + } + + stage('Frontend - Lint et Build') { + steps { + sh ''' + docker run --rm \ + --volumes-from furious-jenkins \ + -v tiptop-npm-cache-frontend:/npm-cache \ + --user "$(id -u):$(id -g)" \ + -e HOME=/tmp \ + -e NODE_OPTIONS=--dns-result-order=ipv4first \ + -e NPM_CONFIG_CACHE=/npm-cache \ + -e NPM_CONFIG_FETCH_RETRIES=5 \ + -e NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=20000 \ + -e NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=120000 \ + -e NPM_CONFIG_FETCH_TIMEOUT=600000 \ + -w "${WORKSPACE}/frontend" \ + node:22-alpine \ + sh -c " + npm ci --prefer-offline --no-audit --no-fund && + npm run lint && + npm run build + " + ''' + } + } + + stage('Construction des images Docker') { + when { + expression { + env.DEPLOY_ENABLED == 'true' + } + } + + steps { + sh ''' + docker build \ + -t "${BACKEND_IMAGE}:${IMAGE_TAG}" \ + backend + + docker build \ + --build-arg VITE_API_URL="${FRONTEND_API_URL}" \ + -t "${FRONTEND_IMAGE}:${IMAGE_TAG}" \ + frontend + ''' + } + } + + stage('Publication dans le Registry') { + when { + expression { + env.DEPLOY_ENABLED == 'true' + } + } + + steps { + sh ''' + docker push "${BACKEND_IMAGE}:${IMAGE_TAG}" + docker push "${FRONTEND_IMAGE}:${IMAGE_TAG}" + ''' + } + } + + stage('Validation production') { + when { + expression { + env.DEPLOY_ENV == 'PROD' + } + } + + steps { + timeout(time: 15, unit: 'MINUTES') { + input( + message: "Déployer ${IMAGE_TAG} en production ?", + ok: "Déployer" + ) + } + } + } + + stage('Déploiement') { + when { + expression { + env.DEPLOY_ENABLED == 'true' + } + } + + steps { + sh ''' + echo "Déploiement de ${BRANCH_NAME} vers ${DEPLOY_ENV}" + echo "Image backend : ${BACKEND_IMAGE}:${IMAGE_TAG}" + echo "Image frontend : ${FRONTEND_IMAGE}:${IMAGE_TAG}" + echo "Compose : ${COMPOSE_FILE}" + + REGISTRY="${REGISTRY}" \ + IMAGE_TAG="${IMAGE_TAG}" \ + docker compose \ + -p "${COMPOSE_PROJECT}" \ + -f "${COMPOSE_FILE}" \ + up -d \ + --pull always \ + --remove-orphans + + echo "Attente du backend ${DEPLOY_ENV}..." + + for i in $(seq 1 30); do + if docker run --rm \ + --network "${DEPLOY_NETWORK}" \ + curlimages/curl:8.12.1 \ + -fsS \ + http://backend:5000/ \ + >/dev/null; then + + echo "Backend ${DEPLOY_ENV} opérationnel." + break + fi + + if [ "$i" -eq 30 ]; then + echo "Le backend ${DEPLOY_ENV} ne répond pas." + + docker compose \ + -p "${COMPOSE_PROJECT}" \ + -f "${COMPOSE_FILE}" \ + ps + + docker compose \ + -p "${COMPOSE_PROJECT}" \ + -f "${COMPOSE_FILE}" \ + logs \ + --tail=150 + + exit 1 + fi + + sleep 5 + done + + echo "Vérification du frontend ${DEPLOY_ENV}..." + + docker run --rm \ + --network "${DEPLOY_NETWORK}" \ + curlimages/curl:8.12.1 \ + -fsS \ + http://frontend/ \ + >/dev/null + + echo "Frontend ${DEPLOY_ENV} opérationnel." + ''' + } + } + } + + post { + always { + sh ''' + docker rm -f "${CI_DATABASE}" 2>/dev/null || true + docker network rm "${CI_NETWORK}" 2>/dev/null || true + ''' + } + + success { + echo """ + Pipeline réussi. + + Branche : ${BRANCH_NAME} + Environnement : ${DEPLOY_ENV} + Version : ${IMAGE_TAG} + """ + } + + failure { + echo """ + Pipeline en échec. + + Branche : ${BRANCH_NAME} + Environnement : ${DEPLOY_ENV} + """ + } + } +} diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..7e5a332 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +.env +coverage +.git +.gitignore +README.md diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..f09f7a9 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY prisma ./prisma +RUN npx prisma generate + +COPY . . + +EXPOSE 5000 + +CMD ["npm", "start"] diff --git a/backend/jest.config.js b/backend/jest.config.js index 5296fb1..b803d50 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -1,26 +1,33 @@ module.exports = { testEnvironment: "node", - // Dossier des tests testMatch: ["**/tests/**/*.test.js"], - // Nettoyage des mocks entre les tests clearMocks: true, - // Timeout (utile pour Prisma / DB) testTimeout: 10000, - // Affichage des logs de test - verbose: true, + verbose: true, - // Couverture de code (optionnel mais bon pour soutenance) collectCoverage: true, coverageDirectory: "coverage", + coverageReporters: [ + "text", + "lcov", + "html" + ], - // Fichiers à ignorer - testPathIgnorePatterns: ["/node_modules/"], + reporters: [ + "default", + [ + "jest-junit", + { + outputDirectory: "./test-results", + outputName: "junit.xml" + } + ] + ], - // Configuration pour Jest avec Supertest - //setupFilesAfterEnv: ["/tests/setup.js"] -}; \ No newline at end of file + testPathIgnorePatterns: ["/node_modules/"] +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index 7e007c1..041d3d6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -29,6 +29,7 @@ "cross-env": "^10.1.0", "dotenv": "^17.4.2", "jest": "^30.4.2", + "jest-junit": "^17.0.0", "nodemon": "^3.1.14", "supertest": "^7.2.2" } @@ -609,9 +610,9 @@ "license": "MIT" }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -3201,9 +3202,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", - "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -3788,9 +3789,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "license": "MIT", "engines": { "node": ">= 12" @@ -4488,6 +4489,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/jest-junit": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-17.0.0.tgz", + "integrity": "sha512-RYWCkq4j59gUXj5DsgbIE7xFBZzu1gtibPhyjSjMmGaOTLnqlXhg7x9zuGCwgbCuMAyoyvk0Mi8wSrRR5uOeLA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^14.0.0", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/jest-leak-detector": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", @@ -5406,6 +5423,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5488,9 +5518,9 @@ } }, "node_modules/nodemailer": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz", - "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.5.tgz", + "integrity": "sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -6952,6 +6982,20 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -7058,6 +7102,13 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index a1f3b3b..a637c62 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,8 @@ "generate:tickets": "node src/scripts/generateTickets.js", "test": "jest --runInBand", "test:watch": "jest --watch", - "test:coverage": "jest --coverage" + "test:coverage": "jest --coverage", + "init:db": "node src/scripts/initDatabase.js" }, "keywords": [], "author": "", @@ -35,6 +36,7 @@ "cross-env": "^10.1.0", "dotenv": "^17.4.2", "jest": "^30.4.2", + "jest-junit": "^17.0.0", "nodemon": "^3.1.14", "supertest": "^7.2.2" }, diff --git a/backend/src/scripts/initDatabase.js b/backend/src/scripts/initDatabase.js new file mode 100644 index 0000000..3d7645c --- /dev/null +++ b/backend/src/scripts/initDatabase.js @@ -0,0 +1,55 @@ +const { execSync } = require("child_process"); +const prisma = require("../config/prisma"); + +async function main() { + let lotCount = await prisma.lot.count(); + let ticketCount = await prisma.ticket.count(); + + console.log(`Lots présents : ${lotCount}`); + console.log(`Tickets présents : ${ticketCount}`); + + if (lotCount === 0) { + console.log("Aucun lot trouvé : exécution du seed..."); + execSync("npx prisma db seed", { + stdio: "inherit", + env: process.env, + }); + + lotCount = await prisma.lot.count(); + console.log(`Lots présents après seed : ${lotCount}`); + } else { + console.log("Les lots existent déjà : seed ignoré."); + } + + if (lotCount !== 5) { + throw new Error( + `Initialisation interrompue : 5 lots attendus, ${lotCount} trouvé(s).` + ); + } + + ticketCount = await prisma.ticket.count(); + + if (ticketCount === 0) { + console.log("Aucun ticket trouvé : génération des tickets..."); + execSync("npm run generate:tickets", { + stdio: "inherit", + env: process.env, + }); + + ticketCount = await prisma.ticket.count(); + console.log(`Tickets présents après génération : ${ticketCount}`); + } else { + console.log("Les tickets existent déjà : génération ignorée."); + } + + console.log("Initialisation de la base terminée."); +} + +main() + .catch((error) => { + console.error("Erreur pendant l'initialisation de la base :", error); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml new file mode 100644 index 0000000..8602afa --- /dev/null +++ b/deploy/dev/docker-compose.yml @@ -0,0 +1,75 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + + environment: + POSTGRES_USER: tiptop + POSTGRES_PASSWORD: tiptop_dev_password + POSTGRES_DB: the_tip_top_db + + volumes: + - tiptop_postgres_dev_data:/var/lib/postgresql/data + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U tiptop -d the_tip_top_db"] + interval: 10s + timeout: 5s + retries: 10 + + networks: + - tiptop-dev + + backend: + image: ${REGISTRY}/the-tip-top/backend:${IMAGE_TAG} + restart: unless-stopped + + depends_on: + postgres: + condition: service_healthy + + environment: + PORT: 5000 + DATABASE_URL: postgresql://tiptop:tiptop_dev_password@postgres:5432/the_tip_top_db + JWT_SECRET: dev_secret_change_me + APP_URL: http://192.168.1.23:5001 + MAIL_HOST: smtp.gmail.com + MAIL_PORT: 587 + MAIL_USER: "" + MAIL_PASS: "" + MAIL_FROM: "The Tip Top " + + command: > + sh -c " + npx prisma migrate deploy && + npm run init:db && + npm start + " + + ports: + - "5001:5000" + + networks: + - tiptop-dev + + frontend: + image: ${REGISTRY}/the-tip-top/frontend:${IMAGE_TAG} + restart: unless-stopped + + depends_on: + - backend + + ports: + - "5173:80" + + networks: + - tiptop-dev + +volumes: + tiptop_postgres_dev_data: + external: true + name: the-tip-top_tiptop_postgres_dev_data + +networks: + tiptop-dev: + name: tiptop-dev diff --git a/deploy/preprod/docker-compose.yml b/deploy/preprod/docker-compose.yml new file mode 100644 index 0000000..b61eef5 --- /dev/null +++ b/deploy/preprod/docker-compose.yml @@ -0,0 +1,79 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + + environment: + POSTGRES_USER: tiptop_preprod + POSTGRES_PASSWORD: tiptop_preprod_password + POSTGRES_DB: the_tip_top_preprod + + volumes: + - tiptop_preprod_postgres_data:/var/lib/postgresql/data + + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U tiptop_preprod -d the_tip_top_preprod" + ] + interval: 10s + timeout: 5s + retries: 10 + + networks: + - tiptop-preprod + + backend: + image: ${REGISTRY}/the-tip-top/backend:${IMAGE_TAG} + restart: unless-stopped + + depends_on: + postgres: + condition: service_healthy + + environment: + PORT: 5000 + NODE_ENV: preproduction + DATABASE_URL: postgresql://tiptop_preprod:tiptop_preprod_password@postgres:5432/the_tip_top_preprod + JWT_SECRET: preprod_secret_change_me + APP_URL: http://192.168.1.23:5002 + MAIL_HOST: smtp.gmail.com + MAIL_PORT: 587 + MAIL_USER: "" + MAIL_PASS: "" + MAIL_FROM: "The Tip Top Preprod " + + command: > + sh -c " + npx prisma migrate deploy && + npm run init:db && + npm start + " + + ports: + - "5002:5000" + + networks: + - tiptop-preprod + + frontend: + image: ${REGISTRY}/the-tip-top/frontend:${IMAGE_TAG} + restart: unless-stopped + + depends_on: + - backend + + ports: + - "5174:80" + + networks: + - tiptop-preprod + +volumes: + tiptop_preprod_postgres_data: + name: tiptop_preprod_postgres_data + +networks: + tiptop-preprod: + name: tiptop-preprod diff --git a/deploy/prod/docker-compose.yml b/deploy/prod/docker-compose.yml new file mode 100644 index 0000000..beeda63 --- /dev/null +++ b/deploy/prod/docker-compose.yml @@ -0,0 +1,80 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + + environment: + POSTGRES_USER: tiptop_prod + POSTGRES_PASSWORD: tiptop_prod_password + POSTGRES_DB: the_tip_top_prod + + volumes: + - tiptop_prod_postgres_data:/var/lib/postgresql/data + + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U tiptop_prod -d the_tip_top_prod" + ] + interval: 10s + timeout: 5s + retries: 10 + + networks: + - tiptop-prod + + backend: + image: ${REGISTRY}/the-tip-top/backend:${IMAGE_TAG} + restart: unless-stopped + + depends_on: + postgres: + condition: service_healthy + + environment: + PORT: 5000 + NODE_ENV: production + DATABASE_URL: postgresql://tiptop_prod:tiptop_prod_password@postgres:5432/the_tip_top_prod + JWT_SECRET: prod_secret_change_me + APP_URL: http://192.168.1.23:5003 + + MAIL_HOST: smtp.gmail.com + MAIL_PORT: 587 + MAIL_USER: "" + MAIL_PASS: "" + MAIL_FROM: "The Tip Top " + + command: > + sh -c " + npx prisma migrate deploy && + npm run init:db && + npm start + " + + ports: + - "5003:5000" + + networks: + - tiptop-prod + + frontend: + image: ${REGISTRY}/the-tip-top/frontend:${IMAGE_TAG} + restart: unless-stopped + + depends_on: + - backend + + ports: + - "5175:80" + + networks: + - tiptop-prod + +volumes: + tiptop_prod_postgres_data: + name: tiptop_prod_postgres_data + +networks: + tiptop-prod: + name: tiptop-prod diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..8df08a2 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,84 @@ +services: + postgres: + image: postgres:16-alpine + container_name: tiptop-postgres-dev + restart: unless-stopped + + environment: + POSTGRES_USER: tiptop + POSTGRES_PASSWORD: tiptop_dev_password + POSTGRES_DB: the_tip_top_db + + volumes: + - tiptop_postgres_dev_data:/var/lib/postgresql/data + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U tiptop -d the_tip_top_db"] + interval: 10s + timeout: 5s + retries: 5 + + networks: + - tiptop-dev + + backend: + build: + context: ./backend + dockerfile: Dockerfile + + container_name: tiptop-backend-dev + restart: unless-stopped + + depends_on: + postgres: + condition: service_healthy + + environment: + PORT: 5000 + DATABASE_URL: postgresql://tiptop:tiptop_dev_password@postgres:5432/the_tip_top_db + JWT_SECRET: dev_secret_change_me + APP_URL: http://localhost:5000 + + MAIL_HOST: smtp.gmail.com + MAIL_PORT: 587 + MAIL_USER: "" + MAIL_PASS: "" + MAIL_FROM: "The Tip Top " + + ports: + - "5001:5000" + + command: > + sh -c " + npx prisma migrate deploy && + npm run init:db && + npm start + " + + networks: + - tiptop-dev + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_URL: http://localhost:5001 + + container_name: tiptop-frontend-dev + restart: unless-stopped + + depends_on: + - backend + + ports: + - "5173:80" + + networks: + - tiptop-dev + +volumes: + tiptop_postgres_dev_data: + +networks: + tiptop-dev: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..cf4ed03 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +npm-debug.log +.env +.git +.gitignore +README.md diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..f5e98df --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,22 @@ +FROM node:22-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} + +RUN npm run build + + +FROM nginx:1.27-alpine + +COPY --from=build /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..3aa17e6 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b475800..0e1daf1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -354,9 +354,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", "dev": true, "funding": [ { @@ -374,9 +374,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -398,9 +398,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", - "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", + "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==", "dev": true, "funding": [ { @@ -414,8 +414,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.2.1" + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -450,9 +450,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", - "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", "dev": true, "funding": [ { @@ -496,9 +496,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, "license": "MIT", "optional": true, @@ -1451,9 +1451,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", + "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", "dev": true, "license": "MIT", "engines": { @@ -2549,7 +2549,6 @@ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", @@ -3050,9 +3049,9 @@ } }, "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -3502,9 +3501,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -3529,9 +3528,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -3539,22 +3538,22 @@ } }, "node_modules/tldts": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", - "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.9" + "tldts-core": "^7.4.10" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", - "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", "dev": true, "license": "MIT" }, @@ -3606,9 +3605,9 @@ } }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": {