Il progetto consiste in un'API Express.js per effettuare le prenotazioni di slot temporali di eventi, simile al servizio Doodle ( https://doodle.com/it/ ).
- Obiettivi
- Diagrammi UML
- Database
- Rotte applicazione
- Installazione ed avvio
- Testing
- Design Pattern Utilizzati
- Tool di sviluppo
- Miglioramenti e sviluppi futuri
Lo scopo è realizzare un back-end che consenta di effettuare le prenotazioni di slot temporali similmente al servizio Doodle ( https://doodle.com/it/ ). In particolare, il back-end deve prevedere che un utente possa effettuare chiamate (payload in JSON) per:
-
Creare un nuovo evento;
-
Restituire la lista degli eventi associati all’utente distinguendo per eventi aperti e chiusi;
-
Cancellare un evento se non è stata inserita alcuna preferenza;
-
Chiudere un evento, ovvero non consentire più alcuna votazione;
-
Restituire le prenotazioni associate all’evento.
Tutte le rotte richiedono autenticazione JWT.
Ogni utente autenticato ha un numero di token (valore iniziale impostato nel seed del database). Ad ogni creazione avvenuta con successo di un evento si deve decrementare i token associati all’utente considerando i seguenti costi:
-
1 token per Modalità 1
-
2 token per Modalità 2
-
4 token per Modalità 3
Nel caso di token terminati ogni richiesta da parte dello stesso utente deve restituire 401 Unauthorized.
Prevedere una rotta per l’utente con ruolo admin che consenta di effettuare la ricarica per un utente fornendo la mail ed il nuovo “credito” .
Sono stati riportarti i diagrammi delle sequenze più rappresentativi. E' stato riportato il Middleware Autenticazione, che è quello usato più spesso e con meno funzioni intermedie, risultando quindi più comprensibile e pulito.
E' stato riportato poi il diagrama delle sequenze della catena di Middleware più lunga ovvero quella della prenotazione di un Evento.
E' la catena di Middleware che si occupa di verificare che l'utente sia autenticato. Viene invocato in ogni rotta, nei prossimi diagrammi verrà indicato come Middleware Autenticazione sottointendo tutti i passaggi qui illustrati.
sequenceDiagram
participant Utente
participant middleAuth as Middleware Autenticazione
participant controllerUser as Controller Utente
participant dbFindOne as Postgres DB
Utente ->> middleAuth : Richiesta
middleAuth ->> middleAuth.checkHeader : Middle Succ
alt Header Valido
middleAuth.checkHeader ->> middleAuth.checkToken : Middle Succ
alt Token Valido
middleAuth.checkToken ->> middleAuth.verifyAndAuthenticate : Middle Succ
alt Utente Autenticato
middleAuth.verifyAndAuthenticate ->> middleAuth.checkUserReq : Controlli Superati
alt Utente da Cercare
middleAuth.checkUserReq -->> controllerUser : Richiesta Controller
controllerUser ->> dbFindOne : Ricerca Utente DB
dbFindOne -->> controllerUser : Utente Trovato
controllerUser -->> middleAuth : Successo
middleAuth -->> Utente : Successo
else Utente Non Trovato DB
middleAuth -->> Utente : Errore Autenticazione 404
end
else Utente Non Autenticato
middleAuth.verifyAndAuthenticate -->> Utente : Errore Autenticazione 401
end
else Token Non Valido
middleAuth.checkToken -->> Utente : Errore Autenticazione 401
end
else Header Non Valido
middleAuth.checkHeader -->> Utente : Errore Autenticazione 412
end
Sono stati omessi alcuni passaggi intermedi d'interazione con il Controller per evitare di rendere il diagramma incomprensibile e confusionario. L'intento è quello di mostrare la catena ed il flusso di Middleware e come solo al termine della stessa viene effettuata la Prenotazione Evento nel DB.
Sono indicati, in caso di errore, tutti gli HTTP Status Code.
Da notare che ogni Middleware in caso di errore interrompe il flusso e restituisce l'errore all'Utente.
sequenceDiagram
participant Client as Utente
participant middlewareAuth as Middleware Autenticazione
participant middleEvent as Middleware Prenotazione Evento
participant controller as Controller Evento
participant Database as Postgres DB
Client ->> middlewareAuth : Richiesta
middlewareAuth ->> middleEvent : Esecuzione
alt Autenticazione Valida
middleEvent ->> middleEvent.checkBookEventBody : Esecuzione
alt Body Corretto
middleEvent.checkBookEventBody ->> middleEvent.checkEventExistence : Middle Succ
alt Evento Esistente
middleEvent.checkEventExistence ->> middleEvent.checkEventStatus : Middle Succ
alt Evento Aperto
middleEvent.checkEventStatus ->> middleEvent.checkDatetimesExistence : Middle Succ
alt Datitme Esistente
middleEvent.checkDatetimesExistence ->> middleEvent.checkBookingExistence : Middle Succ
alt Prenotazione Esistente
middleEvent.checkBookingExistence ->> middleEvent.getEventMode : Middle Succ
alt Evento Mode 2
middleEvent.getEventMode ->> middleEvent.checkBookingSecondMode : Middle Succ
middleEvent.checkBookingSecondMode -->> middleEvent : Controlli Passati
else Evento Mode 3
middleEvent ->> middleEvent.checkBookingThirdMode : Middle Succ
middleEvent.checkBookingThirdMode -->> middleEvent : Controlli Passati
end
alt Tutti i Middleware superati con Successo
middleEvent ->> controller : Richiesta al Controller
controller ->> Database : Aggiunta Prenotazioni DB (Update)
Database -->> controller : Risposta Aggiunta Prenotazioni
controller -->> Client : Successo 200
else Almeno un Middleware ha Fallito
end
else Prenotazione Non Esistente
middleEvent.checkBookingExistence -->> Client : Errore 409
end
else Datetime Non Esistente
middleEvent.checkDatetimesExistence -->> Client : Errore 404
end
else Evento Chiuso
middleEvent.checkEventStatus -->> Client : Errore 403
end
else Evento Inesistente
middleEvent.checkEventExistence -->> Client : Errore 404
end
else Body Non Corretto
middleEvent.checkBookEventBody -->> Client : Errore 422
end
else Autenticazione Non Valida
middlewareAuth -->> Client : Errore 401, 404 o 412
end
All'avvio dell'app il database viene populato tramite il file seed.sql, con la creazione della tabella User e Event.
email VARCHAR NOT NULL,
name VARCHAR NOT NULL,
surname VARCHAR NOT NULL,
role VARCHAR NOT NULL DEFAULT 'user', # o 'admin'
token INTEGER NOT NULL check (token >= 0),
PRIMARY KEY (email)
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
owner VARCHAR NOT NULL REFERENCES "user" (email) ON DELETE CASCADE ON UPDATE CASCADE,
mode INTEGER NOT NULL check (mode between 1 and 3),
datetimes TIMESTAMP WITH TIME ZONE[] NOT NULL,
status INTEGER NOT NULL DEFAULT 1 check (status between 0 and 1),
latitude FLOAT check (latitude between -90 and 90),
longitude FLOAT check (longitude between -180 and 180),
link VARCHAR,
bookings JSONB DEFAULT NULL #Postgres gestisce efficientemente i file JSON
Ad ogni tabella corrisponde un Model in Sequelize (cartella Modelli).
Le rotte partono dall'indirizzo http://localhost:3000/api. La seguente tabella riporta tutte quelle disponibili:
Rotta | Metodo | Autenticazione JWT | Ruolo utente |
---|---|---|---|
/create-event | POST | SI | qualsiasi |
/show-events | GET | SI | qualsiasi |
/show-bookings | GET | SI | qualsiasi |
/show-info-user (non richiesta) | GET | SI | admin |
/close-event | POST | SI | qualsiasi |
/delete-event | DELETE | SI | qualsiasi |
/update-token | POST | SI | admin |
/book-event | POST | SI | qualsiasi |
Le rotte che richiedono autenticazione JWT ricevono un token Bearer inserito nell'header della richiesta generato dalla chiave privata inserita nel file .env (da creare come indicato in Installazione ed Avvio). Nelle descrizioni dettagliate delle rotte sono riportati il body in JSON.
Il campo email e il campo role fanno riferimento all'utente che effettua la richiesta e si trovano nel payload del token JWT.
{
"email":"pippo@gmail.com",
"role":"user"
}
ATTENZIONE! Se il body di ogni richiesta non è ben strutturato (come nei dettagli sotto) viene restituito un errore 422 Malformed body.
POST /create-event
Crea un evento con owner l'utente che ha effettuato la richiesta (non viene inserito nel body perchè viene preso dal token JWT). Se l'utente non ha token sufficienti per la creazione dell'evento non viene creato (errore 401 come richiesto).
Le date di ogni prenotazione vanno inserite singolarmente per evitare errori nella prenotazione, ovvero per ogni slot prenotato aggiungere una booking diversa, sempre strutturata come sotto (array)
{ "title": "Riunione Mattutina", "datetimes": [ "2023-07-11 10:00:00+01", "2023-09-15T08:00:00.000Z", "2023-09-15T10:00:00.000Z" ], "mode": 2, "latitude": 43.52555, "longitude": 13.20437, "link": "https://google.it", "bookings": [ { "user": "ciccio@gmail.com", "datetimes": ["2023-09-15T08:00:00.000Z"] } ] }
POST /close-event
Chiude le prenotazioni di un evento, solo se la richiesta viene effettuata dall'owner
{ "event_id": 2 }
DELETE /delete-event
Cancella un evento se la richiesta viene effettuata dall'owner e se l'evento non ha prenotazioni
{ "event_id": 6 }
GET /show-events
Visualizza tutti gli eventi di cui l'utente che sta effettuando la richiesta è il proprietario (owner). Non è richiesto un body perchè l'utente viene preso dal token JWT
{}
GET /show-info-user
Visualizza tutte le informazioni di un singolo utente. Rotta accessibile solo da utente amministratore
{ "user":"alessio@gmail.com" }
GET /show-bookings
Visualizza tutte le prenotazioni di un singolo evento (di tutti gli utenti, non è necessario esserne il proprietario)
{ "event_id": 3 }
POST /update-token
Sostituisce i token disponibili nell'utente indicato nel campo update_user con update_amount. Rotta accessibile solo da utente amministratore
{ "update_amount": 20, "update_user":"alessio@gmail.com" }
POST /book-event
Prenotazione di slots di un evento. Viene controllata la modalità dell'evento e di conseguenza vengono effettuati i controlli sulla correttezza della prenotazione. Non è possibile mai avere dei doppioni e quindi prenotare due volte lo stesso slot (con lo stesso utente)
{ "event_id": 0, "datetimes": ["2023-09-15T10:00:00.000Z"] }
- Docker e Docker Compose
- Git (per clonare la repository, altrimenti si può scaricare direttamente da GitHub)
-
Clonare la repository:
git clone https://github.com/alexpaulofficial/progettoProgAvanzata.git
-
Creare un file .env nella root della cartella del progetto con i seguenti campi (modificare a piacimento, inserendo obbligatoriamente la chiave segreta)
SECRET_KEY='' # chiave per generare JWT (progettoProgAvanzata) # variabili per database Postgres POSTGRES_HOST='db' POSTGRES_DB='progettoProgAvanzata' POSTGRES_USER='postgres' POSTGRES_PASSWORD='password123'
-
Avviare con Docker Compose (sempre dalla cartella principale del progetto):
$ docker compose up
-
Il servizio è attivo nella porta 3000
É possibile eseguire una serie di test predefiniti importando la collection Postman situata nella cartella testing. I test comprendono vari casi di errore, dall'assenza di token all'impossibilità di prenotare uno slot già prenotato. Nella rotta show-info-user non sono stati inseriti test, non essendo richiesta, ma comunque la rotta viene controllata ed è stata implementata completamente/correttamente.
Per la generazione del token JWT va dichiarata una variabile globale su Postman della chiave privata che si chiama SECRETKEY
ATTENZIONE! I test danno esito 100% positivo se lanciati appena installato il servizio. Chiaramente se si fanno modifiche ai token degli utenti o si prenotano slot specifici i test potrebbero dare errore se lanciati di nuovo.
Il pattern MVC (Model-View-Controller) è un Architectural Design Pattern. Esso suddivide l'applicazione in tre componenti distinti: il Modello (Model), la Vista (View) e il Controller (Controller). In questo specifico progetto non sono state implementate Viste, trattandosi di un API solo back-end.
Il Modello rappresenta la business logic dell'applicazione ed è responsabile della manipolazione dei dati. Nel progetto ci sono i Modelli delle due entità Utente ed Evento ed il modello Database che definisce l'istanza di Sequelize.
Il Controller funge da intermediario tra vari Middleware ed il Modello. Esso riceve le richieste, già processate dai vari Middleware e le gestisce, chiamando i metodi appropriati nel Modello per elaborare la richiesta e ottenere i dati necessari. Il Controller è responsabile della logica di controllo dell'applicazione, che può includere la validazione dei dati, la gestione degli errori e la gestione del flusso di controllo.
Questa suddivisione favorisce una struttura modulare e mantenibile, senza mescolare la business logic con la logica di presentazione o controllo, favorendo la scalabilità del codice.
Il pattern Singleton è un Creational Design Pattern utilizzato per garantire che una classe abbia una sola istanza e fornisca un punto di accesso globale a tale istanza. Nel caso specifico del progetto l'istanza di Sequelize sarà unica in tutto l'applicativo, e tutte le richieste per accedere al database utilizzeranno la stessa istanza.
Sequelize è utilizzato come ORM (Object-Relational Mapping) per gestire le operazioni sul database e poiché è una risorsa costosa da inizializzare e configurare, è desiderabile che venga creato in un'unica istanza e che questa istanza venga appunto condivisa da tutte le parti dell'applicazione che necessitano di accedere al database.
Quando viene richiesto l'utilizzo di Sequelize, l'applicazione verifica se un'istanza è già stata creata in precedenza. Se l'istanza non esiste, ne viene creata una nuova e memorizzata, se invece esiste già, viene restituita quella esistente. Questo contribuisce a una migliore gestione delle risorse e a un codice più pulito e organizzato.
La Chain of Responsability (CoR) è un Behavioural Design Pattern e permette di processare una richiesta attraverso l'esecuzione di funzioni collegate tra loro in un determinato ordine. In questo progetto la CoR è realizzata tramite le funzionalità dei Middleware i quali rappresentano i veri e propri anelli della catena.
Tale pattern è stato utilizzato per filtrare le richieste HTTP in modo da far pervenire al Controller solamente quelle corrette; per ogni rotta è stata definita una catena di Middleware composta da Middleware per il controllo dell'header e del token JWT (ove necessario) oppure da Middleware specifici della rotta (controllo sui tipi, sull'integrità dei dati, sui vincoli del database...) che restituiscono errore dove necessario.
Il pattern Middleware è un Behavioural Design Pattern utilizzato per gestire richieste e risposte in modo flessibile e modulare. Le richieste all'API vengono gestite da una serie di funzioni Middleware, ognuna delle quali esegue una specifica operazione logica e può decidere di passare la richiesta al successivo Middleware nella catena tramite next() o interrompere la catena e restituire la res (Risposta) al client.
In questo specifico progetto, la catena di Middleware viene utilizzata per effettuare controlli sulla richiesta prima di procedere con l'esecuzione del Controller che gestirà l'operazione richiesta dal client, come indicato in CoR. Se uno dei Middleware fallisce nel processo di verifica, la catena interrompe il flusso e restituisce una risposta di errore al client. Se tutti i Middleware superano i controlli, la richiesta viene inoltrata al Controller che effettua la richiesta al database tramite Model.
Questo approccio modulare consente di separare le diverse fasi di controllo della richiesta e favorisce la riutilizzabilità del codice, facilitando inoltre la manutenzione e l'aggiunta di nuovi controlli nel flusso senza dover modificare il core dell'applicazione. Inoltre, rende il codice più leggibile e facile da comprendere, poiché ogni Middleware si concentra solo su una specifica operazione.
Il pattern Router è un Design Pattern utilizzato per gestire il routing delle richieste in un'applicazione web. Il suo obiettivo principale è organizzare in modo strutturato il flusso delle richieste da parte degli utenti e di inviarle ai gestori appropriati per l'elaborazione. In questo modo, si assicura una gestione efficace delle diverse funzionalità dell'applicazione e una chiara separazione delle responsabilità tra i diversi percorsi.
In Express.js il Router è rappresentato da un oggetto Router che funge da modulo di routing autonomo. Esso permette di definire rotte specifiche (definite nel file routes.ts) e associare ad ognuna di esse dei gestori che si occuperanno di rispondere alle richieste in arrivo. L'utilizzo di Router consente una migliore organizzazione del codice, poiché le rotte e i relativi gestori possono essere raggruppati e montati nell'applicazione principale utilizzando il metodo app.use() per creare una gerarchia chiara dei percorsi.
Inoltre, sfruttando il pattern Router, è possibile separare la logica di routing in moduli distinti. Questo offre il vantaggio di rendere il codice più modulare, facilitando la manutenibilità e la scalabilità dell'applicazione. I moduli possono gestire funzionalità specifiche o gruppi di rotte correlate, rendendo il codice più leggibile, comprensibile e organizzato.
- Node.JS
- Express
- PostgreSQL
- Sequelize
- Docker
- Postman
- JWT
- Libreria gestione date (Moment.js)
- Libreria risposte HTTP (http-response-status)
-
Introdurre più filtri nella creazione e gestione eventi
-
Utilizzo di Builder e Factory sia per generare errori, sia per generare Middleware
-
Front-end
-
...