- Introduzione
- Struttura dell'Applicazione
- Configurazione dell'applicazione
- Scelte implementative e features
Questo documento rappresenta la relazione del progetto "Social Network for Music", sviluppato nel contesto del corso "Programmazione e Linguaggi per il Web" durante l'anno accademico 2023.
Il progetto è stato realizzato da:
- Alessandro Riemer (Matricola: 941878)
- Kevin Manca (Matricola: 978578)
- è disponibile una demo (AWS hosted) presso questo link
- La pagina GitHub del progetto si trova a questo link
Front End: Il Front-End è la parte dell'applicazione che si occupa dell'interfaccia utente e dell'interazione con l'utente. Si concentra sulla progettazione e sull'implementazione dell'aspetto visivo dell'applicazione e sulla gestione delle interazioni utente.
All'interno della directory /src/html/
, sono presenti i seguenti elementi principali:
-
Elementi HTML: Questi file definiscono l'interfaccia grafica dell'applicazione, determinando come l'applicazione appare nel browser.
-
/css/
: Questa directory contiene i file di stile che definiscono l'aspetto visivo dell'applicazione. Alcuni dei file principali includono:- /src/html/css/confirmationmodal.css
- /src/html/css/default.css
- /src/html/css/explore.css
-
/src/scripts/
: Questa directory contiene file JavaScript (JScript) che gestiscono la logica del Front-End. Alcuni dei file principali includono:- /src/html/community.html
- /src/html/createcommunity.html
- /src/html/createplaylist.html
- /src/html/editplaylist.html
- /src/html/explore.html
- /src/html/login.html
- /src/html/playlist.html
- /src/html/profile.html
- /src/html/register.html
- /src/html/search.html
La suddivisione chiara tra file HTML, file CSS e file JavaScript (JScript) consente una gestione efficiente del Front-End e garantisce un'esperienza utente di alta qualità.
Per ulteriori dettagli sull'implementazione del Front-End, si rimanda alle specifiche sezioni dei file e dei componenti menzionati sopra.
Back End: Il Back-End è responsabile delle funzionalità e della logica dell'applicazione lato server. Esso comprende una serie di elementi chiave presenti nella nostra struttura di lavoro. Possiamo suddividere il backend in 3 sezioni principali
Node.js ed Express costituiscono un binomio potente nell'ambito dello sviluppo web di applicazioni scalabili ed efficienti.
Node.js fornisce un ambiente runtime JavaScript server-side, ottimizzato per l'efficienza e la scalabilità.
Express, un framework web basato su Node.js, semplifica la creazione di applicazioni web, offrendo funzionalità come la gestione delle richieste HTTP e dell'autenticazione.
Ulteriori info a questa pagina.
-
/serverlogs/
: Questa directory contiene il file di log 'serverlogs.log', che registra i log del server per monitorare il suo funzionamento e le policy associate. -
/src/api/docs/
: In questa directory sono presenti i file utilizzati per la gestione della documentazione pubblica delle nostre API, inclusi:- swagger.js
- swagger_output.js
-
/src/config/
: Questa cartella contiene i file dedicati alla configurazione dell'applicazione, ad eccezione delle variabili d'ambiente. Al suo interno, sono presenti:- prefs.js
-
/src/lib/
: La directory lib contiene tutte le funzioni Node.js utilizzate per le funzionalità degli endpoint. Inoltre, include le immagini pubbliche del sito. Alcuni dei file e delle directory principali sono:- /src/lib/auth.js
- /src/lib/community.js
- /src/lib/database.js
- /src/lib/login.js
- /src/lib/playlist.js
- /src/lib/register.js
- /src/lib/user.js
- /src/lib/utils.js
- /src/lib/spotify/fetch.js
- /src/lib/spotify/token.js
-
app.js
: Questo file rappresenta il punto di ingresso principale dell'applicazione, contenente le istruzioni per l'avvio dell'app e la definizione degli endpoint.
La struttura ben organizzata del Back-End garantisce una gestione efficiente delle funzionalità server-side e contribuisce al corretto funzionamento dell'applicazione.
Nel corso di sviluppo della nostra applicazione, abbiamo fatto largo uso del database MongoDB. Qui di seguito, presentiamo le collezioni che abbiamo creato e utilizzato per immagazzinare i dati essenziali dell'applicazione.
MongoDB: MongoDB è un database NoSQL (non relazionale), flessibile e scalabile, noto per la sua struttura orientata ai documenti. Un documento è un record dati in formato BSON (Binary JSON) che può contenere dati di varie forme e dimensioni. Ogni documento è organizzato in collezioni, offrendo flessibilità nella modellazione dei dati.
Per questa applicazione abbiamo deciso di utilizzare le seguenti collections:
Di seguito viene riportata una descrizione delle collections, del loro schema di validation JSON e dei tipi di dato
Validazione JSON: La validazione JSON è un processo cruciale per garantire che i dati immagazzinati nei database siano coerenti e rispettino gli standard dell'applicazione. Definendo regole specifiche per la struttura e il formato dei dati, la validazione riduce il rischio di errori e contribuisce all'affidabilità e all'integrità del sistema.
La collezione community ha lo scopo di raccogliere informazioni relative alle comunità all'interno della nostra applicazione.
{
"$jsonSchema": {
"bsonType": "object",
"required": [
"_id",
"creatorId",
"name"
],
"properties": {
"_id": {
"bsonType": "objectId",
"description": "_id must be an ObjectId and is required"
},
"creatorId": {
"bsonType": "objectId",
"description": "creatorId must be an ObjectId and is required"
},
"name": {
"bsonType": "string",
"description": "name must be a string and is required"
},
"desc": {
"bsonType": "string",
"description": "desc must be a string"
},
"members": {
"bsonType": "array",
"description": "members must be an array of ObjectIds"
},
"playlists": {
"bsonType": "array",
"description": "playlists must be an array of ObjectIds"
}
}
}
}
-
_id: identificatore univoco di una community, di tipo ObjectId. È un campo obbligatorio per identificare univocamente una community nel db.
-
creatorId: identificatore dell'utente creatore della community, di tipo ObjectId. È un campo obbligatorio e serve a linkare la community al suo creatore.
-
name: nome della community, di tipo stringa. È un campo obbligatorio e contiene il nome della community.
-
desc: rappresenta la descrizione della community, di tipo stringa. È un campo facoltativo e contiene una descrizione testuale della community.
-
members: lista di membri della community, di tipo array. Contiene una serie di ObjectId che identificano gli utenti che fanno parte della community.
-
playlists: lista di playlist associate alla community, di tipo array. Contiene una serie di ObjectId che identificano le playlist associate a questa community.
DESCRIZIONE
La collezione playlists è stata creata per rappresentare le playlist musicali all'interno della nostra applicazione.
{
"$jsonSchema": {
"bsonType": "object",
"required": [
"_id",
"owner_id",
"title"
],
"properties": {
"_id": {
"bsonType": "objectId",
"description": "_id must be a ObjectId and is required"
},
"owner_id": {
"bsonType": "objectId",
"description": "owner_id must be a ObjectId and is required"
},
"title": {
"bsonType": "string",
"description": "title must be a string and is required"
},
"description": {
"bsonType": "string",
"description": "name must be a string"
},
"tags": {
"bsonType": "array",
"description": "name must be an array of ObjectIds"
},
"songs": {
"bsonType": "array",
"description": "name must be a array of ObjectIds"
}
}
}
}
-
_id: identificatore univoco di una playlist, di tipo ObjectId. È un campo obbligatorio per identificare univocamente una playlist nel database.
-
owner_id: identificatore dell'utente proprietario della playlist, di tipo ObjectId. È un campo obbligatorio e serve a linkare la playlist al suo proprietario.
-
title: titolo della playlist, di tipo stringa. È un campo obbligatorio e contiene il titolo della playlist.
-
description: descrizione della playlist, di tipo stringa. È un campo facoltativo e contiene una descrizione testuale della playlist.
-
tags: lista di tag associati alla playlist, di tipo array. Contiene una serie di strings che identificano i tag associati a questa playlist.
-
songs: lista di brani musicali presenti nella playlist, di tipo array. Contiene una serie di ObjectIds che identificano i brani musicali presenti in questa playlist.
La collezione users è destinata a contenere i dati degli utenti all'interno dell'applicazione.
{
"$jsonSchema": {
"bsonType": "object",
"required": [
"_id",
"nickname",
"email",
"password"
],
"properties": {
"_id": {
"bsonType": "objectId",
"description": "_id must be an ObjectId and is required"
},
"name": {
"bsonType": "string",
"description": "name must be a string and is required"
},
"nickname": {
"bsonType": "objectId",
"description": "nickname must be an ObjectId and is required"
},
"email": {
"bsonType": "string",
"description": "email must be a string and is required"
},
"password": {
"bsonType": "string",
"description": "password must be a string"
},
"date": {
"bsonType": "string",
"description": "date must be a string"
},
"genres": {
"bsonType": "array",
"description": "genres must be an array of ObjectIds"
}
}
}
}
-
_id: identificatore univoco di un utente, di tipo ObjectId. È un campo obbligatorio per identificare univocamente un utente nel database.
-
name: nome dell'utente, di tipo stringa. È un campo obbligatorio e contiene il nome dell'utente.
-
nickname: nickname dell'utente, di tipo ObjectId. È un campo obbligatorio e serve a collegare il soprannome dell'utente.
-
email: indirizzo email dell'utente, di tipo stringa. È un campo obbligatorio e contiene l'indirizzo email dell'utente.
-
password: password dell'utente, di tipo stringa. È un campo facoltativo e contiene la password dell'utente.
-
date: data associata all'utente, di tipo stringa. È un campo facoltativo e contiene una data associata all'utente.
-
genres: lista di generi musicali preferiti dall'utente, di tipo array. Contiene una serie di ObjectIds che identificano i generi musicali preferiti dall'utente.
Il sistema di logging o auditing in un'applicazione web rappresenta un componente fondamentale per la tracciabilità delle operazioni e la gestione degli errori.
Questo sistema registra le attività cruciali e gli errori nell'applicazione, offrendo una visione dettagliata delle interazioni e delle problematiche riscontrate.
In questa applicazione ho realizzato questo meccanismo tramite due funzioni presenti nel file utils.js: log e logonly.
-
log(msg)
: effettua operazioni di log e auditing -
logonly(msg)
: effettua solo operazioni di log
Le operazioni di Log e Auditing funzionano nel seguente modo
- Log: viene salvato l'evento nel file serverlogs.log nel seguente formato: [timestamp]:MESSAGGIO
- Auditing: Stampa a video tramite console.log(msg) il messaggio insieme al timestamp per permettere all'admin di vedere gli eventi importanti in modalità live
export function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}]: ${message}\n`;
var stream = fs.createWriteStream("serverlogs/serverlogs.log", {flags:'a'});
stream.write(logMessage);
console.log(logMessage);
}
export function logonly(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}]: ${message}\n`;
var stream = fs.createWriteStream("serverlogs/serverlogs.log", {flags:'a'});
stream.write(logMessage);
}
if (!isValidNickname(nickname)) {
res.status(404).send("invalid nickname");
utils.log("[REGISTER]> register > ERROR 400: invalid nickname");
return;
}
[2023-09-20T17:06:01.826Z]: [REGISTER]> register > ERROR 400: invalid nickname
Il progetto necessita di un file .env
nella directory principale del dove sono contenuti i
parametri necessari per il funzionamento.
Il file .env
è gestito attraverso il pacchetto npm dotenv
che si occupa di popolare le relative variabili d'ambienti e renderne semplice l'utilizzo e accesso
tramite JavaScript.
Un esempio di file env
# Server HOST and PORT
HOST='localhost'
PORT=3000
# Parametri e Credenziali MongoDB
DATABASE='mongodb-cluster'
DB_NAME='mongodb-name'
DB_URI="mongodb+srv://user:[email protected]_id.mongodb.net/"
# Parametri e Credenziali Spotify
BASE_URL="https://api.spotify.com/v1"
TOKEN_URL="https://accounts.spotify.com/api/token"
CLIENT_ID='token_client_generated_from_spotify'
CLIENT_SECRET='token_generated_from_spotify'
Swagger: è un framework open-source per la progettazione, la creazione e la documentazione di API RESTful. La sua utilità si concentra sulla semplificazione del processo di sviluppo API, consentendo agli sviluppatori di definire chiaramente le specifiche delle API, testarle e generare automaticamente documentazione dettagliata.
Per la generazione dello swagger ho utilizzato il module swagger-autogen.
Tramite la creazione di un file swagger.js (/src/docs/) con una apposita configurazione e determinati commenti nella sezione degli endpoint, è possibile generare automaticamente una documentazione per gli endpoint.
è possibile visualizzare lo swagger generato all'endpoint /api-docs
NB: Il codice riportato di seguito non è completo, rappresenta solo un esempio molto vicino a quello utilizzato in questa applicazione!
const doc = {
info: {
version: "1.0.0",
title: "SNM API",
description: "Documentation for the APIs of our website: Social Network for Music."
},
host: `${config.host}:${config.port}`,
basePath: "/",
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{
"name": "fetch",
"description": "Endpoints for fetching and searching content."
},
{
"name": "users",
"description": "..."
},
{
"name": "auth",
"description": "..."
},
{
"name": "playlist",
"description": "..."
},
{
"name": "community",
"description": "..."
},
{
"name": "tracks",
"description": "..."
},
{
"name": "misc",
"description": "..."
},
{
"name": "artists",
"description": "..."
}
]
,
definitions: {
...
user: {
_id: "ObjectId('64df73b31e5eda5eb868ddcd')",
name: "Joe",
nickname: "joedough",
surname: "Joe",
email: "[email protected]",
password: "md5 hashed password",
date: "2001-09-11",
genres: {
0: "pop",
1: "rock",
2: "metal"
}
},
playlists: {
_id: "ObjectId('64e748f0cb18ad90657b9043')",
owner_id: "ObjectId('64df73b31e5eda5eb868ddcd')",
title: "Example Playlist",
description: "Description of playlist",
public: true,
tags: {
0: "chill",
1: "relax",
2: "vibes"
},
songs: {
0:{
title: "Song 1",
artist: "Artist1, Artist2, Artist3",
duraion: "00:01:11"
},
1:{
title: "Song 2",
artist: "Artist1, Artist2, Artist3",
duraion: "00:02:22"
},
2:{
title: "Song 3",
artist: "Artist1, Artist2, Artist3",
duraion: "00:03:33"
}
},
private: true
},
song: {
$_id: "78kf73b31e6yda5eb868dder",
$artist:"['artist1','artist2']",
$duration: "00:11:22",
$year: "1984",
$album: "Album Name"
}
...
}
}
const generateSwagger = async () => {
try {
await swaggerAutogen()(outputFile, endpointsFiles,doc);
utils.log('SWAGGER DOCUMENTATION GENERATED.');
} catch (error) {
utils.error('ERROR WHILE GENERATING SWAGGER DOCUMENTATION:', error);
}
};
generateSwagger();
npm install --save-dev swagger-autogen
ulteriori informazioni sono presenti al link sopra riportato
La maggior parte delle funzioni ( principalmente back-end ) in questa applicazione sono state descritte tramite la convenzione jsdoc
La convenzione JSDoc, ampiamente utilizzata nella programmazione JavaScript, consiste nell'includere commenti strutturati nel codice per documentare funzioni, classi e metodi. Questi commenti migliorano la chiarezza del codice, facilitano la comprensione e consentono la generazione automatica di documentazione tecnica. Questo standard è cruciale per progetti complessi e la collaborazione tra sviluppatori.
Di seguito un esempio di un commento utilizzando lo standard JavaDoc
/**
* Retrieves a playlist by its ID.
*
* @description This function retrieves a playlist by its unique ID.
* It checks the validity of the provided
* playlist ID and returns the playlist data if found.
* If the playlist does not exist, it returns a 404 Not Found response.
* In case of any unexpected errors, it sends a 500 Internal Server Error response.
* @param {Object} res - The HTTP response object.
* @param {string} playlistid - The ID of the playlist to retrieve.
*
* @returns {void}
*
* @throws {Object} 400 Bad Request if the playlist ID is missing or invalid.
* @throws {Object} 404 Not Found if the playlist with the provided ID does not exist.
* @throws {Object} 500 Internal Server Error if any unexpected error occurs during the operation.
*
*/
export async function getPlaylistFromId(res, playlistid) {
if(playlistid==undefined){
res.status(400).send("Missing playlist id");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 400: Missing playlist id");
return;
}
if(!utils.isValidString(playlistid)){
res.status(400).send("Invalid playlistid");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 400: Invalid playlist id");
return;
}
try {
const collection = await dbPlaylistCollection();
const playlist = await collection.findOne({ _id: new ObjectId(playlistid) });
if (!playlist) {
res.status(404).send("Playlist not found");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 404: Playlist not found");
return;
}
res.json(playlist);
utils.log("[PLAYLIST]> getPlaylistFromId > SUCCESS: SUCCESFULLY FETCHED PLAYLIST "+playlistid);
return;
} catch (error) {
res.status(500).send("INTERNAL ERROR");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 500: INTERNAL ERROR "+error);
return;
}
}
I codici HTTP sono standard utilizzati per indicare lo stato di una richiesta HTTP effettuata tra un client (spesso un browser web) e un server. Nell'applicazione, vengono ampiamente utilizzati alcuni di questi codici per comunicare lo stato delle richieste e delle risposte:
-
Codice 400 (BAD REQUEST): Questo codice indica che la richiesta effettuata dal client è stata malformata o non valida. Viene utilizzato quando i dati inviati non corrispondono alle aspettative del server.
-
Codice 401 (UNAUTHORIZED): Indica che l'accesso a una risorsa richiede l'autenticazione.
-
Codice 404 (NOT FOUND): Indica che la risorsa richiesta non è stata trovata sul server.
-
Codice 500 (INTERNAL SERVER ERROR): Questo codice indica un errore interno del server.
-
Codice 200 (OK): Codice di successo. Indica che la richiesta è stata elaborata correttamente e che il server sta restituendo i dati richiesti al client.
La gestione che abbiamo deciso di attuare è stata quella di comunicare al sender il codice che la sua richiesta ha "generato" Nell'esempio di seguito è possibile vedere la gestione dei codici 400,404,500 200
NB: res.json(data) viene percepito dal client come un codice 200
export async function getPlaylistFromId(res, playlistid) {
if(playlistid==undefined){
res.status(400).send("Missing playlist id");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 400: Missing playlist id");
return;
}
if(!utils.isValidString(playlistid)){
res.status(400).send("Invalid playlistid");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 400: Invalid playlist id");
return;
}
try {
const collection = await dbPlaylistCollection();
const playlist = await collection.findOne({ _id: new ObjectId(playlistid) });
if (!playlist) {
res.status(404).send("Playlist not found");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 404: Playlist not found");
return;
}
res.json(playlist);
utils.log("[PLAYLIST]> getPlaylistFromId > SUCCESS: SUCCESFULLY FETCHED PLAYLIST "+playlistid);
return;
} catch (error) {
res.status(500).send("INTERNAL ERROR");
utils.log("[PLAYLIST]> getPlaylistFromId > ERROR 500: INTERNAL ERROR "+error);
return;
}
}
La scelta di utilizzare la lingua inglese, come standard di programmazione, è ampiamente diffusa nell'industria del software ed è guidata principalmente dal desiderio di aderire allo standard internazionale. Questo standard è anche noto nella community di programmatori come "English-based programming" .
Adottare questa convenzione ha numerosi vantaggi, in quanto rende il codice più leggibile e comprensibile per un pubblico globale di sviluppatori.