From c66b23f927ba336644c0b0a015c155110159b3f8 Mon Sep 17 00:00:00 2001 From: AV77web Date: Sat, 20 Jun 2026 23:26:06 +0200 Subject: [PATCH] primo commit --- .github/workflows/deploy.yml | 46 +++++++++++ .gitignore | 3 + docker-compose.yml | 56 +++++++++++++ dockerfile/Dockerfile | 28 +++++++ initdb/00_database.sql | 29 +++++++ initdb/01_schema.sql | 148 +++++++++++++++++++++++++++++++++++ initdb/02_seed.sql | 52 ++++++++++++ 7 files changed, 362 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 docker-compose.yml create mode 100644 dockerfile/Dockerfile create mode 100644 initdb/00_database.sql create mode 100644 initdb/01_schema.sql create mode 100644 initdb/02_seed.sql diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1f5cd13 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,46 @@ +name: Deploy to VPS + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: docker + + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Setup Node.js + uses: https://github.com/actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: npm ci --prefer-offline + + - name: Build + run: npm run build + + deploy: + needs: build + runs-on: docker + + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Deploy via SSH + uses: https://github.com/appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.AZURE_HOST }} + username: ${{ secrets.AZURE_USER }} + key: ${{ secrets.AZURE_SSH_KEY }} + script: | + echo "START DEPLOY" + cd /opt/apps/corrieribackend + git pull --ff-only + docker compose up -d --build \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f94630c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2894787 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +services: + app: + build: + context: . + dockerfile: dockerfile/Dockerfile + container_name: corrieribackend + restart: unless-stopped + env_file: + - .env + environment: + NODE_ENV: production + DB_HOST: mysql + DB_PORT: 3306 + PORT: 3005 + FRONTEND_URL: https://corrierifrontend.andreavillari.it + AUTH_URL: https://corrieribackend.andreavillari.it + AUTH_TRUST_HOST: "true" + ports: + - "127.0.0.1:3005:3005" + depends_on: + mysql: + condition: service_healthy + + mysql: + image: mysql:8.0 + container_name: mysql_corrieri + restart: always + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - dbdata:/var/lib/mysql + - ./initdb:/docker-entrypoint-initdb.d # opzionale: script SQL iniziali + ports: + - "127.0.0.1:3308:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 5s + timeout: 5s + retries: 10 + + phpmyadmin: + image: phpmyadmin:latest + container_name: phpmyadmin_corrieri + restart: always + ports: + - "127.0.0.1:8084:80" + environment: + PMA_HOST: mysql + depends_on: + - mysql + +volumes: + dbdata: \ No newline at end of file diff --git a/dockerfile/Dockerfile b/dockerfile/Dockerfile new file mode 100644 index 0000000..e4cfe39 --- /dev/null +++ b/dockerfile/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 +ARG NODE_VERSION=20 + +FROM node:${NODE_VERSION}-slim AS build +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY tsconfig.json tsconfig.build.json ./ +COPY src ./src +RUN npm run build + +FROM node:${NODE_VERSION}-slim AS runtime +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3001 + +COPY package*.json ./ +RUN npm ci --omit=dev + +COPY --from=build /app/dist ./dist +#COPY --from=build /app/src/openapi ./dist/openapi + +EXPOSE 3001 + +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/initdb/00_database.sql b/initdb/00_database.sql new file mode 100644 index 0000000..59bd14c --- /dev/null +++ b/initdb/00_database.sql @@ -0,0 +1,29 @@ +-- ============================================================ +-- Database: Gestione Prenotazioni Campi Sportivi (Prova S5) +-- Creazione database e utente applicativo +-- MySQL 8.0+ +-- ============================================================ +-- Con Docker Compose il database e l'utente vengono creati +-- automaticamente dalle variabili MYSQL_* nel file .env. +-- Questo script è utile per installazione manuale su MySQL locale. +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS prenotazioni + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +CREATE USER IF NOT EXISTS 'prenotazioni_user'@'%' + IDENTIFIED BY 'altra_password_sicura'; + +CREATE USER IF NOT EXISTS 'prenotazioni_user'@'localhost' + IDENTIFIED BY 'altra_password_sicura'; + +GRANT SELECT, INSERT, UPDATE, DELETE + ON prenotazioni.* + TO 'prenotazioni_user'@'%'; + +GRANT SELECT, INSERT, UPDATE, DELETE + ON prenotazioni.* + TO 'prenotazioni_user'@'localhost'; + +FLUSH PRIVILEGES; diff --git a/initdb/01_schema.sql b/initdb/01_schema.sql new file mode 100644 index 0000000..32f1f86 --- /dev/null +++ b/initdb/01_schema.sql @@ -0,0 +1,148 @@ +-- ============================================================ +-- Schema tabelle – Gestione Prenotazioni campi sportivi +-- Allineato alle API: /operatori, /clienti, /campi, /prenotazioni +-- ============================================================ + +USE prenotazioni; + +-- ------------------------------------------------------------ +-- Tabella operatore (API: GET/POST/PUT/DELETE /operatori) +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS operatore ( + OperatoreID INT UNSIGNED NOT NULL AUTO_INCREMENT, + Nome VARCHAR(100) NOT NULL, + Cognome VARCHAR(100) NOT NULL, + Email VARCHAR(255) NOT NULL, + PasswordHash VARCHAR(255) NOT NULL COMMENT 'Password salvata come hash (es. bcrypt)', + Admin BOOLEAN NOT NULL DEFAULT FALSE, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (OperatoreID), + UNIQUE KEY uk_operatore_email (Email) +) ENGINE=InnoDB + DEFAULT CHARSET=utf8mb4 + COLLATE=utf8mb4_unicode_ci; + + +-- ------------------------------------------------------------ +-- Tabella cliente (API: GET/POST/PUT/DELETE /clienti) +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS cliente ( + ClienteID INT UNSIGNED NOT NULL AUTO_INCREMENT, + Nome VARCHAR(100) NOT NULL, + Cognome VARCHAR(100) NOT NULL, + Telefono VARCHAR(30) NOT NULL, + Email VARCHAR(255) NOT NULL, + Note TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (ClienteID), + UNIQUE KEY uk_cliente_email (Email) +) ENGINE=InnoDB + DEFAULT CHARSET=utf8mb4 + COLLATE=utf8mb4_unicode_ci; + + +-- ------------------------------------------------------------- +-- Tabella camposportivo (API: GET/POST/PUT/DELETE /campi) +-- ------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS camposportivo ( + CampoID INT UNSIGNED NOT NULL AUTO_INCREMENT, + Nome VARCHAR(100) NOT NULL, + Tipo ENUM('tennis', 'padel', 'calcetto') NOT NULL, + Coperto BOOLEAN NOT NULL DEFAULT FALSE, + PrezzoOrario DECIMAL(10, 2) NOT NULL, + Attivo BOOLEAN NOT NULL DEFAULT TRUE, + + PRIMARY KEY (CampoID), + UNIQUE KEY uk_camposportivo_nome (Nome) +) ENGINE=InnoDB + DEFAULT CHARSET=utf8mb4 + COLLATE=utf8mb4_unicode_ci; + + +-- ---------------------------------------------------------------- +-- Tabella prenotazione (API: GET/POST/PUT/DELETE /prenotazioni) +-- ---------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS prenotazione ( + PrenotazioneID INT UNSIGNED NOT NULL AUTO_INCREMENT, + Data DATE NOT NULL, + OraInizio TIME NOT NULL, + OraFine TIME NOT NULL, + Stato ENUM('prenotata', 'confermata', 'annullata', 'completata') NOT NULL DEFAULT 'prenotata', + CodicePrenotazione VARCHAR(8) NOT NULL, + ClienteID INT UNSIGNED NOT NULL, + CampoID INT UNSIGNED NOT NULL, + + PRIMARY KEY (PrenotazioneID), + UNIQUE KEY uk_prenotazione_codice (CodicePrenotazione), + KEY idx_prenotazione_campo_data (CampoID, Data), + + CONSTRAINT ck_prenotazione_orario_valido + CHECK (OraFine > OraInizio), + + CONSTRAINT fk_prenotazione_cliente + FOREIGN KEY (ClienteID) + REFERENCES cliente (ClienteID) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CONSTRAINT fk_prenotazione_campo + FOREIGN KEY (CampoID) + REFERENCES camposportivo (CampoID) + ON UPDATE CASCADE + ON DELETE RESTRICT +) ENGINE=InnoDB + DEFAULT CHARSET=utf8mb4 + COLLATE=utf8mb4_unicode_ci; + + +-- ---------------------------------------------------------------- +-- Trigger anti-sovrapposizione (stesso campo, stessa data) +-- Le prenotazioni annullate non occupano lo slot. +-- ---------------------------------------------------------------- +DROP TRIGGER IF EXISTS trg_prenotazione_no_overlap_insert; +DROP TRIGGER IF EXISTS trg_prenotazione_no_overlap_update; + +DELIMITER $$ + +CREATE TRIGGER trg_prenotazione_no_overlap_insert +BEFORE INSERT ON prenotazione +FOR EACH ROW +BEGIN + IF NEW.Stato <> 'annullata' AND EXISTS ( + SELECT 1 + FROM prenotazione AS p + WHERE p.CampoID = NEW.CampoID + AND p.Data = NEW.Data + AND p.Stato <> 'annullata' + AND NEW.OraInizio < p.OraFine + AND p.OraInizio < NEW.OraFine + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'Sovrapposizione: esiste già una prenotazione attiva in questo orario per il campo selezionato'; + END IF; +END$$ + +CREATE TRIGGER trg_prenotazione_no_overlap_update +BEFORE UPDATE ON prenotazione +FOR EACH ROW +BEGIN + IF NEW.Stato <> 'annullata' AND EXISTS ( + SELECT 1 + FROM prenotazione AS p + WHERE p.PrenotazioneID <> NEW.PrenotazioneID + AND p.CampoID = NEW.CampoID + AND p.Data = NEW.Data + AND p.Stato <> 'annullata' + AND NEW.OraInizio < p.OraFine + AND p.OraInizio < NEW.OraFine + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'Sovrapposizione: esiste già una prenotazione attiva in questo orario per il campo selezionato'; + END IF; +END$$ + +DELIMITER ; diff --git a/initdb/02_seed.sql b/initdb/02_seed.sql new file mode 100644 index 0000000..ff3a387 --- /dev/null +++ b/initdb/02_seed.sql @@ -0,0 +1,52 @@ +-- ============================================================ +-- Dati iniziali di test – Gestione Prenotazioni campi sportivi +-- Password per tutti gli operatori: Password123! +-- +-- Operatori: +-- 1 Andrea Villari – admin@centro.it (Admin) +-- 2 Maria Bianchi – maria.bianchi@centro.it (operatore) +-- +-- Codici prenotazione per test /verifica-prenotazione?codice=... +-- TENN9001, PADL2406, CALC7788, TENN3344 +-- ============================================================ + +USE prenotazioni; + +-- ------------------------------------------------------------ +-- Operatori +-- ------------------------------------------------------------ +INSERT INTO operatore (Nome, Cognome, Email, PasswordHash, Admin) VALUES + ('Andrea', 'Villari', 'admin@centro.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', TRUE), + ('Maria', 'Bianchi', 'maria.bianchi@centro.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', FALSE); + +-- ------------------------------------------------------------ +-- Clienti +-- ------------------------------------------------------------ +INSERT INTO cliente (Nome, Cognome, Telefono, Email, Note) VALUES + ('Mario', 'Rossi', '3331112233', 'mario.rossi@email.it', 'Cliente abituale tennis'), + ('Laura', 'Verdi', '3334445566', 'laura.verdi@email.it', NULL), + ('Marco', 'Neri', '3337778899', 'marco.neri@email.it', 'Preferisce padel la sera'), + ('Giulia', 'Ferrari', '3332223344', 'giulia.ferrari@email.it', 'Squadra calcetto del venerdì'); + +-- ------------------------------------------------------------ +-- Campi sportivi +-- ------------------------------------------------------------ +INSERT INTO camposportivo (Nome, Tipo, Coperto, PrezzoOrario, Attivo) VALUES + ('Tennis Centro 1', 'tennis', FALSE, 25.00, TRUE), + ('Tennis Centro 2', 'tennis', TRUE, 30.00, TRUE), + ('Padel Arena', 'padel', TRUE, 40.00, TRUE), + ('Calcetto Est', 'calcetto', FALSE, 50.00, TRUE); + +-- ------------------------------------------------------------ +-- Prenotazioni (orari non sovrapposti sullo stesso campo) +-- CampoID: 1=Tennis Centro 1, 2=Tennis Centro 2, 3=Padel Arena, 4=Calcetto Est +-- ClienteID: 1=Mario, 2=Laura, 3=Marco, 4=Giulia +-- ------------------------------------------------------------ +INSERT INTO prenotazione (Data, OraInizio, OraFine, Stato, CodicePrenotazione, ClienteID, CampoID) VALUES + ('2026-06-20', '09:00:00', '10:00:00', 'confermata', 'TENN9001', 1, 1), + ('2026-06-20', '10:00:00', '11:00:00', 'prenotata', 'TENN3344', 2, 1), + ('2026-06-20', '15:00:00', '16:30:00', 'confermata', 'PADL2406', 3, 3), + ('2026-06-21', '18:00:00', '19:00:00', 'prenotata', 'CALC7788', 4, 4), + ('2026-06-21', '17:00:00', '18:00:00', 'completata', 'TENN5522', 1, 2), + ('2026-06-22', '09:00:00', '10:00:00', 'annullata', 'ANNU1234', 2, 1), + ('2026-06-22', '09:00:00', '10:00:00', 'confermata', 'TENN9876', 3, 1); \ No newline at end of file