Initial commit
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
|||||||
|
# Changelog - Scommetti App
|
||||||
|
|
||||||
|
Questo file tiene traccia di tutte le modifiche ad alto livello del progetto.
|
||||||
|
|
||||||
|
## Struttura del Progetto
|
||||||
|
|
||||||
|
```
|
||||||
|
scomettiapp/
|
||||||
|
├── backend-node/ # Backend Node.js con Express
|
||||||
|
├── frontend-angular/ # Frontend Angular 20
|
||||||
|
├── frontend-flutter/ # Frontend Flutter (placeholder)
|
||||||
|
└── docker-compose.yml # Orchestrazione dei servizi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Completato
|
||||||
|
|
||||||
|
### Infrastruttura Base
|
||||||
|
- [x] Setup struttura progetto con backend Node.js e frontend Angular
|
||||||
|
- [x] Configurazione Docker Compose per orchestrazione servizi
|
||||||
|
- [x] Setup database PostgreSQL per dati relazionali (utenti, gruppi, amicizie)
|
||||||
|
- [x] Setup database MongoDB per scommesse
|
||||||
|
- [x] Script di seed per popolamento dati di test
|
||||||
|
|
||||||
|
### Backend (Node.js + Express)
|
||||||
|
|
||||||
|
#### Database & Modelli
|
||||||
|
- [x] Modello User (PostgreSQL) - id, name, email, avatarUrl, score
|
||||||
|
- [x] Modello Group (PostgreSQL) - id, name
|
||||||
|
- [x] Modello Friendship (PostgreSQL) - gestione amicizie bidirezionali
|
||||||
|
- [x] Modello FriendRequest (PostgreSQL) - gestione richieste di amicizia
|
||||||
|
- [x] Modello Bet (MongoDB) - scommesse con partecipanti, stato, posta in gioco
|
||||||
|
- [x] Associazioni many-to-many tra User e Group
|
||||||
|
- [x] Schema SQL di inizializzazione database (init.sql)
|
||||||
|
|
||||||
|
#### API Endpoints
|
||||||
|
- [x] `/api/initial-data` - Caricamento dati iniziali (utente corrente, amici, gruppi)
|
||||||
|
- [x] `/api/users` - Gestione utenti
|
||||||
|
- [x] `/api/groups` - CRUD gruppi
|
||||||
|
- [x] `/api/groups/:id/members` - Gestione membri dei gruppi
|
||||||
|
- [x] `/api/bets` - CRUD scommesse
|
||||||
|
- [x] `/api/bets/by-time` - Scommesse filtrate per periodo temporale
|
||||||
|
- [x] `/api/bets/by-group/:groupId` - Scommesse per gruppo
|
||||||
|
- [x] `/api/friends` - Lista amici
|
||||||
|
- [x] `/api/friends/requests` - Richieste di amicizia (GET, POST, PATCH)
|
||||||
|
|
||||||
|
#### Configurazione
|
||||||
|
- [x] Server Express con middleware CORS
|
||||||
|
- [x] Connessione doppio database (PostgreSQL + MongoDB)
|
||||||
|
- [x] Seed automatico al primo avvio
|
||||||
|
- [x] Gestione variabili ambiente (PORT, MONGO_URI, POSTGRES_URI)
|
||||||
|
|
||||||
|
### Frontend (Angular 20)
|
||||||
|
|
||||||
|
#### Servizi
|
||||||
|
- [x] `ApiService` - Comunicazione HTTP con backend
|
||||||
|
- [x] `DataService` - Gestione stato applicazione con RxJS signals
|
||||||
|
|
||||||
|
#### Componenti Principali
|
||||||
|
- [x] `AppComponent` - Layout principale con navigation bar
|
||||||
|
- [x] `HomeComponent` - Dashboard principale con visualizzazione scommesse
|
||||||
|
- [x] `BetsByTimeComponent` - Filtro scommesse per periodo (oggi, settimana, mese)
|
||||||
|
- [x] `BetsByGroupComponent` - Filtro scommesse per gruppo
|
||||||
|
- [x] `DiscoverComponent` - Scoperta scommesse pubbliche
|
||||||
|
- [x] `CreateBetComponent` - Creazione nuove scommesse
|
||||||
|
- [x] `GroupsComponent` - Gestione gruppi
|
||||||
|
- [x] `GroupDetailComponent` - Dettaglio singolo gruppo
|
||||||
|
- [x] `ProfileComponent` - Profilo utente
|
||||||
|
- [x] `EditProfileComponent` - Modifica dati profilo
|
||||||
|
- [x] `BetCardComponent` - Card riutilizzabile per visualizzare scommesse
|
||||||
|
|
||||||
|
#### UI/UX
|
||||||
|
- [x] Design responsive con Tailwind CSS
|
||||||
|
- [x] Bottom navigation con 5 sezioni (Home, Scopri, Crea, Gruppi, Profilo)
|
||||||
|
- [x] Sistema di routing client-side tra viste
|
||||||
|
- [x] Tema dark con palette slate/cyan
|
||||||
|
|
||||||
|
#### Modelli TypeScript
|
||||||
|
- [x] Modelli TypeScript per tutte le entità (Bet, User, Group, ecc.)
|
||||||
|
- [x] Enum per stati scommesse (ACTIVE, AWAITING_VALIDATION, COMPLETED)
|
||||||
|
- [x] Enum per ruoli partecipanti (PARTICIPANT, OBSERVER)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Da Fare
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
#### Autenticazione & Sicurezza
|
||||||
|
- [ ] Sistema di autenticazione (JWT/OAuth)
|
||||||
|
- [ ] Middleware di protezione delle route
|
||||||
|
- [ ] Validazione input utente
|
||||||
|
- [ ] Rate limiting
|
||||||
|
- [ ] Gestione sessioni utente
|
||||||
|
|
||||||
|
#### Funzionalità Scommesse
|
||||||
|
- [ ] Logica di validazione vincitori
|
||||||
|
- [ ] Sistema di votazione per vincitori (per scommesse tra amici)
|
||||||
|
- [ ] Notifiche per eventi scommessa (inizio, fine, vincitore)
|
||||||
|
- [ ] Cronologia scommesse completate
|
||||||
|
- [ ] Statistiche utente (win rate, streak, ecc.)
|
||||||
|
|
||||||
|
#### Funzionalità Sociali
|
||||||
|
- [ ] Chat/commenti nelle scommesse
|
||||||
|
- [ ] Notifiche in tempo reale
|
||||||
|
- [ ] Sistema di reputazione/badge
|
||||||
|
- [ ] Feed attività amici
|
||||||
|
|
||||||
|
#### Ottimizzazioni
|
||||||
|
- [ ] Caching con Redis
|
||||||
|
- [ ] Paginazione risultati
|
||||||
|
- [ ] Query optimization
|
||||||
|
- [ ] File upload per avatar personalizzati
|
||||||
|
- [ ] Compressione risposte HTTP
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
#### Funzionalità Mancanti
|
||||||
|
- [ ] Visualizzazione dettaglio scommessa
|
||||||
|
- [ ] Modifica/eliminazione scommessa
|
||||||
|
- [ ] Gestione richieste di amicizia (UI)
|
||||||
|
- [ ] Ricerca utenti
|
||||||
|
- [ ] Notifiche push
|
||||||
|
- [ ] Stati di caricamento e gestione errori
|
||||||
|
- [ ] Conferme azioni critiche (elimina, ecc.)
|
||||||
|
|
||||||
|
#### UX Improvements
|
||||||
|
- [ ] Animazioni transizioni tra viste
|
||||||
|
- [ ] Skeleton loaders
|
||||||
|
- [ ] Pull to refresh
|
||||||
|
- [ ] Infinite scroll per liste lunghe
|
||||||
|
- [ ] Filtri avanzati scommesse
|
||||||
|
- [ ] Ordinamento personalizzato
|
||||||
|
|
||||||
|
#### Testing
|
||||||
|
- [ ] Unit test per componenti
|
||||||
|
- [ ] Integration test per servizi
|
||||||
|
- [ ] E2E test per flussi principali
|
||||||
|
|
||||||
|
### DevOps & Deployment
|
||||||
|
- [ ] CI/CD pipeline
|
||||||
|
- [ ] Health check endpoints
|
||||||
|
- [ ] Logging strutturato
|
||||||
|
- [ ] Monitoring e alerting
|
||||||
|
- [ ] Backup automatici database
|
||||||
|
- [ ] Documentazione API (Swagger/OpenAPI)
|
||||||
|
- [ ] Docker production-ready configuration
|
||||||
|
- [ ] Environment separati (dev, staging, prod)
|
||||||
|
|
||||||
|
### Flutter App
|
||||||
|
- [ ] Inizializzare progetto Flutter
|
||||||
|
- [ ] Implementare le stesse funzionalità del frontend Angular
|
||||||
|
- [ ] Setup build per iOS/Android
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 Possibili Sviluppi Futuri
|
||||||
|
- [ ] Integrazione con provider di pagamento per scommesse con denaro reale
|
||||||
|
- [ ] Gamification avanzata (livelli, achievement)
|
||||||
|
- [ ] Scommesse su eventi pubblici (sport, politica, ecc.)
|
||||||
|
- [ ] Marketplace di scommesse
|
||||||
|
- [ ] Analytics dashboard per amministratori
|
||||||
|
- [ ] API pubblica per integrazioni esterne
|
||||||
|
- [ ] App mobile nativa (Flutter)
|
||||||
|
- [ ] Versione web pubblica (landing page)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note Tecniche
|
||||||
|
|
||||||
|
### Stack Tecnologico
|
||||||
|
- **Backend**: Node.js, Express, Sequelize (PostgreSQL), Mongoose (MongoDB)
|
||||||
|
- **Frontend**: Angular 20, RxJS, Tailwind CSS, Vite
|
||||||
|
- **Database**: PostgreSQL (dati relazionali), MongoDB (scommesse)
|
||||||
|
- **DevOps**: Docker, Docker Compose
|
||||||
|
|
||||||
|
### Porte
|
||||||
|
- Backend API: `3000`
|
||||||
|
- Frontend Dev: `4200`
|
||||||
|
- PostgreSQL: `5432`
|
||||||
|
- MongoDB: `27017`
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- PostgreSQL contiene: Users, Groups, Friendships, FriendRequests
|
||||||
|
- MongoDB contiene: Bets (per maggiore flessibilità dello schema)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Node
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.travis.yml
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose*.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# ========================================
|
||||||
|
# ENVIRONMENT CONFIGURATION TEMPLATE
|
||||||
|
# ========================================
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
# DO NOT commit .env to version control
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Application Configuration
|
||||||
|
# ========================================
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# PostgreSQL Database
|
||||||
|
# ========================================
|
||||||
|
POSTGRES_HOST=localhost
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_DB=amici_scommesse
|
||||||
|
POSTGRES_USER=admin
|
||||||
|
POSTGRES_PASSWORD=admin
|
||||||
|
POSTGRES_URI=postgres://admin:admin@localhost:5432/amici_scommesse
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# MongoDB Database
|
||||||
|
# ========================================
|
||||||
|
MONGO_HOST=localhost
|
||||||
|
MONGO_PORT=27017
|
||||||
|
MONGO_DB=amici-scommesse
|
||||||
|
MONGO_USER=root
|
||||||
|
MONGO_PASSWORD=root@servovio
|
||||||
|
MONGO_AUTH_SOURCE=admin
|
||||||
|
MONGO_URI=mongodb://root:root@servovio@localhost:27017/amici-scommesse?authSource=admin
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Logging
|
||||||
|
# ========================================
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
ENABLE_SQL_LOGGING=false
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# CORS Configuration
|
||||||
|
# ========================================
|
||||||
|
CORS_ORIGIN=http://localhost:5173
|
||||||
|
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3001
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Mock Authentication (Development Only)
|
||||||
|
# ========================================
|
||||||
|
MOCK_USER_NAME=Mario Rossi
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# API Configuration
|
||||||
|
# ========================================
|
||||||
|
API_VERSION=v1
|
||||||
|
API_PREFIX=/api
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Database Sync (Development Only)
|
||||||
|
# ========================================
|
||||||
|
# WARNING: force: true drops all tables and recreates them
|
||||||
|
# Set to false in production and use migrations instead
|
||||||
|
DB_SYNC_FORCE=true
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Future: JWT Authentication (Not Yet Implemented)
|
||||||
|
# ========================================
|
||||||
|
# JWT_SECRET=your-secret-key-change-this
|
||||||
|
# JWT_EXPIRES_IN=7d
|
||||||
|
# JWT_REFRESH_EXPIRES_IN=30d
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Future: Rate Limiting (Not Yet Implemented)
|
||||||
|
# ========================================
|
||||||
|
# RATE_LIMIT_WINDOW_MS=900000
|
||||||
|
# RATE_LIMIT_MAX_REQUESTS=100
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Future: Redis (Not Yet Implemented)
|
||||||
|
# ========================================
|
||||||
|
# REDIS_HOST=localhost
|
||||||
|
# REDIS_PORT=6379
|
||||||
|
# REDIS_PASSWORD=
|
||||||
|
# REDIS_DB=0
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Future: File Upload (Not Yet Implemented)
|
||||||
|
# ========================================
|
||||||
|
# UPLOAD_MAX_FILE_SIZE=5242880
|
||||||
|
# UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif
|
||||||
|
# AWS_S3_BUCKET=
|
||||||
|
# AWS_S3_REGION=
|
||||||
|
# AWS_ACCESS_KEY_ID=
|
||||||
|
# AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Future: Email Service (Not Yet Implemented)
|
||||||
|
# ========================================
|
||||||
|
# SMTP_HOST=smtp.gmail.com
|
||||||
|
# SMTP_PORT=587
|
||||||
|
# SMTP_USER=
|
||||||
|
# SMTP_PASSWORD=
|
||||||
|
# EMAIL_FROM=noreply@amiciscommesse.com
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Future: Monitoring & Analytics (Not Yet Implemented)
|
||||||
|
# ========================================
|
||||||
|
# SENTRY_DSN=
|
||||||
|
# ANALYTICS_ENABLED=false
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
coverage/
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": ["airbnb-base", "prettier"],
|
||||||
|
"plugins": ["prettier"],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"no-console": "off",
|
||||||
|
"consistent-return": "warn",
|
||||||
|
"no-underscore-dangle": ["error", { "allow": ["_id"] }],
|
||||||
|
"import/no-extraneous-dependencies": [
|
||||||
|
"error",
|
||||||
|
{ "devDependencies": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
# API Documentation - Amici Scommesse
|
||||||
|
|
||||||
|
## 📚 Overview
|
||||||
|
|
||||||
|
Documentazione completa dell'API Amici Scommesse v1.1.0.
|
||||||
|
|
||||||
|
**Base URL**: `http://localhost:3000/api/v1`
|
||||||
|
|
||||||
|
## 🔗 Specifica OpenAPI
|
||||||
|
|
||||||
|
La specifica OpenAPI completa è disponibile nel file `openapi.yaml`.
|
||||||
|
|
||||||
|
### Visualizzare la Specifica
|
||||||
|
|
||||||
|
**Opzione 1: Swagger UI Online**
|
||||||
|
1. Vai su https://editor.swagger.io/
|
||||||
|
2. File → Import File → Seleziona `openapi.yaml`
|
||||||
|
3. Visualizza la documentazione interattiva
|
||||||
|
|
||||||
|
**Opzione 2: Swagger UI Locale**
|
||||||
|
```bash
|
||||||
|
npx swagger-ui-watcher openapi.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Opzione 3: Redoc**
|
||||||
|
```bash
|
||||||
|
npx @redocly/cli preview-docs openapi.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generare Client SDK
|
||||||
|
|
||||||
|
**TypeScript/JavaScript:**
|
||||||
|
```bash
|
||||||
|
npx @openapitools/openapi-generator-cli generate \
|
||||||
|
-i openapi.yaml \
|
||||||
|
-g typescript-axios \
|
||||||
|
-o ./frontend/src/api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
```bash
|
||||||
|
openapi-generator-cli generate \
|
||||||
|
-i openapi.yaml \
|
||||||
|
-g python \
|
||||||
|
-o ./python-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Endpoints API v1
|
||||||
|
|
||||||
|
### 🎯 Groups
|
||||||
|
|
||||||
|
| Metodo | Endpoint | Descrizione |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/groups` | Lista tutti i gruppi |
|
||||||
|
| GET | `/groups/:id` | Dettaglio gruppo |
|
||||||
|
| POST | `/groups` | Crea gruppo |
|
||||||
|
| PUT | `/groups/:id` | Aggiorna gruppo |
|
||||||
|
| DELETE | `/groups/:id` | Elimina gruppo |
|
||||||
|
| PUT | `/groups/:id/members` | Aggiungi membri |
|
||||||
|
| DELETE | `/groups/:id/members/:userId` | Rimuovi membro |
|
||||||
|
|
||||||
|
### 🎲 Bets
|
||||||
|
|
||||||
|
| Metodo | Endpoint | Descrizione |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/bets` | Lista tutte le scommesse |
|
||||||
|
| GET | `/bets/:id` | Dettaglio scommessa |
|
||||||
|
| POST | `/bets` | Crea scommessa |
|
||||||
|
| PUT | `/bets/:id` | Aggiorna scommessa |
|
||||||
|
| DELETE | `/bets/:id` | Elimina scommessa |
|
||||||
|
|
||||||
|
### 👥 Friend Requests
|
||||||
|
|
||||||
|
| Metodo | Endpoint | Descrizione |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/friend-requests` | Lista amici |
|
||||||
|
| GET | `/friend-requests/pending` | Richieste pending |
|
||||||
|
| POST | `/friend-requests` | Invia richiesta |
|
||||||
|
| POST | `/friend-requests/:id/accept` | Accetta richiesta |
|
||||||
|
| DELETE | `/friend-requests/:id` | Rifiuta richiesta |
|
||||||
|
|
||||||
|
### 👤 Users
|
||||||
|
|
||||||
|
| Metodo | Endpoint | Descrizione |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| PUT | `/users/current` | Aggiorna profilo |
|
||||||
|
|
||||||
|
### 📦 Initial Data
|
||||||
|
|
||||||
|
| Metodo | Endpoint | Descrizione |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/initial-data` | Carica dati iniziali |
|
||||||
|
|
||||||
|
### ❤️ Health
|
||||||
|
|
||||||
|
| Metodo | Endpoint | Descrizione |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/health` | Health check database |
|
||||||
|
|
||||||
|
## 📝 Formato Risposte
|
||||||
|
|
||||||
|
### Successo (2xx)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Operation successful",
|
||||||
|
"data": { ... },
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errore (4xx, 5xx)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error message",
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"field": "name",
|
||||||
|
"message": "Name is required"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Autenticazione
|
||||||
|
|
||||||
|
**Attuale**: Mock authentication (Mario Rossi hardcoded)
|
||||||
|
**Prossimo**: JWT authentication da implementare
|
||||||
|
|
||||||
|
## 🧪 Esempi di Chiamate
|
||||||
|
|
||||||
|
### Creare un Gruppo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/groups \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Gruppo Calcio",
|
||||||
|
"members": [1, 2, 3]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Risposta:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Group created successfully",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Gruppo Calcio",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Mario Rossi",
|
||||||
|
"email": "mario.rossi@example.com",
|
||||||
|
"avatarUrl": null,
|
||||||
|
"score": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creare una Scommessa
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/bets \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"title": "Chi vince il campionato?",
|
||||||
|
"description": "Scommessa sul vincitore del campionato 2024",
|
||||||
|
"stake": "50 euro",
|
||||||
|
"participants": [
|
||||||
|
{
|
||||||
|
"user": 1,
|
||||||
|
"role": "PARTICIPANT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user": 2,
|
||||||
|
"role": "PARTICIPANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isPublic": false,
|
||||||
|
"groupId": "1"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inviare Richiesta di Amicizia
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/friend-requests \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"identifier": 2
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
oppure con email:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/friend-requests \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"identifier": "luigi.verdi@example.com"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aggiornare Profilo Utente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://localhost:3000/api/v1/users/current \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Mario Rossi",
|
||||||
|
"email": "mario.rossi.new@example.com",
|
||||||
|
"avatarUrl": "https://example.com/avatar.jpg"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caricare Dati Iniziali
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/v1/initial-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Risposta:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||||
|
"services": {
|
||||||
|
"postgres": "connected",
|
||||||
|
"mongodb": "connected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Migrazione da API Legacy
|
||||||
|
|
||||||
|
### API Legacy (Backward Compatibility)
|
||||||
|
|
||||||
|
Le vecchie route sono ancora disponibili su `/api/legacy/*` per compatibilità:
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/legacy/groups → legacy format
|
||||||
|
/api/legacy/bets → legacy format
|
||||||
|
/api/legacy/friend-requests → legacy format
|
||||||
|
```
|
||||||
|
|
||||||
|
### Differenze tra v1 e Legacy
|
||||||
|
|
||||||
|
| Aspetto | Legacy | v1 |
|
||||||
|
|---------|--------|-----|
|
||||||
|
| Formato risposta | Varia | Standardizzato |
|
||||||
|
| Validazione | Inline | Joi schema |
|
||||||
|
| Error handling | Inconsistente | Centralizzato |
|
||||||
|
| Success status | Varia | Sempre con wrapper |
|
||||||
|
| Timestamp | No | Sì |
|
||||||
|
|
||||||
|
### Piano di Migrazione Frontend
|
||||||
|
|
||||||
|
1. **Fase 1**: Testare endpoint v1 in sviluppo
|
||||||
|
2. **Fase 2**: Migrare chiamate API da `/api/*` a `/api/v1/*`
|
||||||
|
3. **Fase 3**: Aggiornare gestione risposte (nuovo formato)
|
||||||
|
4. **Fase 4**: Gestire nuovi errori standardizzati
|
||||||
|
5. **Fase 5**: Deprecare chiamate legacy
|
||||||
|
|
||||||
|
## 📊 Codici di Stato HTTP
|
||||||
|
|
||||||
|
| Codice | Significato | Quando |
|
||||||
|
|--------|-------------|--------|
|
||||||
|
| 200 | OK | Operazione riuscita |
|
||||||
|
| 201 | Created | Risorsa creata |
|
||||||
|
| 400 | Bad Request | Validation error |
|
||||||
|
| 404 | Not Found | Risorsa non trovata |
|
||||||
|
| 409 | Conflict | Conflitto (es: già amici) |
|
||||||
|
| 500 | Internal Error | Errore server |
|
||||||
|
| 503 | Service Unavailable | Database non disponibile |
|
||||||
|
|
||||||
|
## 🛠️ Testing con Postman
|
||||||
|
|
||||||
|
### Importare Specifica OpenAPI
|
||||||
|
|
||||||
|
1. Apri Postman
|
||||||
|
2. Import → Link → Incolla URL o File
|
||||||
|
3. Seleziona `openapi.yaml`
|
||||||
|
4. Postman genera automaticamente la collection
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Crea un environment Postman con:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:3000/api/v1",
|
||||||
|
"legacyUrl": "http://localhost:3000/api/legacy"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Integrare con Frontend
|
||||||
|
|
||||||
|
### Axios Example (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:3000/api/v1',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response Interceptor per formato standardizzato
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response.data, // Ritorna solo { success, message, data, timestamp }
|
||||||
|
(error) => {
|
||||||
|
if (error.response) {
|
||||||
|
// Server ha risposto con errore
|
||||||
|
return Promise.reject(error.response.data);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Esempio uso
|
||||||
|
async function createGroup(name: string, members: number[]) {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/groups', { name, members });
|
||||||
|
console.log('Success:', response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.message, error.errors);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch Example (JavaScript)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function createGroup(name, members) {
|
||||||
|
const response = await fetch('http://localhost:3000/api/v1/groups', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, members }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Hook Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
function useGroups() {
|
||||||
|
const [groups, setGroups] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchGroups() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.get('/groups');
|
||||||
|
setGroups(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { groups, loading, error };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Errore 404 su endpoint v1
|
||||||
|
|
||||||
|
Verifica che il server sia avviato e che stai usando `/api/v1/` e non `/api/`.
|
||||||
|
|
||||||
|
### Validation errors
|
||||||
|
|
||||||
|
Controlla la specifica OpenAPI per i campi required e i formati accettati.
|
||||||
|
|
||||||
|
### CORS errors
|
||||||
|
|
||||||
|
Il server ha CORS abilitato. Se riscontri problemi, verifica la configurazione del frontend.
|
||||||
|
|
||||||
|
### Health check fallisce
|
||||||
|
|
||||||
|
Verifica che PostgreSQL e MongoDB siano in esecuzione e accessibili.
|
||||||
|
|
||||||
|
## 📞 Supporto
|
||||||
|
|
||||||
|
Per problemi o domande:
|
||||||
|
- Consulta la specifica OpenAPI completa in `openapi.yaml`
|
||||||
|
- Vedi esempi in `IMPLEMENTATION_GUIDE.md`
|
||||||
|
- Controlla il CHANGELOG.md per ultime modifiche
|
||||||
|
|
||||||
|
## 🔗 Link Utili
|
||||||
|
|
||||||
|
- [Swagger Editor](https://editor.swagger.io/)
|
||||||
|
- [OpenAPI Generator](https://openapi-generator.tech/)
|
||||||
|
- [Postman](https://www.postman.com/)
|
||||||
|
- [Redoc](https://redocly.com/redoc/)
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
# Changelog - Backend Node.js
|
||||||
|
|
||||||
|
Tracciamento modifiche specifiche del backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Completato
|
||||||
|
|
||||||
|
### Setup Iniziale
|
||||||
|
- [x] Inizializzazione progetto Node.js con npm
|
||||||
|
- [x] Installazione dipendenze: Express, Sequelize, Mongoose, CORS, pg
|
||||||
|
- [x] Configurazione server Express (server.js)
|
||||||
|
- [x] Setup doppia connessione database (PostgreSQL + MongoDB)
|
||||||
|
|
||||||
|
### Database PostgreSQL
|
||||||
|
|
||||||
|
#### Modelli Sequelize
|
||||||
|
- [x] **User Model** (`models/user.model.js`)
|
||||||
|
- Campi: id, name, email, avatarUrl, score
|
||||||
|
- Validazioni email unique
|
||||||
|
- Relazioni: belongsToMany Groups, hasMany FriendRequests
|
||||||
|
|
||||||
|
- [x] **Group Model** (`models/group.model.js`)
|
||||||
|
- Campi: id, name
|
||||||
|
- Relazioni: belongsToMany Users (many-to-many)
|
||||||
|
|
||||||
|
- [x] **Friendship Model** (`models/friendship.model.js`)
|
||||||
|
- Campi: id, user1Id, user2Id
|
||||||
|
- Vincolo: user1Id < user2Id (evita duplicati)
|
||||||
|
- Hook beforeValidate per garantire ordine corretto
|
||||||
|
|
||||||
|
- [x] **FriendRequest Model** (`models/friendRequest.model.js`)
|
||||||
|
- Campi: id, fromUserId, toUserId, status, createdAt, updatedAt
|
||||||
|
- Stati: PENDING, ACCEPTED, REJECTED
|
||||||
|
- Timestamps automatici
|
||||||
|
|
||||||
|
- [x] **Associations** (`models/associations.js`)
|
||||||
|
- User ↔ Group (many-to-many via UserGroups)
|
||||||
|
- User → FriendRequest (fromUser, toUser)
|
||||||
|
- Friendship → User (user1, user2)
|
||||||
|
|
||||||
|
#### Schema SQL
|
||||||
|
- [x] Script di inizializzazione (`init.sql`)
|
||||||
|
- [x] Definizione tabelle con constraint e foreign keys
|
||||||
|
- [x] Indici per performance su ricerche frequenti
|
||||||
|
- [x] Check constraints per data integrity
|
||||||
|
|
||||||
|
### Database MongoDB
|
||||||
|
|
||||||
|
- [x] **Bet Model** (`models/bet.model.js`)
|
||||||
|
- Schema: title, description, stake, participants[], status, startDate, endDate, isPublic, groupId, winner
|
||||||
|
- Partecipanti con ruolo (PARTICIPANT/OBSERVER)
|
||||||
|
- Stati: ACTIVE, AWAITING_VALIDATION, COMPLETED
|
||||||
|
- Timestamp automatici (createdAt, updatedAt)
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
|
||||||
|
#### Initial Data Route (`routes/initial-data.routes.js`)
|
||||||
|
- [x] `GET /api/initial-data` - Carica dati iniziali per l'app
|
||||||
|
- Utente corrente (hardcoded userId=1 per ora)
|
||||||
|
- Lista amici con info complete
|
||||||
|
- Gruppi dell'utente con membri
|
||||||
|
- Scommesse dell'utente (tutte, attive, passate)
|
||||||
|
|
||||||
|
#### Users Routes (`routes/users.routes.js`)
|
||||||
|
- [x] `GET /api/users` - Lista tutti gli utenti
|
||||||
|
- [x] `GET /api/users/:id` - Dettaglio utente singolo
|
||||||
|
|
||||||
|
#### Groups Routes (`routes/groups.routes.js`)
|
||||||
|
- [x] `GET /api/groups` - Lista tutti i gruppi
|
||||||
|
- [x] `GET /api/groups/:id` - Dettaglio gruppo con membri
|
||||||
|
- [x] `POST /api/groups` - Crea nuovo gruppo
|
||||||
|
- [x] `PUT /api/groups/:id` - Aggiorna gruppo
|
||||||
|
- [x] `DELETE /api/groups/:id` - Elimina gruppo
|
||||||
|
- [x] `POST /api/groups/:id/members` - Aggiungi membro al gruppo
|
||||||
|
- [x] `DELETE /api/groups/:id/members/:userId` - Rimuovi membro
|
||||||
|
|
||||||
|
#### Bets Routes (`routes/bets.routes.js`)
|
||||||
|
- [x] `GET /api/bets` - Lista tutte le scommesse
|
||||||
|
- [x] `GET /api/bets/:id` - Dettaglio scommessa
|
||||||
|
- [x] `POST /api/bets` - Crea nuova scommessa
|
||||||
|
- [x] `PUT /api/bets/:id` - Aggiorna scommessa
|
||||||
|
- [x] `DELETE /api/bets/:id` - Elimina scommessa
|
||||||
|
- [x] `GET /api/bets/by-time` - Filtra scommesse per periodo (today, week, month)
|
||||||
|
- [x] `GET /api/bets/by-group/:groupId` - Scommesse per gruppo specifico
|
||||||
|
|
||||||
|
#### Friends Routes (`routes/friends.routes.js`)
|
||||||
|
- [x] `GET /api/friends/:userId` - Lista amici di un utente
|
||||||
|
- [x] `GET /api/friends/requests/:userId` - Richieste di amicizia pending
|
||||||
|
- [x] `POST /api/friends/requests` - Invia richiesta di amicizia
|
||||||
|
- [x] `PATCH /api/friends/requests/:requestId` - Accetta/rifiuta richiesta
|
||||||
|
|
||||||
|
#### API Router (`routes/api.js`)
|
||||||
|
- [x] Router factory che accetta istanza Sequelize
|
||||||
|
- [x] Composizione di tutti i sub-router
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
- [x] **Seed Script** (`seed.js`)
|
||||||
|
- Popola PostgreSQL con 6 utenti di test
|
||||||
|
- Crea 3 amicizie e 2 richieste pending
|
||||||
|
- Crea 2 gruppi con membri
|
||||||
|
- Popola MongoDB con 5 scommesse di esempio
|
||||||
|
- Skip automatico se database già popolato
|
||||||
|
|
||||||
|
- [x] **Populate Bets** (`utils/populateBets.js`)
|
||||||
|
- Utility per popolare scommesse (se necessario)
|
||||||
|
|
||||||
|
### Configurazione
|
||||||
|
- [x] Sync automatico con `{ force: true }` in development
|
||||||
|
- [x] Seed automatico al primo avvio
|
||||||
|
- [x] CORS abilitato per frontend
|
||||||
|
- [x] JSON body parser
|
||||||
|
- [x] Gestione errori connessione database
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Da Fare
|
||||||
|
|
||||||
|
### Autenticazione & Autorizzazione
|
||||||
|
- [ ] Implementare JWT authentication
|
||||||
|
- [ ] Middleware di protezione route
|
||||||
|
- [ ] Login/Register endpoints
|
||||||
|
- [ ] Password hashing (bcrypt)
|
||||||
|
- [ ] Refresh token mechanism
|
||||||
|
- [ ] OAuth2 integration (Google, Facebook)
|
||||||
|
- [ ] Gestione sessioni
|
||||||
|
|
||||||
|
### Validazione & Sicurezza
|
||||||
|
- [ ] Validation middleware con Joi/express-validator
|
||||||
|
- [ ] Sanitizzazione input utente
|
||||||
|
- [ ] Rate limiting per API
|
||||||
|
- [ ] CORS configuration più restrittiva per production
|
||||||
|
- [ ] Helmet.js per security headers
|
||||||
|
- [ ] SQL injection prevention (già parzialmente coperto da Sequelize)
|
||||||
|
- [ ] XSS protection
|
||||||
|
|
||||||
|
### Funzionalità Scommesse
|
||||||
|
- [ ] Endpoint per dichiarare vincitore
|
||||||
|
- [ ] Sistema di votazione vincitori (per scommesse tra amici)
|
||||||
|
- [ ] Logica di calcolo punteggi utenti
|
||||||
|
- [ ] Notifiche per eventi scommessa
|
||||||
|
- [ ] Endpoint statistiche utente
|
||||||
|
- Win rate
|
||||||
|
- Streak attuali
|
||||||
|
- Scommesse vinte/perse
|
||||||
|
- Classifica generale
|
||||||
|
- [ ] Filtri avanzati per ricerca scommesse
|
||||||
|
- [ ] Paginazione scommesse
|
||||||
|
- [ ] Cronologia completa scommesse
|
||||||
|
|
||||||
|
### Funzionalità Sociali
|
||||||
|
- [ ] Endpoint per chat/commenti nelle scommesse
|
||||||
|
- [ ] WebSocket/SSE per notifiche real-time
|
||||||
|
- [ ] Sistema di notifiche persistenti
|
||||||
|
- [ ] Feed attività (chi ha creato cosa, chi ha vinto, ecc.)
|
||||||
|
- [ ] Richieste di amicizia: suggerimenti basati su amici in comune
|
||||||
|
- [ ] Blocco utenti
|
||||||
|
- [ ] Report/segnalazioni
|
||||||
|
|
||||||
|
### Funzionalità Gruppi
|
||||||
|
- [ ] Ruoli nei gruppi (admin, membro)
|
||||||
|
- [ ] Inviti al gruppo
|
||||||
|
- [ ] Statistiche gruppo
|
||||||
|
- [ ] Impostazioni privacy gruppo
|
||||||
|
|
||||||
|
### File & Media
|
||||||
|
- [ ] Upload avatar utente
|
||||||
|
- [ ] Upload immagini nelle scommesse
|
||||||
|
- [ ] Integrazione storage (S3, Cloudinary)
|
||||||
|
- [ ] Resize/compression immagini
|
||||||
|
|
||||||
|
### Performance & Optimization
|
||||||
|
- [ ] Caching con Redis
|
||||||
|
- Cache lista amici
|
||||||
|
- Cache gruppi utente
|
||||||
|
- Cache scommesse popolari
|
||||||
|
- [ ] Query optimization
|
||||||
|
- [ ] Indici database aggiuntivi dove necessario
|
||||||
|
- [ ] Paginazione su tutti gli endpoint liste
|
||||||
|
- [ ] Response compression (gzip)
|
||||||
|
- [ ] Database connection pooling optimization
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Unit test per modelli
|
||||||
|
- [ ] Integration test per API endpoints
|
||||||
|
- [ ] Test copertura almeno 80%
|
||||||
|
- [ ] Mock database per test
|
||||||
|
- [ ] Setup test runner (Jest/Mocha)
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- [ ] Logging strutturato (Winston/Pino)
|
||||||
|
- [ ] Health check endpoint (`/health`)
|
||||||
|
- [ ] Metrics endpoint (`/metrics`)
|
||||||
|
- [ ] Error tracking (Sentry)
|
||||||
|
- [ ] Environment variables validation
|
||||||
|
- [ ] Graceful shutdown
|
||||||
|
- [ ] Dockerfile per production
|
||||||
|
- [ ] Docker multi-stage build
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- [ ] Migration system (invece di sync force)
|
||||||
|
- [ ] Backup automatici
|
||||||
|
- [ ] Soft delete per entità critiche
|
||||||
|
- [ ] Audit log per modifiche importanti
|
||||||
|
- [ ] Database indexing strategy review
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
- [ ] Swagger/OpenAPI specification
|
||||||
|
- [ ] Endpoint documentation con esempi
|
||||||
|
- [ ] Postman collection
|
||||||
|
- [ ] API versioning strategy
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- [ ] Application Performance Monitoring (APM)
|
||||||
|
- [ ] Database query monitoring
|
||||||
|
- [ ] Error rate alerts
|
||||||
|
- [ ] Response time monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Bug Conosciuti
|
||||||
|
- Nessuno al momento
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Miglioramenti Tecnici Completati (v1.1.0 - 2024)
|
||||||
|
|
||||||
|
### Refactoring Architetturale
|
||||||
|
- [x] **Separazione logica business dai controller (service layer)**
|
||||||
|
- Creati 5 services: users, groups, friends, bets, data-stitching
|
||||||
|
- Business logic completamente spostata dai controller ai services
|
||||||
|
- `services/` directory con pattern consistente
|
||||||
|
|
||||||
|
- [x] **Dependency Injection Container**
|
||||||
|
- Implementato Awilix per DI
|
||||||
|
- Container setup in `di/container.js`
|
||||||
|
- Registrazione automatica di repositories, services, controllers
|
||||||
|
- Lifetime management: singleton per database, scoped per services
|
||||||
|
|
||||||
|
- [x] **Request/Response DTOs**
|
||||||
|
- Validation schemas con Joi
|
||||||
|
- 4 schemas: group, bet, user, friendRequest
|
||||||
|
- Middleware validation in `middleware/validation.js`
|
||||||
|
- Input sanitization automatico
|
||||||
|
|
||||||
|
- [x] **Standardizzazione formato risposte API**
|
||||||
|
- ResponseHandler class in `shared/response.handler.js`
|
||||||
|
- Formato consistente per successo e errori
|
||||||
|
- Timestamp automatico in ogni risposta
|
||||||
|
- Metodi: success, created, error, badRequest, notFound, etc.
|
||||||
|
|
||||||
|
- [x] **Error handling centralizzato**
|
||||||
|
- Custom errors: ValidationError, NotFoundError, ConflictError, etc.
|
||||||
|
- Global error handler middleware
|
||||||
|
- Gestione errori Sequelize e Mongoose
|
||||||
|
- Stack trace solo in development
|
||||||
|
|
||||||
|
- [x] **Constants file per magic numbers/strings**
|
||||||
|
- `config/constants.js`: HTTP_STATUS, BET_STATUS, FRIEND_REQUEST_STATUS, etc.
|
||||||
|
- `config/database.js`: configurazione database centralizzata
|
||||||
|
- `config/index.js`: export centralizzato
|
||||||
|
|
||||||
|
- [x] **Code linting (ESLint) e formatting (Prettier)**
|
||||||
|
- ESLint configurato con airbnb-base
|
||||||
|
- Prettier per code formatting
|
||||||
|
- Scripts npm: lint, lint:fix, format
|
||||||
|
- .eslintrc.json e .prettierrc
|
||||||
|
|
||||||
|
- [ ] **Type checking con TypeScript** - SKIPPED (troppo invasivo per questa fase)
|
||||||
|
|
||||||
|
### Nuova Struttura Architetturale
|
||||||
|
|
||||||
|
```
|
||||||
|
Routes (thin) → Validation → Controllers → Services → Repositories → Database
|
||||||
|
↓ ↓ ↓
|
||||||
|
ResponseHandler Business Logic DB Access
|
||||||
|
```
|
||||||
|
|
||||||
|
**Directory create:**
|
||||||
|
- `config/` - Configurazione centralizzata (3 files)
|
||||||
|
- `shared/errors/` - Custom errors (2 files)
|
||||||
|
- `shared/validators/schemas/` - Joi schemas (4 files)
|
||||||
|
- `shared/response.handler.js` - Standardized responses
|
||||||
|
- `middleware/` - errorHandler, validation, auth.mock (3 files)
|
||||||
|
- `di/` - Dependency injection container (1 file)
|
||||||
|
- `repositories/postgres/` - PostgreSQL repositories (4 files)
|
||||||
|
- `repositories/mongo/` - MongoDB repositories (1 file)
|
||||||
|
- `services/` - Business logic services (5 files)
|
||||||
|
- `controllers/` - Thin controllers (5 files)
|
||||||
|
- `routes/v1/` - API v1 routes (6 files)
|
||||||
|
- `routes/health.routes.js` - Health check endpoint
|
||||||
|
|
||||||
|
**File modificati:**
|
||||||
|
- `server.js` - Refactored per integrare DI container e nuova architettura
|
||||||
|
- `package.json` - Aggiunte dipendenze e scripts
|
||||||
|
|
||||||
|
### API Versioning
|
||||||
|
|
||||||
|
- **`/api/v1/*`** - Nuova API con architettura refactored
|
||||||
|
- Formato risposte standardizzato
|
||||||
|
- Validazione automatica input
|
||||||
|
- Error handling consistente
|
||||||
|
|
||||||
|
- **`/api/legacy/*`** - API legacy (backward compatibility)
|
||||||
|
- Routes originali spostate qui
|
||||||
|
- Mantiene formato originale
|
||||||
|
- Per garantire compatibilità durante migrazione
|
||||||
|
|
||||||
|
- **`/health`** - Health check endpoint
|
||||||
|
- Verifica connessioni PostgreSQL e MongoDB
|
||||||
|
- Status: healthy/unhealthy
|
||||||
|
|
||||||
|
### Endpoints API v1
|
||||||
|
|
||||||
|
#### Groups (`/api/v1/groups`)
|
||||||
|
- `GET /` - Lista tutti i gruppi
|
||||||
|
- `GET /:id` - Dettaglio gruppo con membri
|
||||||
|
- `POST /` - Crea gruppo (validato con Joi)
|
||||||
|
- `PUT /:id` - Aggiorna gruppo (validato)
|
||||||
|
- `DELETE /:id` - Elimina gruppo
|
||||||
|
- `PUT /:id/members` - Aggiungi membri (validato)
|
||||||
|
- `DELETE /:id/members/:userId` - Rimuovi membro
|
||||||
|
|
||||||
|
#### Bets (`/api/v1/bets`)
|
||||||
|
- `GET /` - Lista tutte le scommesse
|
||||||
|
- `GET /:id` - Dettaglio scommessa
|
||||||
|
- `POST /` - Crea scommessa (validato con Joi)
|
||||||
|
- `PUT /:id` - Aggiorna scommessa (validato)
|
||||||
|
- `DELETE /:id` - Elimina scommessa
|
||||||
|
|
||||||
|
#### Friend Requests (`/api/v1/friend-requests`)
|
||||||
|
- `GET /` - Lista amici dell'utente corrente
|
||||||
|
- `GET /pending` - Richieste pending
|
||||||
|
- `POST /` - Invia richiesta (validato con Joi)
|
||||||
|
- `POST /:id/accept` - Accetta richiesta
|
||||||
|
- `DELETE /:id` - Rifiuta/cancella richiesta
|
||||||
|
|
||||||
|
#### Users (`/api/v1/users`)
|
||||||
|
- `PUT /current` - Aggiorna profilo utente corrente (validato)
|
||||||
|
|
||||||
|
#### Initial Data (`/api/v1/initial-data`)
|
||||||
|
- `GET /` - Carica dati iniziali (currentUser, users, friends, groups, bets)
|
||||||
|
|
||||||
|
#### Health Check (`/health`)
|
||||||
|
- `GET /` - Status database (PostgreSQL, MongoDB)
|
||||||
|
|
||||||
|
### Formato Risposte Standardizzato
|
||||||
|
|
||||||
|
**Successo:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Operation successful",
|
||||||
|
"data": { ... },
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errore:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error message",
|
||||||
|
"errors": [{"field": "name", "message": "Required"}],
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefici dell'Architettura
|
||||||
|
|
||||||
|
- ✅ **Separation of Concerns**: Ogni layer ha responsabilità ben definite
|
||||||
|
- ✅ **Testability**: Services e repositories facilmente testabili
|
||||||
|
- ✅ **Maintainability**: Business logic centralizzata
|
||||||
|
- ✅ **Consistency**: Formato risposte e errori standardizzati
|
||||||
|
- ✅ **Scalability**: Facile aggiungere nuove feature
|
||||||
|
- ✅ **Type Safety**: Validation layer previene dati inconsistenti
|
||||||
|
- ✅ **Backward Compatibility**: Routes legacy funzionano ancora
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note Tecniche
|
||||||
|
|
||||||
|
### Dipendenze Principali
|
||||||
|
- **express**: ^4.19.2
|
||||||
|
- **sequelize**: ^6.37.3 (PostgreSQL ORM)
|
||||||
|
- **mongoose**: ^8.4.0 (MongoDB ODM)
|
||||||
|
- **pg**: ^8.11.5 (PostgreSQL driver)
|
||||||
|
- **cors**: ^2.8.5
|
||||||
|
- **awilix**: ^10.0.2 (Dependency Injection)
|
||||||
|
- **joi**: ^17.12.1 (Validation)
|
||||||
|
|
||||||
|
### Dev Dependencies
|
||||||
|
- **eslint**: ^8.56.0
|
||||||
|
- **prettier**: ^3.2.4
|
||||||
|
- **eslint-config-airbnb-base**: ^15.0.0
|
||||||
|
- **eslint-config-prettier**: ^9.1.0
|
||||||
|
|
||||||
|
### Variabili Ambiente Richieste
|
||||||
|
```bash
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
MONGO_URI=mongodb://root:root@servovio@localhost:27017/amici-scommesse
|
||||||
|
POSTGRES_URI=postgres://admin:admin@localhost:5432/amici_scommesse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Porte
|
||||||
|
- Server: `3000`
|
||||||
|
- PostgreSQL: `5432`
|
||||||
|
- MongoDB: `27017`
|
||||||
|
|
||||||
|
### Scripts npm
|
||||||
|
```bash
|
||||||
|
npm start # Avvia server
|
||||||
|
npm run lint # Check linting
|
||||||
|
npm run lint:fix # Fix linting automaticamente
|
||||||
|
npm run format # Format codice con Prettier
|
||||||
|
```
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
# 🐳 Docker Guide - Amici Scommesse Backend
|
||||||
|
|
||||||
|
Guida completa per eseguire il backend con Docker e Docker Compose.
|
||||||
|
|
||||||
|
## 📋 Prerequisiti
|
||||||
|
|
||||||
|
- Docker >= 20.10
|
||||||
|
- Docker Compose >= 2.0
|
||||||
|
|
||||||
|
Verifica installazione:
|
||||||
|
```bash
|
||||||
|
docker --version
|
||||||
|
docker compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Development (con Hot Reload)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Avvia tutti i servizi in development
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Visualizza logs
|
||||||
|
docker compose -f docker-compose.dev.yml logs -f backend
|
||||||
|
|
||||||
|
# Ferma tutti i servizi
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Avvia tutti i servizi in production
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Visualizza logs
|
||||||
|
docker compose logs -f backend
|
||||||
|
|
||||||
|
# Ferma tutti i servizi
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Servizi Inclusi
|
||||||
|
|
||||||
|
### Docker Compose Production (`docker-compose.yml`)
|
||||||
|
|
||||||
|
| Servizio | Porta | Descrizione |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| `backend` | 3000 | Backend Node.js (production build) |
|
||||||
|
| `postgres` | 5432 | PostgreSQL 16 |
|
||||||
|
| `mongodb` | 27017 | MongoDB 7 |
|
||||||
|
|
||||||
|
### Docker Compose Development (`docker-compose.dev.yml`)
|
||||||
|
|
||||||
|
| Servizio | Porta | Descrizione |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| `backend` | 3000 | Backend Node.js (development + hot reload) |
|
||||||
|
| `postgres` | 5432 | PostgreSQL 16 |
|
||||||
|
| `mongodb` | 27017 | MongoDB 7 |
|
||||||
|
| `mongo-express` | 8081 | Web UI per MongoDB (opzionale) |
|
||||||
|
| `pgadmin` | 5050 | Web UI per PostgreSQL (opzionale) |
|
||||||
|
|
||||||
|
## 🔧 Configurazione
|
||||||
|
|
||||||
|
### File .env
|
||||||
|
|
||||||
|
Crea un file `.env` nella root del progetto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copia il template
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Modifica le variabili se necessario
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Le variabili d'ambiente sono automaticamente caricate da Docker Compose.
|
||||||
|
|
||||||
|
### Variabili Importanti
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Application
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_DB=amici_scommesse
|
||||||
|
POSTGRES_USER=admin
|
||||||
|
POSTGRES_PASSWORD=admin
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
MONGO_DB=amici-scommesse
|
||||||
|
MONGO_USER=root
|
||||||
|
MONGO_PASSWORD=root@servovio
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Comandi Docker Compose
|
||||||
|
|
||||||
|
### Gestione Servizi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Avvia tutti i servizi (production)
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Avvia tutti i servizi (development)
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Avvia solo alcuni servizi
|
||||||
|
docker compose up -d postgres mongodb
|
||||||
|
|
||||||
|
# Ferma tutti i servizi
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Ferma e rimuovi volumi (ATTENZIONE: cancella i dati!)
|
||||||
|
docker compose down -v
|
||||||
|
|
||||||
|
# Restart di un servizio specifico
|
||||||
|
docker compose restart backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build e Rebuild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build dell'immagine backend
|
||||||
|
docker compose build backend
|
||||||
|
|
||||||
|
# Rebuild forzato (ignora cache)
|
||||||
|
docker compose build --no-cache backend
|
||||||
|
|
||||||
|
# Build e avvia
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs e Debug
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Visualizza logs di tutti i servizi
|
||||||
|
docker compose logs
|
||||||
|
|
||||||
|
# Visualizza logs di un servizio specifico
|
||||||
|
docker compose logs backend
|
||||||
|
|
||||||
|
# Follow logs in real-time
|
||||||
|
docker compose logs -f backend
|
||||||
|
|
||||||
|
# Ultimi 100 log lines
|
||||||
|
docker compose logs --tail=100 backend
|
||||||
|
|
||||||
|
# Logs da timestamp specifico
|
||||||
|
docker compose logs --since 2024-01-01T00:00:00
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell nei Container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Shell nel container backend
|
||||||
|
docker compose exec backend sh
|
||||||
|
|
||||||
|
# Shell nel container postgres
|
||||||
|
docker compose exec postgres psql -U admin -d amici_scommesse
|
||||||
|
|
||||||
|
# Shell nel container mongodb
|
||||||
|
docker compose exec mongodb mongosh -u root -p root@servovio --authenticationDatabase admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verifica health di tutti i servizi
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Health check specifico
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
|
### Il backend non si avvia
|
||||||
|
|
||||||
|
**1. Verifica che i database siano healthy:**
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Dovresti vedere:
|
||||||
|
```
|
||||||
|
NAME STATUS HEALTH
|
||||||
|
amici-scommesse-backend Up 30 seconds healthy
|
||||||
|
amici-scommesse-postgres Up 45 seconds (healthy) healthy
|
||||||
|
amici-scommesse-mongodb Up 45 seconds (healthy) healthy
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Controlla i logs:**
|
||||||
|
```bash
|
||||||
|
docker compose logs backend
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Verifica la connessione ai database:**
|
||||||
|
```bash
|
||||||
|
# Test PostgreSQL
|
||||||
|
docker compose exec postgres pg_isready -U admin
|
||||||
|
|
||||||
|
# Test MongoDB
|
||||||
|
docker compose exec mongodb mongosh --eval "db.adminCommand('ping')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errore "port already in use"
|
||||||
|
|
||||||
|
**Soluzione**: Cambia la porta nel file `.env`:
|
||||||
|
```env
|
||||||
|
PORT=3001
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
MONGO_PORT=27018
|
||||||
|
```
|
||||||
|
|
||||||
|
### I database non mantengono i dati
|
||||||
|
|
||||||
|
**Soluzione**: Verifica che i volumi esistano:
|
||||||
|
```bash
|
||||||
|
docker volume ls | grep amici-scommesse
|
||||||
|
```
|
||||||
|
|
||||||
|
Per ripristinare i volumi:
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hot reload non funziona in development
|
||||||
|
|
||||||
|
**Verifica volumi**: Il file `docker-compose.dev.yml` deve avere:
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
- /app/node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rebuild del container**:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errore di connessione tra servizi
|
||||||
|
|
||||||
|
**Soluzione**: Usa i nomi dei servizi come hostname:
|
||||||
|
- Backend → Postgres: `postgres:5432`
|
||||||
|
- Backend → MongoDB: `mongodb:27017`
|
||||||
|
|
||||||
|
Non usare `localhost` quando sei dentro Docker!
|
||||||
|
|
||||||
|
## 🌐 Accesso ai Servizi
|
||||||
|
|
||||||
|
### Backend API
|
||||||
|
|
||||||
|
- **API v1**: http://localhost:3000/api/v1
|
||||||
|
- **Legacy API**: http://localhost:3000/api/legacy
|
||||||
|
- **Health Check**: http://localhost:3000/health
|
||||||
|
|
||||||
|
### Database Web UIs (Solo Development)
|
||||||
|
|
||||||
|
**Mongo Express** (MongoDB UI):
|
||||||
|
- URL: http://localhost:8081
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `admin`
|
||||||
|
|
||||||
|
**pgAdmin** (PostgreSQL UI):
|
||||||
|
- URL: http://localhost:5050
|
||||||
|
- Email: `admin@admin.com`
|
||||||
|
- Password: `admin`
|
||||||
|
|
||||||
|
**Connessione PostgreSQL in pgAdmin**:
|
||||||
|
1. Add New Server
|
||||||
|
2. General → Name: `Amici Scommesse`
|
||||||
|
3. Connection:
|
||||||
|
- Host: `postgres` (il nome del servizio Docker)
|
||||||
|
- Port: `5432`
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `admin`
|
||||||
|
- Database: `amici_scommesse`
|
||||||
|
|
||||||
|
## 📊 Gestione Dati
|
||||||
|
|
||||||
|
### Backup Database
|
||||||
|
|
||||||
|
**PostgreSQL**:
|
||||||
|
```bash
|
||||||
|
# Backup
|
||||||
|
docker compose exec postgres pg_dump -U admin amici_scommesse > backup.sql
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
docker compose exec -T postgres psql -U admin amici_scommesse < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**MongoDB**:
|
||||||
|
```bash
|
||||||
|
# Backup
|
||||||
|
docker compose exec mongodb mongodump --username root --password root@servovio --authenticationDatabase admin --db amici-scommesse --out /tmp/backup
|
||||||
|
|
||||||
|
# Export dal container
|
||||||
|
docker compose cp mongodb:/tmp/backup ./mongodb-backup
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
docker compose cp ./mongodb-backup mongodb:/tmp/backup
|
||||||
|
docker compose exec mongodb mongorestore --username root --password root@servovio --authenticationDatabase admin --db amici-scommesse /tmp/backup/amici-scommesse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Completo
|
||||||
|
|
||||||
|
**ATTENZIONE: Cancella tutti i dati!**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ferma tutto e rimuovi volumi
|
||||||
|
docker compose down -v
|
||||||
|
|
||||||
|
# Rimuovi immagini
|
||||||
|
docker compose rm -f
|
||||||
|
|
||||||
|
# Rebuild e riavvia
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Best Practices
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
1. **Usa docker-compose.dev.yml** per development
|
||||||
|
2. **Mount dei volumi** per hot reload
|
||||||
|
3. **Abilita SQL logging** per debug
|
||||||
|
4. **Usa le Web UI** per ispezionare i database
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
1. **Usa docker-compose.yml** per production
|
||||||
|
2. **Non montare volumi sorgente**
|
||||||
|
3. **Disabilita SQL logging**
|
||||||
|
4. **Usa secrets** per password sensibili
|
||||||
|
5. **Configura backup automatici**
|
||||||
|
|
||||||
|
### Sicurezza
|
||||||
|
|
||||||
|
1. **Non committare .env** nel repository
|
||||||
|
2. **Cambia password** di default per production
|
||||||
|
3. **Usa user non-root** (già configurato nel Dockerfile)
|
||||||
|
4. **Limita risorse** del container se necessario
|
||||||
|
|
||||||
|
## 🔨 Multi-Stage Build
|
||||||
|
|
||||||
|
Il Dockerfile usa multi-stage build per ottimizzare le immagini:
|
||||||
|
|
||||||
|
| Stage | Target | Uso |
|
||||||
|
|-------|--------|-----|
|
||||||
|
| `base` | - | Base comune |
|
||||||
|
| `dependencies` | - | Tutte le dipendenze |
|
||||||
|
| `development` | Development | Con devDependencies + hot reload |
|
||||||
|
| `production-dependencies` | - | Solo prod dependencies |
|
||||||
|
| `production` | Production | Ottimizzato, user non-root |
|
||||||
|
|
||||||
|
**Build target specifico**:
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
docker build --target development -t backend:dev .
|
||||||
|
|
||||||
|
# Production
|
||||||
|
docker build --target production -t backend:prod .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Monitoraggio
|
||||||
|
|
||||||
|
### Resource Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Uso risorse di tutti i container
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# Uso risorse di un container specifico
|
||||||
|
docker stats amici-scommesse-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Logs in Produzione
|
||||||
|
|
||||||
|
Per produzione, considera l'uso di:
|
||||||
|
- **Docker logging drivers** (json-file, syslog, fluentd)
|
||||||
|
- **Log aggregators** (ELK Stack, Loki)
|
||||||
|
- **APM tools** (Datadog, New Relic)
|
||||||
|
|
||||||
|
## 🚀 Deploy in Production
|
||||||
|
|
||||||
|
### Docker Swarm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inizializza Swarm
|
||||||
|
docker swarm init
|
||||||
|
|
||||||
|
# Deploy lo stack
|
||||||
|
docker stack deploy -c docker-compose.yml amici-scommesse
|
||||||
|
|
||||||
|
# Lista servizi
|
||||||
|
docker service ls
|
||||||
|
|
||||||
|
# Scala il backend
|
||||||
|
docker service scale amici-scommesse_backend=3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
Per Kubernetes, converti il docker-compose:
|
||||||
|
```bash
|
||||||
|
# Installa kompose
|
||||||
|
curl -L https://github.com/kubernetes/kompose/releases/download/v1.31.2/kompose-linux-amd64 -o kompose
|
||||||
|
chmod +x kompose
|
||||||
|
sudo mv kompose /usr/local/bin
|
||||||
|
|
||||||
|
# Converti
|
||||||
|
kompose convert -f docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔗 Link Utili
|
||||||
|
|
||||||
|
- [Docker Documentation](https://docs.docker.com/)
|
||||||
|
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
||||||
|
- [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/)
|
||||||
|
- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres)
|
||||||
|
- [MongoDB Docker Image](https://hub.docker.com/_/mongo)
|
||||||
|
|
||||||
|
## 💡 Tips
|
||||||
|
|
||||||
|
1. **Cache delle dipendenze**: `COPY package*.json` prima di `COPY .` per ottimizzare il build
|
||||||
|
2. **Multi-stage**: Usa stage diversi per dev e prod
|
||||||
|
3. **Health checks**: Sempre configurare healthcheck per monitoraggio
|
||||||
|
4. **Named volumes**: Usa volumi nominati per persistenza dati
|
||||||
|
5. **Networks**: Isola i servizi in network dedicati
|
||||||
|
6. **Secrets**: Usa Docker secrets per dati sensibili in produzione
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# ========================================
|
||||||
|
# Multi-stage Dockerfile for Backend Node.js
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Stage 1: Base
|
||||||
|
# ========================================
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Install dumb-init for proper signal handling
|
||||||
|
RUN apk add --no-cache dumb-init
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files for dependency installation
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Stage 2: Dependencies
|
||||||
|
# ========================================
|
||||||
|
FROM base AS dependencies
|
||||||
|
|
||||||
|
# Install ALL dependencies (including devDependencies for build)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Stage 3: Development
|
||||||
|
# ========================================
|
||||||
|
FROM dependencies AS development
|
||||||
|
|
||||||
|
# Set NODE_ENV
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Use dumb-init to handle signals properly
|
||||||
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
|
||||||
|
# Start with nodemon for hot reload
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Stage 4: Production Dependencies
|
||||||
|
# ========================================
|
||||||
|
FROM base AS production-dependencies
|
||||||
|
|
||||||
|
# Install only production dependencies
|
||||||
|
RUN npm ci --only=production && \
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Stage 5: Production Build
|
||||||
|
# ========================================
|
||||||
|
FROM base AS production
|
||||||
|
|
||||||
|
# Set NODE_ENV
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Copy production dependencies
|
||||||
|
COPY --from=production-dependencies /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nodejs -u 1001 && \
|
||||||
|
chown -R nodejs:nodejs /app
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER nodejs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||||
|
|
||||||
|
# Use dumb-init to handle signals properly
|
||||||
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Guida all'Implementazione - Miglioramenti Tecnici
|
||||||
|
|
||||||
|
## ✅ Implementazione Completata
|
||||||
|
|
||||||
|
Tutti i miglioramenti tecnici proposti nel CHANGELOG.md sono stati implementati:
|
||||||
|
|
||||||
|
1. ✅ Separazione logica business dai controller (service layer)
|
||||||
|
2. ✅ Dependency injection container (Awilix)
|
||||||
|
3. ✅ Request/Response DTOs (Joi validation)
|
||||||
|
4. ✅ Standardizzazione formato risposte API
|
||||||
|
5. ✅ Error handling centralizzato
|
||||||
|
6. ✅ Constants file per magic numbers/strings
|
||||||
|
7. ⏭️ Type checking con TypeScript - SKIPPED (come pianificato)
|
||||||
|
8. ✅ Code linting (ESLint) e formatting (Prettier)
|
||||||
|
|
||||||
|
## 🚀 Prossimi Passi per Avviare il Backend
|
||||||
|
|
||||||
|
### 1. Installare le Dipendenze
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/claude/scomettiapp/backend-node
|
||||||
|
|
||||||
|
# Installare dipendenze di produzione
|
||||||
|
npm install awilix@^10.0.2 joi@^17.12.1
|
||||||
|
|
||||||
|
# Installare dipendenze di sviluppo
|
||||||
|
npm install --save-dev eslint@^8.56.0 eslint-config-airbnb-base@^15.0.0 eslint-plugin-import@^2.29.1 prettier@^3.2.4 eslint-config-prettier@^9.1.0 eslint-plugin-prettier@^5.1.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verificare Linting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Controllare errori di linting
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Fixare automaticamente errori di linting
|
||||||
|
npm run lint:fix
|
||||||
|
|
||||||
|
# Formattare tutto il codice
|
||||||
|
npm run format
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Avviare il Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Output atteso:
|
||||||
|
```
|
||||||
|
Connected to MongoDB
|
||||||
|
Connected to PostgreSQL
|
||||||
|
PostgreSQL models synchronized
|
||||||
|
Server is running on http://localhost:3000
|
||||||
|
Environment: development
|
||||||
|
API v1 endpoints available at /api/v1/
|
||||||
|
Legacy endpoints available at /api/legacy/
|
||||||
|
Health check available at /health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing dell'Implementazione
|
||||||
|
|
||||||
|
### Test 1: Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Risposta attesa:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z",
|
||||||
|
"services": {
|
||||||
|
"postgres": "connected",
|
||||||
|
"mongodb": "connected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Creare un Gruppo (v1 API)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/groups \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Test Group",
|
||||||
|
"members": [1, 2]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Risposta attesa (formato standardizzato):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Group created successfully",
|
||||||
|
"data": {
|
||||||
|
"id": 3,
|
||||||
|
"name": "Test Group",
|
||||||
|
"members": [...]
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Validazione Errore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/groups \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "",
|
||||||
|
"members": []
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Risposta attesa (errore standardizzato):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Validation failed",
|
||||||
|
"errors": [
|
||||||
|
{"field": "name", "message": "Group name is required"},
|
||||||
|
{"field": "members", "message": "At least one member is required"}
|
||||||
|
],
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 4: Not Found Error
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/v1/groups/999
|
||||||
|
```
|
||||||
|
|
||||||
|
Risposta attesa:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Group not found",
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 5: Verificare Compatibilità Legacy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Legacy route (vecchio formato, dovrebbe ancora funzionare)
|
||||||
|
curl http://localhost:3000/api/legacy/groups
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 6: Initial Data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/v1/initial-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Risposta attesa:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Initial data retrieved successfully",
|
||||||
|
"data": {
|
||||||
|
"currentUser": {...},
|
||||||
|
"users": [...],
|
||||||
|
"friends": [...],
|
||||||
|
"friendRequests": [...],
|
||||||
|
"groups": [...],
|
||||||
|
"userBets": [...],
|
||||||
|
"publicBets": [...]
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Checklist Verifica Completa
|
||||||
|
|
||||||
|
- [ ] Server si avvia senza errori
|
||||||
|
- [ ] PostgreSQL connesso
|
||||||
|
- [ ] MongoDB connesso
|
||||||
|
- [ ] Health check risponde correttamente
|
||||||
|
- [ ] Routes v1 rispondono con formato standardizzato
|
||||||
|
- [ ] Validation errors catturati e formattati
|
||||||
|
- [ ] Not found errors ritornano 404 consistente
|
||||||
|
- [ ] Routes legacy funzionano ancora (backward compatibility)
|
||||||
|
- [ ] ESLint passa senza errori critici
|
||||||
|
- [ ] Nessuna logica business nei controllers (solo nei services)
|
||||||
|
|
||||||
|
## 🎯 Endpoints API v1
|
||||||
|
|
||||||
|
### Groups
|
||||||
|
- `GET /api/v1/groups` - Lista tutti i gruppi
|
||||||
|
- `GET /api/v1/groups/:id` - Dettaglio gruppo
|
||||||
|
- `POST /api/v1/groups` - Crea gruppo (validato)
|
||||||
|
- `PUT /api/v1/groups/:id` - Aggiorna gruppo (validato)
|
||||||
|
- `DELETE /api/v1/groups/:id` - Elimina gruppo
|
||||||
|
- `PUT /api/v1/groups/:id/members` - Aggiungi membri (validato)
|
||||||
|
- `DELETE /api/v1/groups/:id/members/:userId` - Rimuovi membro
|
||||||
|
|
||||||
|
### Bets
|
||||||
|
- `GET /api/v1/bets` - Lista tutte le scommesse
|
||||||
|
- `GET /api/v1/bets/:id` - Dettaglio scommessa
|
||||||
|
- `POST /api/v1/bets` - Crea scommessa (validato)
|
||||||
|
- `PUT /api/v1/bets/:id` - Aggiorna scommessa (validato)
|
||||||
|
- `DELETE /api/v1/bets/:id` - Elimina scommessa
|
||||||
|
|
||||||
|
### Friend Requests
|
||||||
|
- `GET /api/v1/friend-requests` - Lista amici
|
||||||
|
- `GET /api/v1/friend-requests/pending` - Richieste pending
|
||||||
|
- `POST /api/v1/friend-requests` - Invia richiesta (validato)
|
||||||
|
- `POST /api/v1/friend-requests/:id/accept` - Accetta richiesta
|
||||||
|
- `DELETE /api/v1/friend-requests/:id` - Rifiuta richiesta
|
||||||
|
|
||||||
|
### Users
|
||||||
|
- `PUT /api/v1/users/current` - Aggiorna profilo (validato)
|
||||||
|
|
||||||
|
### Initial Data
|
||||||
|
- `GET /api/v1/initial-data` - Carica dati iniziali
|
||||||
|
|
||||||
|
### Health
|
||||||
|
- `GET /health` - Status database
|
||||||
|
|
||||||
|
## 🏗️ Architettura Implementata
|
||||||
|
|
||||||
|
```
|
||||||
|
Request → Routes → Validation → Controller → Service → Repository → Database
|
||||||
|
↓ ↓
|
||||||
|
Joi Schema Business Logic
|
||||||
|
↓
|
||||||
|
Custom Errors (ValidationError, etc.)
|
||||||
|
↓
|
||||||
|
Global Error Handler Middleware
|
||||||
|
↓
|
||||||
|
Standardized Response (ResponseHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Benefici dell'Architettura
|
||||||
|
|
||||||
|
1. **Separation of Concerns**: Ogni layer ha responsabilità ben definite
|
||||||
|
2. **Testability**: Services e repositories facilmente testabili con mock
|
||||||
|
3. **Maintainability**: Business logic centralizzata
|
||||||
|
4. **Consistency**: Formato risposte e errori standardizzati
|
||||||
|
5. **Scalability**: Facile aggiungere nuove feature
|
||||||
|
6. **Type Safety**: Validation layer previene dati inconsistenti
|
||||||
|
7. **Backward Compatibility**: Routes legacy funzionano ancora
|
||||||
|
8. **Flexibility**: DI container permette sostituzioni facili
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Errore: "Cannot find module 'awilix'"
|
||||||
|
Eseguire `npm install` per installare tutte le dipendenze.
|
||||||
|
|
||||||
|
### Errore: Database connection failed
|
||||||
|
Verificare che PostgreSQL e MongoDB siano in esecuzione e accessibili.
|
||||||
|
|
||||||
|
### Errore: ESLint errors
|
||||||
|
Eseguire `npm run lint:fix` per correggere automaticamente i problemi di formattazione.
|
||||||
|
|
||||||
|
### Routes legacy non funzionano
|
||||||
|
Le routes legacy sono state spostate su `/api/legacy/*` per mantenere compatibilità.
|
||||||
|
|
||||||
|
## 📝 Note Importanti
|
||||||
|
|
||||||
|
- **TypeScript**: Non implementato in questa fase (come pianificato)
|
||||||
|
- **Testing automatici**: Da implementare in fase successiva
|
||||||
|
- **Logging strutturato**: Opzionale, non implementato
|
||||||
|
- **Production**: Richiede ulteriori configurazioni (migrations, real auth, etc.)
|
||||||
|
|
||||||
|
## 🎉 Prossimi Step Consigliati
|
||||||
|
|
||||||
|
1. Installare dipendenze con npm
|
||||||
|
2. Testare tutti gli endpoint v1
|
||||||
|
3. Migrare frontend da `/api/*` a `/api/v1/*`
|
||||||
|
4. Eventualmente deprecare `/api/legacy/*`
|
||||||
|
5. Aggiungere test automatici (Jest)
|
||||||
|
6. Implementare logging strutturato (Winston)
|
||||||
|
7. Considerare migrazione a TypeScript in futuro
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
module.exports = {
|
||||||
|
// HTTP Status Codes
|
||||||
|
HTTP_STATUS: {
|
||||||
|
OK: 200,
|
||||||
|
CREATED: 201,
|
||||||
|
BAD_REQUEST: 400,
|
||||||
|
UNAUTHORIZED: 401,
|
||||||
|
FORBIDDEN: 403,
|
||||||
|
NOT_FOUND: 404,
|
||||||
|
CONFLICT: 409,
|
||||||
|
INTERNAL_SERVER_ERROR: 500,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bet Status
|
||||||
|
BET_STATUS: {
|
||||||
|
ACTIVE: 'ACTIVE',
|
||||||
|
AWAITING_VALIDATION: 'AWAITING_VALIDATION',
|
||||||
|
COMPLETED: 'COMPLETED',
|
||||||
|
EXPIRED: 'EXPIRED',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Friend Request Status
|
||||||
|
FRIEND_REQUEST_STATUS: {
|
||||||
|
PENDING: 'PENDING',
|
||||||
|
ACCEPTED: 'ACCEPTED',
|
||||||
|
REJECTED: 'REJECTED',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Participant Roles
|
||||||
|
PARTICIPANT_ROLES: {
|
||||||
|
PARTICIPANT: 'PARTICIPANT',
|
||||||
|
OBSERVER: 'OBSERVER',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pagination Defaults
|
||||||
|
PAGINATION: {
|
||||||
|
DEFAULT_PAGE: 1,
|
||||||
|
DEFAULT_LIMIT: 20,
|
||||||
|
MAX_LIMIT: 100,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mock Auth (temporaneo)
|
||||||
|
MOCK_AUTH: {
|
||||||
|
DEFAULT_USER_NAME: 'Mario Rossi',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
module.exports = {
|
||||||
|
postgres: {
|
||||||
|
uri:
|
||||||
|
process.env.POSTGRES_URI ||
|
||||||
|
'postgres://admin:admin@localhost:5432/amici_scommesse',
|
||||||
|
options: {
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: process.env.NODE_ENV === 'development' ? console.log : false,
|
||||||
|
pool: {
|
||||||
|
max: 10,
|
||||||
|
min: 0,
|
||||||
|
acquire: 30000,
|
||||||
|
idle: 10000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mongo: {
|
||||||
|
uri:
|
||||||
|
process.env.MONGO_URI ||
|
||||||
|
'mongodb://localhost:27017/amici-scommesse',
|
||||||
|
options: {
|
||||||
|
user: 'root',
|
||||||
|
pass: 'root@servovio',
|
||||||
|
authSource: 'admin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const constants = require('./constants');
|
||||||
|
const database = require('./database');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
nodeEnv: process.env.NODE_ENV || 'development',
|
||||||
|
isDevelopment: process.env.NODE_ENV === 'development',
|
||||||
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
|
...constants,
|
||||||
|
database,
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
const ResponseHandler = require('../shared/response.handler');
|
||||||
|
|
||||||
|
class BetsController {
|
||||||
|
constructor({ betsService }) {
|
||||||
|
this.betsService = betsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllBets(req, res, next) {
|
||||||
|
try {
|
||||||
|
const bets = await this.betsService.getAllBets();
|
||||||
|
return ResponseHandler.success(res, bets, 'Bets retrieved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBetById(req, res, next) {
|
||||||
|
try {
|
||||||
|
const bet = await this.betsService.getBetById(req.params.id);
|
||||||
|
return ResponseHandler.success(res, bet, 'Bet retrieved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBet(req, res, next) {
|
||||||
|
try {
|
||||||
|
const bet = await this.betsService.createBet(req.body);
|
||||||
|
return ResponseHandler.created(res, bet, 'Bet created successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBet(req, res, next) {
|
||||||
|
try {
|
||||||
|
const bet = await this.betsService.updateBet(req.params.id, req.body);
|
||||||
|
return ResponseHandler.success(res, bet, 'Bet updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBet(req, res, next) {
|
||||||
|
try {
|
||||||
|
await this.betsService.deleteBet(req.params.id);
|
||||||
|
return ResponseHandler.success(res, null, 'Bet deleted successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BetsController;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const ResponseHandler = require('../shared/response.handler');
|
||||||
|
|
||||||
|
class FriendsController {
|
||||||
|
constructor({ friendsService }) {
|
||||||
|
this.friendsService = friendsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFriends(req, res, next) {
|
||||||
|
try {
|
||||||
|
const friends = await this.friendsService.getFriendsByUserId(req.userId);
|
||||||
|
return ResponseHandler.success(res, friends, 'Friends retrieved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPendingRequests(req, res, next) {
|
||||||
|
try {
|
||||||
|
const requests = await this.friendsService.getPendingRequests(req.userId);
|
||||||
|
return ResponseHandler.success(
|
||||||
|
res,
|
||||||
|
requests,
|
||||||
|
'Pending requests retrieved successfully'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendRequest(req, res, next) {
|
||||||
|
try {
|
||||||
|
const request = await this.friendsService.sendFriendRequest(
|
||||||
|
req.userId,
|
||||||
|
req.body.identifier
|
||||||
|
);
|
||||||
|
return ResponseHandler.created(res, request, 'Friend request sent successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptRequest(req, res, next) {
|
||||||
|
try {
|
||||||
|
const result = await this.friendsService.acceptFriendRequest(
|
||||||
|
req.params.id,
|
||||||
|
req.userId
|
||||||
|
);
|
||||||
|
return ResponseHandler.success(res, result, result.message);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectRequest(req, res, next) {
|
||||||
|
try {
|
||||||
|
const result = await this.friendsService.rejectFriendRequest(
|
||||||
|
req.params.id,
|
||||||
|
req.userId
|
||||||
|
);
|
||||||
|
return ResponseHandler.success(res, result, result.message);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FriendsController;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
const ResponseHandler = require('../shared/response.handler');
|
||||||
|
|
||||||
|
class GroupsController {
|
||||||
|
constructor({ groupsService }) {
|
||||||
|
this.groupsService = groupsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllGroups(req, res, next) {
|
||||||
|
try {
|
||||||
|
const groups = await this.groupsService.getAllGroups();
|
||||||
|
return ResponseHandler.success(res, groups, 'Groups retrieved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupById(req, res, next) {
|
||||||
|
try {
|
||||||
|
const group = await this.groupsService.getGroupById(req.params.id);
|
||||||
|
return ResponseHandler.success(res, group, 'Group retrieved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createGroup(req, res, next) {
|
||||||
|
try {
|
||||||
|
const group = await this.groupsService.createGroup(req.body);
|
||||||
|
return ResponseHandler.created(res, group, 'Group created successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGroup(req, res, next) {
|
||||||
|
try {
|
||||||
|
const group = await this.groupsService.updateGroup(req.params.id, req.body);
|
||||||
|
return ResponseHandler.success(res, group, 'Group updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGroup(req, res, next) {
|
||||||
|
try {
|
||||||
|
await this.groupsService.deleteGroup(req.params.id);
|
||||||
|
return ResponseHandler.success(res, null, 'Group deleted successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMembers(req, res, next) {
|
||||||
|
try {
|
||||||
|
const group = await this.groupsService.addMembersToGroup(
|
||||||
|
req.params.id,
|
||||||
|
req.body.userIds
|
||||||
|
);
|
||||||
|
return ResponseHandler.success(res, group, 'Members added successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeMember(req, res, next) {
|
||||||
|
try {
|
||||||
|
const group = await this.groupsService.removeMemberFromGroup(
|
||||||
|
req.params.id,
|
||||||
|
req.params.userId
|
||||||
|
);
|
||||||
|
return ResponseHandler.success(res, group, 'Member removed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GroupsController;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const ResponseHandler = require('../shared/response.handler');
|
||||||
|
|
||||||
|
class InitialDataController {
|
||||||
|
constructor({ usersService, friendsService, groupsService, betsService }) {
|
||||||
|
this.usersService = usersService;
|
||||||
|
this.friendsService = friendsService;
|
||||||
|
this.groupsService = groupsService;
|
||||||
|
this.betsService = betsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInitialData(req, res, next) {
|
||||||
|
try {
|
||||||
|
const currentUser = await this.usersService.getUserById(req.userId);
|
||||||
|
const users = await this.usersService.getAllUsers();
|
||||||
|
const friends = await this.friendsService.getFriendsByUserId(req.userId);
|
||||||
|
const friendRequests = await this.friendsService.getPendingRequests(req.userId);
|
||||||
|
const groups = await this.groupsService.getAllGroups();
|
||||||
|
const userBets = await this.betsService.getUserBets(req.userId);
|
||||||
|
const publicBets = await this.betsService.getPublicBetsExcludingUser(req.userId);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
currentUser,
|
||||||
|
users,
|
||||||
|
friends,
|
||||||
|
friendRequests,
|
||||||
|
groups,
|
||||||
|
userBets,
|
||||||
|
publicBets,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ResponseHandler.success(res, data, 'Initial data retrieved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = InitialDataController;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const ResponseHandler = require('../shared/response.handler');
|
||||||
|
|
||||||
|
class UsersController {
|
||||||
|
constructor({ usersService }) {
|
||||||
|
this.usersService = usersService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCurrentUser(req, res, next) {
|
||||||
|
try {
|
||||||
|
const user = await this.usersService.updateUserProfile(req.userId, req.body);
|
||||||
|
return ResponseHandler.success(res, user, 'User profile updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UsersController;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
const { createContainer, asClass, asFunction, asValue } = require('awilix');
|
||||||
|
const { Sequelize } = require('sequelize');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
function setupContainer() {
|
||||||
|
const container = createContainer();
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
sequelize: asFunction(() => {
|
||||||
|
const sequelize = new Sequelize(
|
||||||
|
config.database.postgres.uri,
|
||||||
|
config.database.postgres.options
|
||||||
|
);
|
||||||
|
|
||||||
|
const modelsDir = path.join(__dirname, '../models');
|
||||||
|
fs.readdirSync(modelsDir)
|
||||||
|
.filter(
|
||||||
|
(file) =>
|
||||||
|
file.indexOf('.') !== 0 &&
|
||||||
|
file.slice(-3) === '.js' &&
|
||||||
|
file !== 'bet.model.js' &&
|
||||||
|
file !== 'associations.js'
|
||||||
|
)
|
||||||
|
.forEach((file) => {
|
||||||
|
require(path.join(modelsDir, file))(sequelize);
|
||||||
|
});
|
||||||
|
|
||||||
|
const defineAssociations = require('../models/associations');
|
||||||
|
defineAssociations(sequelize);
|
||||||
|
|
||||||
|
return sequelize;
|
||||||
|
}).singleton(),
|
||||||
|
|
||||||
|
mongoose: asValue(mongoose),
|
||||||
|
});
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
usersRepository: asClass(
|
||||||
|
require('../repositories/postgres/users.repository')
|
||||||
|
).scoped(),
|
||||||
|
groupsRepository: asClass(
|
||||||
|
require('../repositories/postgres/groups.repository')
|
||||||
|
).scoped(),
|
||||||
|
friendshipsRepository: asClass(
|
||||||
|
require('../repositories/postgres/friendships.repository')
|
||||||
|
).scoped(),
|
||||||
|
friendRequestsRepository: asClass(
|
||||||
|
require('../repositories/postgres/friendRequests.repository')
|
||||||
|
).scoped(),
|
||||||
|
betsRepository: asClass(
|
||||||
|
require('../repositories/mongo/bets.repository')
|
||||||
|
).scoped(),
|
||||||
|
});
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
dataStitchingService: asClass(
|
||||||
|
require('../services/data-stitching.service')
|
||||||
|
).scoped(),
|
||||||
|
usersService: asClass(require('../services/users.service')).scoped(),
|
||||||
|
groupsService: asClass(require('../services/groups.service')).scoped(),
|
||||||
|
friendsService: asClass(require('../services/friends.service')).scoped(),
|
||||||
|
betsService: asClass(require('../services/bets.service')).scoped(),
|
||||||
|
});
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
betsController: asClass(require('../controllers/bets.controller')).scoped(),
|
||||||
|
groupsController: asClass(require('../controllers/groups.controller')).scoped(),
|
||||||
|
usersController: asClass(require('../controllers/users.controller')).scoped(),
|
||||||
|
friendsController: asClass(require('../controllers/friends.controller')).scoped(),
|
||||||
|
initialDataController: asClass(
|
||||||
|
require('../controllers/initialData.controller')
|
||||||
|
).scoped(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = setupContainer;
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ========================================
|
||||||
|
# PostgreSQL Database (Development)
|
||||||
|
# ========================================
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: amici-scommesse-postgres-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-admin}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data_dev:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-admin} -d ${POSTGRES_DB:-amici_scommesse}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network-dev
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# MongoDB Database (Development)
|
||||||
|
# ========================================
|
||||||
|
mongodb:
|
||||||
|
image: mongo:7-jammy
|
||||||
|
container_name: amici-scommesse-mongodb-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-root}
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-root@servovio}
|
||||||
|
MONGO_INITDB_DATABASE: ${MONGO_DB:-amici-scommesse}
|
||||||
|
ports:
|
||||||
|
- "${MONGO_PORT:-27017}:27017"
|
||||||
|
volumes:
|
||||||
|
- mongodb_data_dev:/data/db
|
||||||
|
- mongodb_config_dev:/data/configdb
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network-dev
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Backend Node.js (Development with Hot Reload)
|
||||||
|
# ========================================
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: development
|
||||||
|
container_name: amici-scommesse-backend-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
mongodb:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
PORT: ${PORT:-3000}
|
||||||
|
|
||||||
|
# PostgreSQL connection
|
||||||
|
POSTGRES_HOST: postgres
|
||||||
|
POSTGRES_PORT: 5432
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-admin}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin}
|
||||||
|
POSTGRES_URI: postgres://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD:-admin}@postgres:5432/${POSTGRES_DB:-amici_scommesse}
|
||||||
|
|
||||||
|
# MongoDB connection
|
||||||
|
MONGO_HOST: mongodb
|
||||||
|
MONGO_PORT: 27017
|
||||||
|
MONGO_DB: ${MONGO_DB:-amici-scommesse}
|
||||||
|
MONGO_USER: ${MONGO_USER:-root}
|
||||||
|
MONGO_PASSWORD: ${MONGO_PASSWORD:-root@servovio}
|
||||||
|
MONGO_AUTH_SOURCE: ${MONGO_AUTH_SOURCE:-admin}
|
||||||
|
MONGO_URI: mongodb://${MONGO_USER:-root}:${MONGO_PASSWORD:-root@servovio}@mongodb:27017/${MONGO_DB:-amici-scommesse}?authSource=${MONGO_AUTH_SOURCE:-admin}
|
||||||
|
|
||||||
|
# Development config
|
||||||
|
LOG_LEVEL: debug
|
||||||
|
ENABLE_SQL_LOGGING: true
|
||||||
|
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173}
|
||||||
|
MOCK_USER_NAME: ${MOCK_USER_NAME:-Mario Rossi}
|
||||||
|
DB_SYNC_FORCE: true
|
||||||
|
ports:
|
||||||
|
- "${PORT:-3000}:3000"
|
||||||
|
volumes:
|
||||||
|
# Mount source code for hot reload
|
||||||
|
- ./:/app
|
||||||
|
- /app/node_modules
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 40s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network-dev
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Mongo Express (Optional - Web UI for MongoDB)
|
||||||
|
# ========================================
|
||||||
|
mongo-express:
|
||||||
|
image: mongo-express:latest
|
||||||
|
container_name: amici-scommesse-mongo-express
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
environment:
|
||||||
|
ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGO_USER:-root}
|
||||||
|
ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGO_PASSWORD:-root@servovio}
|
||||||
|
ME_CONFIG_MONGODB_URL: mongodb://${MONGO_USER:-root}:${MONGO_PASSWORD:-root@servovio}@mongodb:27017/
|
||||||
|
ME_CONFIG_BASICAUTH_USERNAME: admin
|
||||||
|
ME_CONFIG_BASICAUTH_PASSWORD: admin
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network-dev
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# pgAdmin (Optional - Web UI for PostgreSQL)
|
||||||
|
# ========================================
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4:latest
|
||||||
|
container_name: amici-scommesse-pgadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: admin@admin.com
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: admin
|
||||||
|
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||||
|
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
||||||
|
ports:
|
||||||
|
- "5050:80"
|
||||||
|
volumes:
|
||||||
|
- pgadmin_data_dev:/var/lib/pgadmin
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network-dev
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Networks
|
||||||
|
# ========================================
|
||||||
|
networks:
|
||||||
|
amici-scommesse-network-dev:
|
||||||
|
driver: bridge
|
||||||
|
name: amici-scommesse-network-dev
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Volumes
|
||||||
|
# ========================================
|
||||||
|
volumes:
|
||||||
|
postgres_data_dev:
|
||||||
|
name: amici-scommesse-postgres-data-dev
|
||||||
|
mongodb_data_dev:
|
||||||
|
name: amici-scommesse-mongodb-data-dev
|
||||||
|
mongodb_config_dev:
|
||||||
|
name: amici-scommesse-mongodb-config-dev
|
||||||
|
pgadmin_data_dev:
|
||||||
|
name: amici-scommesse-pgadmin-data-dev
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ========================================
|
||||||
|
# PostgreSQL Database
|
||||||
|
# ========================================
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: amici-scommesse-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-admin}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-admin} -d ${POSTGRES_DB:-amici_scommesse}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# MongoDB Database
|
||||||
|
# ========================================
|
||||||
|
mongodb:
|
||||||
|
image: mongo:7-jammy
|
||||||
|
container_name: amici-scommesse-mongodb
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-root}
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-root@servovio}
|
||||||
|
MONGO_INITDB_DATABASE: ${MONGO_DB:-amici-scommesse}
|
||||||
|
ports:
|
||||||
|
- "${MONGO_PORT:-27017}:27017"
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
- mongodb_config:/data/configdb
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Backend Node.js (Production)
|
||||||
|
# ========================================
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: production
|
||||||
|
container_name: amici-scommesse-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
mongodb:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: ${PORT:-3000}
|
||||||
|
|
||||||
|
# PostgreSQL connection (using service name as host)
|
||||||
|
POSTGRES_HOST: postgres
|
||||||
|
POSTGRES_PORT: 5432
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-admin}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin}
|
||||||
|
POSTGRES_URI: postgres://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD:-admin}@postgres:5432/${POSTGRES_DB:-amici_scommesse}
|
||||||
|
|
||||||
|
# MongoDB connection (using service name as host)
|
||||||
|
MONGO_HOST: mongodb
|
||||||
|
MONGO_PORT: 27017
|
||||||
|
MONGO_DB: ${MONGO_DB:-amici-scommesse}
|
||||||
|
MONGO_USER: ${MONGO_USER:-root}
|
||||||
|
MONGO_PASSWORD: ${MONGO_PASSWORD:-root@servovio}
|
||||||
|
MONGO_AUTH_SOURCE: ${MONGO_AUTH_SOURCE:-admin}
|
||||||
|
MONGO_URI: mongodb://${MONGO_USER:-root}:${MONGO_PASSWORD:-root@servovio}@mongodb:27017/${MONGO_DB:-amici-scommesse}?authSource=${MONGO_AUTH_SOURCE:-admin}
|
||||||
|
|
||||||
|
# Application config
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||||
|
ENABLE_SQL_LOGGING: ${ENABLE_SQL_LOGGING:-false}
|
||||||
|
CORS_ORIGIN: ${CORS_ORIGIN:-*}
|
||||||
|
MOCK_USER_NAME: ${MOCK_USER_NAME:-Mario Rossi}
|
||||||
|
DB_SYNC_FORCE: ${DB_SYNC_FORCE:-false}
|
||||||
|
ports:
|
||||||
|
- "${PORT:-3000}:3000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 40s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- amici-scommesse-network
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Networks
|
||||||
|
# ========================================
|
||||||
|
networks:
|
||||||
|
amici-scommesse-network:
|
||||||
|
driver: bridge
|
||||||
|
name: amici-scommesse-network
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Volumes
|
||||||
|
# ========================================
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
name: amici-scommesse-postgres-data
|
||||||
|
mongodb_data:
|
||||||
|
name: amici-scommesse-mongodb-data
|
||||||
|
mongodb_config:
|
||||||
|
name: amici-scommesse-mongodb-config
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
-- PostgreSQL schema initialization for core models
|
||||||
|
-- Derived from Sequelize models: User, Group, Friendship, FriendRequest, and the User<->Group join table
|
||||||
|
|
||||||
|
-- Drop tables in dependency order
|
||||||
|
DROP TABLE IF EXISTS "UserGroups" CASCADE;
|
||||||
|
DROP TABLE IF EXISTS friend_requests CASCADE;
|
||||||
|
DROP TABLE IF EXISTS friendships CASCADE;
|
||||||
|
DROP TABLE IF EXISTS groups CASCADE;
|
||||||
|
DROP TABLE IF EXISTS users CASCADE;
|
||||||
|
|
||||||
|
-- users table
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
"email" VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
"avatarUrl" VARCHAR(255) NOT NULL,
|
||||||
|
score INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- groups table
|
||||||
|
CREATE TABLE groups (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- friendships table
|
||||||
|
-- In-model hook ensures user1Id < user2Id at application level; we also add a DB check for safety.
|
||||||
|
CREATE TABLE friendships (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
"user1Id" INTEGER NOT NULL,
|
||||||
|
"user2Id" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT friendships_user_order_chk CHECK ("user1Id" < "user2Id"),
|
||||||
|
CONSTRAINT friendships_user_pair_uniq UNIQUE ("user1Id", "user2Id"),
|
||||||
|
CONSTRAINT friendships_user1_fk FOREIGN KEY ("user1Id") REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT friendships_user2_fk FOREIGN KEY ("user2Id") REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- friend_requests table (with Sequelize default timestamps)
|
||||||
|
CREATE TABLE friend_requests (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||||
|
"fromUserId" INTEGER NOT NULL,
|
||||||
|
"toUserId" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT friend_requests_from_fk FOREIGN KEY ("fromUserId") REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT friend_requests_to_fk FOREIGN KEY ("toUserId") REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Optional: keep updatedAt current via trigger (Sequelize usually updates it in app code)
|
||||||
|
-- You can uncomment the below if you want DB-side auto update
|
||||||
|
-- CREATE OR REPLACE FUNCTION set_updated_at()
|
||||||
|
-- RETURNS TRIGGER AS $$
|
||||||
|
-- BEGIN
|
||||||
|
-- NEW."updatedAt" = NOW();
|
||||||
|
-- RETURN NEW;
|
||||||
|
-- END;
|
||||||
|
-- $$ LANGUAGE plpgsql;
|
||||||
|
-- CREATE TRIGGER friend_requests_set_updated_at
|
||||||
|
-- BEFORE UPDATE ON friend_requests
|
||||||
|
-- FOR EACH ROW
|
||||||
|
-- EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- User <-> Group many-to-many join table
|
||||||
|
CREATE TABLE "UserGroups" (
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"groupId" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT usergroups_user_fk FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT usergroups_group_fk FOREIGN KEY ("groupId") REFERENCES groups(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT usergroups_user_group_uniq UNIQUE ("userId", "groupId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Helpful indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_from ON friend_requests ("fromUserId");
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_to ON friend_requests ("toUserId");
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friendships_user1 ON friendships ("user1Id");
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friendships_user2 ON friendships ("user2Id");
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { User } = sequelize.models;
|
||||||
|
let MOCK_USER_ID = null;
|
||||||
|
|
||||||
|
return async (req, res, next) => {
|
||||||
|
if (!MOCK_USER_ID) {
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: { name: config.MOCK_AUTH.DEFAULT_USER_NAME },
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
MOCK_USER_ID = user.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.userId = MOCK_USER_ID;
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const { AppError } = require('../shared/errors');
|
||||||
|
const ResponseHandler = require('../shared/response.handler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
const errorHandler = (err, req, res, next) => {
|
||||||
|
if (config.isDevelopment) {
|
||||||
|
console.error('Error caught by middleware:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AppError) {
|
||||||
|
return ResponseHandler.error(res, err.message, err.statusCode, err.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
err.name === 'SequelizeValidationError' ||
|
||||||
|
err.name === 'SequelizeUniqueConstraintError'
|
||||||
|
) {
|
||||||
|
const errors = err.errors
|
||||||
|
? err.errors.map((e) => ({ field: e.path, message: e.message }))
|
||||||
|
: null;
|
||||||
|
return ResponseHandler.badRequest(res, 'Validation error', errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.name === 'ValidationError' && err.errors) {
|
||||||
|
const errors = Object.keys(err.errors).map((key) => ({
|
||||||
|
field: key,
|
||||||
|
message: err.errors[key].message,
|
||||||
|
}));
|
||||||
|
return ResponseHandler.badRequest(res, 'Validation error', errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.name === 'CastError') {
|
||||||
|
return ResponseHandler.badRequest(res, 'Invalid ID format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = config.isDevelopment ? err.message : 'Internal server error';
|
||||||
|
return ResponseHandler.internalError(res, message, err);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = errorHandler;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const { ValidationError } = require('../shared/errors');
|
||||||
|
|
||||||
|
const validate = (schema) => {
|
||||||
|
return (req, res, next) => {
|
||||||
|
const { error, value } = schema.validate(req.body, {
|
||||||
|
abortEarly: false,
|
||||||
|
stripUnknown: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const errors = error.details.map((detail) => ({
|
||||||
|
field: detail.path.join('.'),
|
||||||
|
message: detail.message,
|
||||||
|
}));
|
||||||
|
return next(new ValidationError('Validation failed', errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body = value;
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { validate };
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { User, Group, FriendRequest, Friendship } = sequelize.models;
|
||||||
|
|
||||||
|
// User <-> Group: Many-to-Many
|
||||||
|
// A User can be in many Groups, and a Group can have many Users (members).
|
||||||
|
const UserGroup = sequelize.define('UserGroup', {}, { timestamps: false });
|
||||||
|
User.belongsToMany(Group, { through: UserGroup, foreignKey: 'userId' });
|
||||||
|
Group.belongsToMany(User, { through: UserGroup, as: 'members', foreignKey: 'groupId' });
|
||||||
|
|
||||||
|
// Friendship is a direct link between two users.
|
||||||
|
// user1Id will always be less than user2Id to ensure uniqueness.
|
||||||
|
Friendship.belongsTo(User, { as: 'user1', foreignKey: 'user1Id' });
|
||||||
|
Friendship.belongsTo(User, { as: 'user2', foreignKey: 'user2Id' });
|
||||||
|
|
||||||
|
// User <-> FriendRequest: One-to-Many
|
||||||
|
// A User can send many friend requests and receive many friend requests.
|
||||||
|
User.hasMany(FriendRequest, { as: 'sentRequests', foreignKey: 'fromUserId', onDelete: 'CASCADE' });
|
||||||
|
User.hasMany(FriendRequest, { as: 'receivedRequests', foreignKey: 'toUserId', onDelete: 'CASCADE' });
|
||||||
|
FriendRequest.belongsTo(User, { as: 'fromUser', foreignKey: 'fromUserId' });
|
||||||
|
FriendRequest.belongsTo(User, { as: 'toUser', foreignKey: 'toUserId' });
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const BetParticipantSchema = new Schema({
|
||||||
|
user: { type: Number, required: true }, // Stores the PostgreSQL User ID
|
||||||
|
role: { type: String, enum: ['PARTICIPANT', 'OBSERVER'], required: true },
|
||||||
|
}, { _id: false });
|
||||||
|
|
||||||
|
const BetSchema = new mongoose.Schema({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
description: { type: String },
|
||||||
|
stake: { type: String, required: true },
|
||||||
|
participants: [BetParticipantSchema],
|
||||||
|
status: { type: String, enum: ['ACTIVE', 'AWAITING_VALIDATION', 'COMPLETED', 'EXPIRED'], required: true },
|
||||||
|
startDate: { type: Date, default: Date.now },
|
||||||
|
endDate: { type: Date, default: null },
|
||||||
|
isPublic: { type: Boolean, default: false },
|
||||||
|
groupId: { type: String, default: null }, // Stores the PostgreSQL Group ID
|
||||||
|
winner: { type: Number, default: null }, // Stores the PostgreSQL User ID
|
||||||
|
});
|
||||||
|
|
||||||
|
BetSchema.set('toJSON', {
|
||||||
|
virtuals: true,
|
||||||
|
versionKey: false,
|
||||||
|
transform: function (doc, ret) {
|
||||||
|
delete ret._id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Bet', BetSchema);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const FriendRequest = sequelize.define('FriendRequest', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
defaultValue: 'PENDING',
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
// Foreign keys 'fromUserId' and 'toUserId' are added via associations
|
||||||
|
}, {
|
||||||
|
// Uses default `createdAt` and `updatedAt` timestamps from Sequelize
|
||||||
|
tableName: 'friend_requests'
|
||||||
|
});
|
||||||
|
return FriendRequest;
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const Friendship = sequelize.define('Friendship', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
// Foreign keys 'user1Id' and 'user2Id' are added via associations
|
||||||
|
}, {
|
||||||
|
timestamps: false,
|
||||||
|
tableName: 'friendships',
|
||||||
|
hooks: {
|
||||||
|
// Ensure user1Id is always less than user2Id to guarantee
|
||||||
|
// a unique friendship pair regardless of creation order.
|
||||||
|
beforeValidate: (friendship) => {
|
||||||
|
if (friendship.user1Id > friendship.user2Id) {
|
||||||
|
[friendship.user1Id, friendship.user2Id] = [friendship.user2Id, friendship.user1Id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Friendship;
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const Group = sequelize.define('Group', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: false,
|
||||||
|
tableName: 'groups'
|
||||||
|
});
|
||||||
|
return Group;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const User = sequelize.define('User', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
validate: {
|
||||||
|
isEmail: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
avatarUrl: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
timestamps: false,
|
||||||
|
tableName: 'users'
|
||||||
|
});
|
||||||
|
return User;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
Generated
+4780
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "amici-scommesse-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Backend for the Amici Scommesse app",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node server.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write \"**/*.js\"",
|
||||||
|
"docker:dev": "docker compose -f docker-compose.dev.yml up -d",
|
||||||
|
"docker:dev:logs": "docker compose -f docker-compose.dev.yml logs -f backend",
|
||||||
|
"docker:dev:down": "docker compose -f docker-compose.dev.yml down",
|
||||||
|
"docker:dev:rebuild": "docker compose -f docker-compose.dev.yml up -d --build",
|
||||||
|
"docker:prod": "docker compose up -d",
|
||||||
|
"docker:prod:logs": "docker compose logs -f backend",
|
||||||
|
"docker:prod:down": "docker compose down",
|
||||||
|
"docker:prod:rebuild": "docker compose up -d --build",
|
||||||
|
"docker:clean": "docker compose down -v && docker system prune -f",
|
||||||
|
"docker:ps": "docker compose ps",
|
||||||
|
"docker:restart": "docker compose restart backend"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"awilix": "^10.0.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"joi": "^17.12.1",
|
||||||
|
"mongoose": "^8.4.0",
|
||||||
|
"pg": "^8.11.5",
|
||||||
|
"pg-hstore": "^2.3.4",
|
||||||
|
"sequelize": "^6.37.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"prettier": "^3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
const Bet = require('../../models/bet.model');
|
||||||
|
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||||
|
const config = require('../../config');
|
||||||
|
|
||||||
|
class BetsRepository {
|
||||||
|
async findAll(filter = {}, options = {}) {
|
||||||
|
try {
|
||||||
|
const { sort = { startDate: -1 }, limit, skip } = options;
|
||||||
|
let query = Bet.find(filter);
|
||||||
|
|
||||||
|
if (sort) query = query.sort(sort);
|
||||||
|
if (limit) query = query.limit(limit);
|
||||||
|
if (skip) query = query.skip(skip);
|
||||||
|
|
||||||
|
return await query.exec();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to fetch bets', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id) {
|
||||||
|
try {
|
||||||
|
const bet = await Bet.findById(id);
|
||||||
|
if (!bet) {
|
||||||
|
throw new NotFoundError('Bet not found');
|
||||||
|
}
|
||||||
|
return bet;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
if (error.name === 'CastError') {
|
||||||
|
throw new NotFoundError('Bet not found');
|
||||||
|
}
|
||||||
|
throw new DatabaseError(`Failed to fetch bet with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUser(userId) {
|
||||||
|
try {
|
||||||
|
return await Bet.find({ 'participants.user': userId }).sort({ startDate: -1 });
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError(`Failed to fetch bets for user ${userId}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByGroup(groupId) {
|
||||||
|
try {
|
||||||
|
return await Bet.find({ groupId }).sort({ startDate: -1 });
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError(`Failed to fetch bets for group ${groupId}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPublicBetsExcludingUser(userId) {
|
||||||
|
try {
|
||||||
|
return await Bet.find({
|
||||||
|
isPublic: true,
|
||||||
|
'participants.user': { $ne: userId },
|
||||||
|
}).sort({ startDate: -1 });
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to fetch public bets', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(betData) {
|
||||||
|
try {
|
||||||
|
const bet = new Bet({
|
||||||
|
...betData,
|
||||||
|
startDate: new Date(),
|
||||||
|
status: config.BET_STATUS.ACTIVE,
|
||||||
|
});
|
||||||
|
return await bet.save();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to create bet', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, updateData) {
|
||||||
|
try {
|
||||||
|
const bet = await this.findById(id);
|
||||||
|
Object.assign(bet, updateData);
|
||||||
|
return await bet.save();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to update bet with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
try {
|
||||||
|
const result = await Bet.findByIdAndDelete(id);
|
||||||
|
if (!result) {
|
||||||
|
throw new NotFoundError('Bet not found');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to delete bet with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BetsRepository;
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
class FriendRequestsRepository {
|
||||||
|
constructor({ sequelize }) {
|
||||||
|
this.sequelize = sequelize;
|
||||||
|
this.FriendRequest = sequelize.models.FriendRequest;
|
||||||
|
this.User = sequelize.models.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id) {
|
||||||
|
try {
|
||||||
|
const request = await this.FriendRequest.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{ model: this.User, as: 'fromUser' },
|
||||||
|
{ model: this.User, as: 'toUser' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (!request) {
|
||||||
|
throw new NotFoundError('Friend request not found');
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to fetch friend request with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPendingByUserId(userId) {
|
||||||
|
try {
|
||||||
|
return await this.FriendRequest.findAll({
|
||||||
|
where: {
|
||||||
|
toUserId: userId,
|
||||||
|
status: 'PENDING',
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{ model: this.User, as: 'fromUser' },
|
||||||
|
{ model: this.User, as: 'toUser' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError(
|
||||||
|
`Failed to fetch pending requests for user ${userId}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findExisting(fromUserId, toUserId) {
|
||||||
|
try {
|
||||||
|
return await this.FriendRequest.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ fromUserId, toUserId },
|
||||||
|
{ fromUserId: toUserId, toUserId: fromUserId },
|
||||||
|
],
|
||||||
|
status: 'PENDING',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to check existing friend request', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(fromUserId, toUserId) {
|
||||||
|
try {
|
||||||
|
return await this.FriendRequest.create({
|
||||||
|
fromUserId,
|
||||||
|
toUserId,
|
||||||
|
status: 'PENDING',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to create friend request', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStatus(id, status) {
|
||||||
|
try {
|
||||||
|
const request = await this.findById(id);
|
||||||
|
return await request.update({ status });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError('Failed to update friend request status', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
try {
|
||||||
|
const request = await this.findById(id);
|
||||||
|
await request.destroy();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError('Failed to delete friend request', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FriendRequestsRepository;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
class FriendshipsRepository {
|
||||||
|
constructor({ sequelize }) {
|
||||||
|
this.sequelize = sequelize;
|
||||||
|
this.Friendship = sequelize.models.Friendship;
|
||||||
|
this.User = sequelize.models.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUserId(userId) {
|
||||||
|
try {
|
||||||
|
const friendships = await this.Friendship.findAll({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [{ user1Id: userId }, { user2Id: userId }],
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{ model: this.User, as: 'user1' },
|
||||||
|
{ model: this.User, as: 'user2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return friendships.map((friendship) => {
|
||||||
|
const friend =
|
||||||
|
friendship.user1.id === userId ? friendship.user2 : friendship.user1;
|
||||||
|
return friend;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError(`Failed to fetch friendships for user ${userId}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(user1Id, user2Id) {
|
||||||
|
try {
|
||||||
|
const minId = Math.min(user1Id, user2Id);
|
||||||
|
const maxId = Math.max(user1Id, user2Id);
|
||||||
|
|
||||||
|
const friendship = await this.Friendship.findOne({
|
||||||
|
where: { user1Id: minId, user2Id: maxId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!friendship;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to check friendship existence', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(user1Id, user2Id) {
|
||||||
|
try {
|
||||||
|
const minId = Math.min(user1Id, user2Id);
|
||||||
|
const maxId = Math.max(user1Id, user2Id);
|
||||||
|
|
||||||
|
return await this.Friendship.create({
|
||||||
|
user1Id: minId,
|
||||||
|
user2Id: maxId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to create friendship', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(user1Id, user2Id) {
|
||||||
|
try {
|
||||||
|
const minId = Math.min(user1Id, user2Id);
|
||||||
|
const maxId = Math.max(user1Id, user2Id);
|
||||||
|
|
||||||
|
const result = await this.Friendship.destroy({
|
||||||
|
where: { user1Id: minId, user2Id: maxId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return result > 0;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to delete friendship', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FriendshipsRepository;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||||
|
|
||||||
|
class GroupsRepository {
|
||||||
|
constructor({ sequelize }) {
|
||||||
|
this.sequelize = sequelize;
|
||||||
|
this.Group = sequelize.models.Group;
|
||||||
|
this.User = sequelize.models.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(options = {}) {
|
||||||
|
try {
|
||||||
|
return await this.Group.findAll(options);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to fetch groups', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id, includeMembers = false) {
|
||||||
|
try {
|
||||||
|
const options = {};
|
||||||
|
if (includeMembers) {
|
||||||
|
options.include = [
|
||||||
|
{
|
||||||
|
model: this.User,
|
||||||
|
as: 'members',
|
||||||
|
through: { attributes: [] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const group = await this.Group.findByPk(id, options);
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFoundError('Group not found');
|
||||||
|
}
|
||||||
|
return group;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to fetch group with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(groupData) {
|
||||||
|
try {
|
||||||
|
return await this.Group.create(groupData);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to create group', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMembers(groupId, userIds) {
|
||||||
|
try {
|
||||||
|
const group = await this.findById(groupId);
|
||||||
|
await group.addMembers(userIds);
|
||||||
|
return this.findById(groupId, true);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError('Failed to add members to group', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeMember(groupId, userId) {
|
||||||
|
try {
|
||||||
|
const group = await this.findById(groupId);
|
||||||
|
await group.removeMember(userId);
|
||||||
|
return this.findById(groupId, true);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError('Failed to remove member from group', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, updateData) {
|
||||||
|
try {
|
||||||
|
const group = await this.findById(id);
|
||||||
|
return await group.update(updateData);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to update group with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
try {
|
||||||
|
const group = await this.findById(id);
|
||||||
|
await group.destroy();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to delete group with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GroupsRepository;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||||
|
|
||||||
|
class UsersRepository {
|
||||||
|
constructor({ sequelize }) {
|
||||||
|
this.sequelize = sequelize;
|
||||||
|
this.User = sequelize.models.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(options = {}) {
|
||||||
|
try {
|
||||||
|
return await this.User.findAll(options);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to fetch users', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id, options = {}) {
|
||||||
|
try {
|
||||||
|
const user = await this.User.findByPk(id, options);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundError('User not found');
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to fetch user with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(where, options = {}) {
|
||||||
|
try {
|
||||||
|
return await this.User.findOne({ where, ...options });
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to find user', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(userData) {
|
||||||
|
try {
|
||||||
|
return await this.User.create(userData);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError('Failed to create user', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, updateData) {
|
||||||
|
try {
|
||||||
|
const user = await this.findById(id);
|
||||||
|
return await user.update(updateData);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to update user with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
try {
|
||||||
|
const user = await this.findById(id);
|
||||||
|
await user.destroy();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) throw error;
|
||||||
|
throw new DatabaseError(`Failed to delete user with id ${id}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UsersRepository;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
// Il router è ora una factory function che riceve l'istanza di sequelize
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// --- Middleware per simulare un utente loggato ---
|
||||||
|
const { User } = sequelize.models;
|
||||||
|
const MOCK_USER_NAME = 'Mario Rossi';
|
||||||
|
let MOCK_USER_ID = null;
|
||||||
|
|
||||||
|
router.use(async (req, res, next) => {
|
||||||
|
if (!MOCK_USER_ID) {
|
||||||
|
const user = await User.findOne({ where: { name: MOCK_USER_NAME } });
|
||||||
|
if (user) {
|
||||||
|
MOCK_USER_ID = user.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.userId = MOCK_USER_ID;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Monta i router da file separati ---
|
||||||
|
router.use(require('./initial-data.routes')(sequelize));
|
||||||
|
router.use('/bets', require('./bets.routes')(sequelize));
|
||||||
|
router.use('/groups', require('./groups.routes')(sequelize));
|
||||||
|
router.use('/friend-requests', require('./friends.routes')(sequelize));
|
||||||
|
router.use('/users', require('./users.routes')(sequelize));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Modello Mongoose per le Scommesse
|
||||||
|
const Bet = require('../models/bet.model');
|
||||||
|
|
||||||
|
// Il router è ora una factory function che riceve l'istanza di sequelize
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { populateBetsWithUsers } = require('../utils/populateBets')(sequelize);
|
||||||
|
|
||||||
|
// POST /api/bets
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const betData = req.body;
|
||||||
|
// Assicura che gli ID utente dal frontend siano correttamente parsati come interi per lo schema MongoDB
|
||||||
|
const participants = betData.participants.map(p => ({
|
||||||
|
...p,
|
||||||
|
user: parseInt(p.user, 10)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bet = new Bet({
|
||||||
|
...betData,
|
||||||
|
participants,
|
||||||
|
startDate: new Date(),
|
||||||
|
status: 'ACTIVE'
|
||||||
|
});
|
||||||
|
await bet.save();
|
||||||
|
|
||||||
|
// Popola nuovamente la scommessa creata per restituire oggetti utente completi
|
||||||
|
const populatedMongoBet = await Bet.findById(bet._id);
|
||||||
|
const finalBet = (await populateBetsWithUsers([populatedMongoBet]))[0];
|
||||||
|
res.status(201).json(finalBet);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: 'Error creating bet', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { User, Friendship, FriendRequest } = sequelize.models;
|
||||||
|
|
||||||
|
// POST /api/friend-requests
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { identifier } = req.body;
|
||||||
|
const toUser = await User.findOne({ where: { [Op.or]: [{ email: identifier }, { name: identifier }] } });
|
||||||
|
|
||||||
|
if (!toUser) return res.status(404).json({ message: 'Utente non trovato.' });
|
||||||
|
if (toUser.id === req.userId) return res.status(400).json({ message: 'Non puoi aggiungere te stesso.'});
|
||||||
|
|
||||||
|
const user1Id = Math.min(req.userId, toUser.id);
|
||||||
|
const user2Id = Math.max(req.userId, toUser.id);
|
||||||
|
const friendshipExists = await Friendship.findOne({ where: { user1Id, user2Id } });
|
||||||
|
if (friendshipExists) return res.status(400).json({ message: 'Siete già amici.'});
|
||||||
|
|
||||||
|
const requestExists = await FriendRequest.findOne({
|
||||||
|
where: { [Op.or]: [{ fromUserId: req.userId, toUserId: toUser.id }, { fromUserId: toUser.id, toUserId: req.userId }] }
|
||||||
|
});
|
||||||
|
if (requestExists) return res.status(400).json({ message: 'Richiesta già inviata.'});
|
||||||
|
|
||||||
|
const friendRequest = await FriendRequest.create({ fromUserId: req.userId, toUserId: toUser.id });
|
||||||
|
const populatedReq = await FriendRequest.findByPk(friendRequest.id, {
|
||||||
|
include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }]
|
||||||
|
});
|
||||||
|
res.status(201).json(populatedReq);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: 'Error sending friend request', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/friend-requests/:id/accept
|
||||||
|
router.post('/:id/accept', async (req, res) => {
|
||||||
|
const t = await sequelize.transaction();
|
||||||
|
try {
|
||||||
|
const request = await FriendRequest.findByPk(req.params.id, { transaction: t });
|
||||||
|
if (!request || request.toUserId !== req.userId) {
|
||||||
|
await t.rollback();
|
||||||
|
return res.status(404).json({ message: 'Request not found or not for you.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user1Id = Math.min(request.fromUserId, request.toUserId);
|
||||||
|
const user2Id = Math.max(request.fromUserId, request.toUserId);
|
||||||
|
await Friendship.create({ user1Id, user2Id }, { transaction: t });
|
||||||
|
await request.destroy({ transaction: t });
|
||||||
|
|
||||||
|
await t.commit();
|
||||||
|
const newFriend = await User.findByPk(request.fromUserId);
|
||||||
|
res.status(200).json({ message: 'Friend request accepted', newFriend });
|
||||||
|
} catch (error) {
|
||||||
|
await t.rollback();
|
||||||
|
res.status(400).json({ message: 'Error accepting friend request', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/friend-requests/:id
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const count = await FriendRequest.destroy({ where: { id: req.params.id } });
|
||||||
|
if (count === 0) return res.status(404).json({ message: 'Request not found.' });
|
||||||
|
res.status(200).json({ message: 'Friend request cancelled/declined.' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: 'Error deleting friend request', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { User, Group } = sequelize.models;
|
||||||
|
|
||||||
|
// POST /api/groups
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, members } = req.body;
|
||||||
|
const group = await Group.create({ name });
|
||||||
|
await group.setMembers(members);
|
||||||
|
const populatedGroup = await Group.findByPk(group.id, {
|
||||||
|
include: [{ model: User, as: 'members', through: { attributes: [] } }]
|
||||||
|
});
|
||||||
|
res.status(201).json(populatedGroup);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: 'Error creating group', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/groups/:id/members
|
||||||
|
router.put('/:id/members', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userIds } = req.body;
|
||||||
|
const group = await Group.findByPk(req.params.id);
|
||||||
|
if (!group) return res.status(404).json({ message: 'Group not found' });
|
||||||
|
await group.addMembers(userIds);
|
||||||
|
const populatedGroup = await Group.findByPk(group.id, {
|
||||||
|
include: [{ model: User, as: 'members', through: { attributes: [] } }]
|
||||||
|
});
|
||||||
|
res.json(populatedGroup);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: 'Error adding members to group', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/groups/:id/members/:userId
|
||||||
|
router.delete('/:id/members/:userId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const group = await Group.findByPk(req.params.id);
|
||||||
|
if (!group) return res.status(404).json({ message: 'Group not found' });
|
||||||
|
await group.removeMember(req.params.userId);
|
||||||
|
const populatedGroup = await Group.findByPk(group.id, {
|
||||||
|
include: [{ model: User, as: 'members', through: { attributes: [] } }]
|
||||||
|
});
|
||||||
|
res.json(populatedGroup);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: 'Error removing member from group', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = ({ sequelize, mongoose }) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/health', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
const pgStatus = 'connected';
|
||||||
|
|
||||||
|
const mongoStatus =
|
||||||
|
mongoose.connection.readyState === 1 ? 'connected' : 'disconnected';
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'healthy',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
services: {
|
||||||
|
postgres: pgStatus,
|
||||||
|
mongodb: mongoStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(503).json({
|
||||||
|
status: 'unhealthy',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
// Modello Mongoose per le Scommesse
|
||||||
|
const Bet = require('../models/bet.model');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { User, Friendship, FriendRequest } = sequelize.models;
|
||||||
|
const { populateBetsWithUsers } = require('../utils/populateBets')(sequelize);
|
||||||
|
|
||||||
|
// GET /api/initial-data
|
||||||
|
router.get('/initial-data', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// --- Query PostgreSQL ---
|
||||||
|
const currentUser = await User.findByPk(req.userId);
|
||||||
|
const users = await User.findAll();
|
||||||
|
|
||||||
|
const friendships = await Friendship.findAll({
|
||||||
|
where: { [Op.or]: [{ user1Id: req.userId }, { user2Id: req.userId }] },
|
||||||
|
include: [{ model: User, as: 'user1' }, { model: User, as: 'user2' }]
|
||||||
|
});
|
||||||
|
const friends = friendships.map(f => (f.user1Id === req.userId ? f.user2 : f.user1));
|
||||||
|
|
||||||
|
const receivedFriendRequests = await FriendRequest.findAll({
|
||||||
|
where: { toUserId: req.userId, status: 'PENDING' },
|
||||||
|
include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const sentFriendRequests = await FriendRequest.findAll({
|
||||||
|
where: { fromUserId: req.userId, status: 'PENDING' },
|
||||||
|
include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const userGroups = await currentUser.getGroups({
|
||||||
|
include: [{ model: User, as: 'members', through: { attributes: [] } }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Query MongoDB ---
|
||||||
|
const mongoBets = await Bet.find({ 'participants.user': req.userId }).sort({ startDate: -1 });
|
||||||
|
const mongoPublicBets = await Bet.find({ isPublic: true, 'participants.user': { $ne: req.userId } });
|
||||||
|
|
||||||
|
// --- Data Stitching ---
|
||||||
|
const bets = await populateBetsWithUsers(mongoBets);
|
||||||
|
const publicBets = await populateBetsWithUsers(mongoPublicBets);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
currentUser, users, friends, receivedFriendRequests, sentFriendRequests,
|
||||||
|
bets, publicBets, groups: userGroups
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching initial data:', error);
|
||||||
|
res.status(500).json({ message: 'Error fetching initial data', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { User } = sequelize.models;
|
||||||
|
|
||||||
|
// PUT /api/users/current
|
||||||
|
router.put('/current', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, email } = req.body;
|
||||||
|
const user = await User.findByPk(req.userId);
|
||||||
|
if (!user) return res.status(404).json({ message: 'User not found' });
|
||||||
|
|
||||||
|
user.name = name;
|
||||||
|
user.email = email;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: 'Error updating user', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { validate } = require('../../middleware/validation');
|
||||||
|
const {
|
||||||
|
createBetSchema,
|
||||||
|
updateBetSchema,
|
||||||
|
} = require('../../shared/validators/schemas/bet.schema');
|
||||||
|
|
||||||
|
module.exports = ({ betsController }) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const controller = {
|
||||||
|
getAllBets: (req, res, next) => betsController.getAllBets(req, res, next),
|
||||||
|
getBetById: (req, res, next) => betsController.getBetById(req, res, next),
|
||||||
|
createBet: (req, res, next) => betsController.createBet(req, res, next),
|
||||||
|
updateBet: (req, res, next) => betsController.updateBet(req, res, next),
|
||||||
|
deleteBet: (req, res, next) => betsController.deleteBet(req, res, next),
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/', controller.getAllBets);
|
||||||
|
router.get('/:id', controller.getBetById);
|
||||||
|
router.post('/', validate(createBetSchema), controller.createBet);
|
||||||
|
router.put('/:id', validate(updateBetSchema), controller.updateBet);
|
||||||
|
router.delete('/:id', controller.deleteBet);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { validate } = require('../../middleware/validation');
|
||||||
|
const {
|
||||||
|
sendFriendRequestSchema,
|
||||||
|
} = require('../../shared/validators/schemas/friendRequest.schema');
|
||||||
|
|
||||||
|
module.exports = ({ friendsController }) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const controller = {
|
||||||
|
getFriends: (req, res, next) => friendsController.getFriends(req, res, next),
|
||||||
|
getPendingRequests: (req, res, next) =>
|
||||||
|
friendsController.getPendingRequests(req, res, next),
|
||||||
|
sendRequest: (req, res, next) => friendsController.sendRequest(req, res, next),
|
||||||
|
acceptRequest: (req, res, next) => friendsController.acceptRequest(req, res, next),
|
||||||
|
rejectRequest: (req, res, next) => friendsController.rejectRequest(req, res, next),
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/', controller.getFriends);
|
||||||
|
router.get('/pending', controller.getPendingRequests);
|
||||||
|
router.post('/', validate(sendFriendRequestSchema), controller.sendRequest);
|
||||||
|
router.post('/:id/accept', controller.acceptRequest);
|
||||||
|
router.delete('/:id', controller.rejectRequest);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { validate } = require('../../middleware/validation');
|
||||||
|
const {
|
||||||
|
createGroupSchema,
|
||||||
|
updateGroupSchema,
|
||||||
|
addMembersSchema,
|
||||||
|
} = require('../../shared/validators/schemas/group.schema');
|
||||||
|
|
||||||
|
module.exports = ({ groupsController }) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const controller = {
|
||||||
|
getAllGroups: (req, res, next) => groupsController.getAllGroups(req, res, next),
|
||||||
|
getGroupById: (req, res, next) => groupsController.getGroupById(req, res, next),
|
||||||
|
createGroup: (req, res, next) => groupsController.createGroup(req, res, next),
|
||||||
|
updateGroup: (req, res, next) => groupsController.updateGroup(req, res, next),
|
||||||
|
deleteGroup: (req, res, next) => groupsController.deleteGroup(req, res, next),
|
||||||
|
addMembers: (req, res, next) => groupsController.addMembers(req, res, next),
|
||||||
|
removeMember: (req, res, next) => groupsController.removeMember(req, res, next),
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/', controller.getAllGroups);
|
||||||
|
router.get('/:id', controller.getGroupById);
|
||||||
|
router.post('/', validate(createGroupSchema), controller.createGroup);
|
||||||
|
router.put('/:id', validate(updateGroupSchema), controller.updateGroup);
|
||||||
|
router.delete('/:id', controller.deleteGroup);
|
||||||
|
router.put('/:id/members', validate(addMembersSchema), controller.addMembers);
|
||||||
|
router.delete('/:id/members/:userId', controller.removeMember);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = (container) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const controllers = {
|
||||||
|
groupsController: container.resolve('groupsController'),
|
||||||
|
betsController: container.resolve('betsController'),
|
||||||
|
usersController: container.resolve('usersController'),
|
||||||
|
friendsController: container.resolve('friendsController'),
|
||||||
|
initialDataController: container.resolve('initialDataController'),
|
||||||
|
};
|
||||||
|
|
||||||
|
router.use('/groups', require('./groups.routes')(controllers));
|
||||||
|
router.use('/bets', require('./bets.routes')(controllers));
|
||||||
|
router.use('/users', require('./users.routes')(controllers));
|
||||||
|
router.use('/friend-requests', require('./friends.routes')(controllers));
|
||||||
|
router.use('/initial-data', require('./initialData.routes')(controllers));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = ({ initialDataController }) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const controller = {
|
||||||
|
getInitialData: (req, res, next) => initialDataController.getInitialData(req, res, next),
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/', controller.getInitialData);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { validate } = require('../../middleware/validation');
|
||||||
|
const { updateUserSchema } = require('../../shared/validators/schemas/user.schema');
|
||||||
|
|
||||||
|
module.exports = ({ usersController }) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const controller = {
|
||||||
|
updateCurrentUser: (req, res, next) => usersController.updateCurrentUser(req, res, next),
|
||||||
|
};
|
||||||
|
|
||||||
|
router.put('/current', validate(updateUserSchema), controller.updateCurrentUser);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Bet = require('./models/bet.model');
|
||||||
|
|
||||||
|
const seedDatabase = async (sequelize) => {
|
||||||
|
const { User, Group, Friendship, FriendRequest } = sequelize.models;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userCount = await User.count();
|
||||||
|
if (userCount > 0) {
|
||||||
|
// console.log('PostgreSQL database is not empty. Skipping seed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Seeding databases...');
|
||||||
|
|
||||||
|
// PostgreSQL tables are already empty due to `sync({ force: true })` in server.js
|
||||||
|
// Clear MongoDB data
|
||||||
|
await Bet.deleteMany({});
|
||||||
|
|
||||||
|
// --- SEED POSTGRESQL ---
|
||||||
|
|
||||||
|
// Create Users
|
||||||
|
const usersData = [
|
||||||
|
{ id: 1, name: 'Mario Rossi', email: 'mario.rossi@example.com', avatarUrl: 'https://picsum.photos/id/1005/200/200', score: 120 },
|
||||||
|
{ id: 2, name: 'Luigi Verdi', email: 'luigi.verdi@example.com', avatarUrl: 'https://picsum.photos/id/1012/200/200', score: 95 },
|
||||||
|
{ id: 3, name: 'Giovanna Bianchi', email: 'giovanna.bianchi@example.com', avatarUrl: 'https://picsum.photos/id/1027/200/200', score: 150 },
|
||||||
|
{ id: 4, name: 'Anna Neri', email: 'anna.neri@example.com', avatarUrl: 'https://picsum.photos/id/1011/200/200', score: 80 },
|
||||||
|
{ id: 5, name: 'Carlo Gialli', email: 'carlo.gialli@example.com', avatarUrl: 'https://picsum.photos/id/20/200/200', score: 200 },
|
||||||
|
{ id: 6, name: 'Paolo Marroni', email: 'paolo.marroni@example.com', avatarUrl: 'https://picsum.photos/id/30/200/200', score: 50 },
|
||||||
|
];
|
||||||
|
await User.bulkCreate(usersData);
|
||||||
|
|
||||||
|
// Create Friendships
|
||||||
|
const friendshipsData = [
|
||||||
|
{ user1Id: 1, user2Id: 2 },
|
||||||
|
{ user1Id: 1, user2Id: 3 },
|
||||||
|
{ user1Id: 2, user2Id: 3 },
|
||||||
|
];
|
||||||
|
await Friendship.bulkCreate(friendshipsData);
|
||||||
|
|
||||||
|
// Create Friend Requests
|
||||||
|
const friendRequestsData = [
|
||||||
|
{ fromUserId: 4, toUserId: 1, status: 'PENDING' },
|
||||||
|
{ fromUserId: 1, toUserId: 5, status: 'PENDING' },
|
||||||
|
];
|
||||||
|
await FriendRequest.bulkCreate(friendRequestsData);
|
||||||
|
|
||||||
|
// Create Groups and add members
|
||||||
|
const group1 = await Group.create({ id: 1, name: 'Amici Stretti' });
|
||||||
|
await group1.setMembers([1, 2, 3]);
|
||||||
|
const group2 = await Group.create({ id: 2, name: 'Colleghi' });
|
||||||
|
await group2.setMembers([1, 4, 5]);
|
||||||
|
|
||||||
|
// --- SEED MONGODB ---
|
||||||
|
|
||||||
|
// Create Bets, referencing PostgreSQL IDs
|
||||||
|
const betsData = [
|
||||||
|
{ title: 'Chi vince a calcetto?', description: 'Partita di venerdì sera', stake: 'Una pizza', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 2, role: 'PARTICIPANT' }], status: 'ACTIVE', startDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), endDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), isPublic: false, groupId: group1.id.toString() },
|
||||||
|
{ title: 'Prossimo film al cinema', description: 'Quale film vedremo?', stake: 'Offre i biglietti', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 3, role: 'PARTICIPANT' }], status: 'AWAITING_VALIDATION', startDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), isPublic: false, groupId: group1.id.toString() },
|
||||||
|
{ title: 'Riuscirò a finire il report?', description: 'Entro la deadline di domani!', stake: 'Un caffè', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 4, role: 'OBSERVER' }], status: 'ACTIVE', startDate: new Date(), endDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), isPublic: true },
|
||||||
|
{ title: 'Scommessa passata', description: 'Chi ha mangiato più sushi', stake: '€20', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 2, role: 'PARTICIPANT' }], status: 'COMPLETED', startDate: new Date(Date.now() - 12 * 24 * 60 * 60 * 1000), endDate: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), isPublic: false, groupId: group1.id.toString(), winner: 2 },
|
||||||
|
{ title: 'Scommessa pubblica', description: 'Quanti goal segnerà la nazionale?', stake: 'Gloria eterna', participants: [{ user: 5, role: 'PARTICIPANT' }, { user: 6, role: 'PARTICIPANT' }], status: 'ACTIVE', startDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), endDate: null, isPublic: true },
|
||||||
|
];
|
||||||
|
await Bet.insertMany(betsData);
|
||||||
|
|
||||||
|
console.log('Database seeded successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error seeding database:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { seedDatabase };
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const cors = require('cors');
|
||||||
|
const config = require('./config');
|
||||||
|
const setupContainer = require('./di/container');
|
||||||
|
const errorHandler = require('./middleware/errorHandler');
|
||||||
|
const mockAuthMiddleware = require('./middleware/auth.mock');
|
||||||
|
const { seedDatabase } = require('./seed');
|
||||||
|
|
||||||
|
const legacyApiRoutes = require('./routes/api');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const container = setupContainer();
|
||||||
|
|
||||||
|
const sequelize = container.resolve('sequelize');
|
||||||
|
|
||||||
|
mongoose
|
||||||
|
.connect(config.database.mongo.uri, config.database.mongo.options)
|
||||||
|
.then(() => console.log('Connected to MongoDB'))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('MongoDB connection error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/api', mockAuthMiddleware(sequelize));
|
||||||
|
|
||||||
|
app.use('/api/legacy', legacyApiRoutes(sequelize));
|
||||||
|
|
||||||
|
const v1Routes = require('./routes/v1');
|
||||||
|
app.use('/api/v1', v1Routes(container));
|
||||||
|
app.use('/api', v1Routes(container));
|
||||||
|
|
||||||
|
const healthRoutes = require('./routes/health.routes');
|
||||||
|
app.use('/', healthRoutes({ sequelize, mongoose }));
|
||||||
|
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
sequelize
|
||||||
|
.authenticate()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Connected to PostgreSQL');
|
||||||
|
return sequelize.sync({ force: true });
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('PostgreSQL models synchronized');
|
||||||
|
return seedDatabase(sequelize);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
app.listen(config.port, () => {
|
||||||
|
console.log(`Server is running on http://localhost:${config.port}`);
|
||||||
|
console.log(`Environment: ${config.nodeEnv}`);
|
||||||
|
console.log(`API v1 endpoints available at /api/v1/`);
|
||||||
|
console.log(`Legacy endpoints available at /api/legacy/`);
|
||||||
|
console.log(`Health check available at /health`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('PostgreSQL connection or sync error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
const { ValidationError } = require('../shared/errors');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
class BetsService {
|
||||||
|
constructor({ betsRepository, usersRepository, dataStitchingService }) {
|
||||||
|
this.betsRepository = betsRepository;
|
||||||
|
this.usersRepository = usersRepository;
|
||||||
|
this.dataStitchingService = dataStitchingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllBets() {
|
||||||
|
const bets = await this.betsRepository.findAll();
|
||||||
|
return await this.dataStitchingService.populateBetsWithUsers(bets);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBetById(betId) {
|
||||||
|
const bet = await this.betsRepository.findById(betId);
|
||||||
|
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
|
||||||
|
return populated[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserBets(userId) {
|
||||||
|
const bets = await this.betsRepository.findByUser(userId);
|
||||||
|
return await this.dataStitchingService.populateBetsWithUsers(bets);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupBets(groupId) {
|
||||||
|
const bets = await this.betsRepository.findByGroup(groupId);
|
||||||
|
return await this.dataStitchingService.populateBetsWithUsers(bets);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicBetsExcludingUser(userId) {
|
||||||
|
const bets = await this.betsRepository.findPublicBetsExcludingUser(userId);
|
||||||
|
return await this.dataStitchingService.populateBetsWithUsers(bets);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBet(betData) {
|
||||||
|
const { title, description, stake, participants, isPublic, groupId } = betData;
|
||||||
|
|
||||||
|
if (!title || title.trim() === '') {
|
||||||
|
throw new ValidationError('Bet title is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stake || stake.trim() === '') {
|
||||||
|
throw new ValidationError('Bet stake is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!participants || !Array.isArray(participants) || participants.length === 0) {
|
||||||
|
throw new ValidationError('At least one participant is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedParticipants = participants.map((p) => ({
|
||||||
|
...p,
|
||||||
|
user: parseInt(p.user, 10),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const userIds = normalizedParticipants.map((p) => p.user);
|
||||||
|
for (const userId of userIds) {
|
||||||
|
await this.usersRepository.findById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bet = await this.betsRepository.create({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
stake,
|
||||||
|
participants: normalizedParticipants,
|
||||||
|
isPublic: isPublic || false,
|
||||||
|
groupId: groupId || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
|
||||||
|
return populated[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBet(betId, updateData) {
|
||||||
|
if (updateData.title && updateData.title.trim() === '') {
|
||||||
|
throw new ValidationError('Bet title cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bet = await this.betsRepository.update(betId, updateData);
|
||||||
|
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
|
||||||
|
return populated[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBet(betId) {
|
||||||
|
return await this.betsRepository.delete(betId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async declareWinner(betId, winnerId) {
|
||||||
|
await this.usersRepository.findById(winnerId);
|
||||||
|
|
||||||
|
const bet = await this.betsRepository.update(betId, {
|
||||||
|
winner: winnerId,
|
||||||
|
status: config.BET_STATUS.COMPLETED,
|
||||||
|
endDate: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
|
||||||
|
return populated[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BetsService;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
class DataStitchingService {
|
||||||
|
constructor({ usersRepository }) {
|
||||||
|
this.usersRepository = usersRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async populateBetsWithUsers(bets) {
|
||||||
|
if (!bets || bets.length === 0) return [];
|
||||||
|
|
||||||
|
const userIds = new Set();
|
||||||
|
bets.forEach((bet) => {
|
||||||
|
bet.participants.forEach((p) => userIds.add(p.user));
|
||||||
|
if (bet.winner) {
|
||||||
|
userIds.add(bet.winner);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userIds.size === 0)
|
||||||
|
return bets.map((b) => (typeof b.toJSON === 'function' ? b.toJSON() : b));
|
||||||
|
|
||||||
|
const users = await this.usersRepository.findAll({
|
||||||
|
where: { id: Array.from(userIds) },
|
||||||
|
});
|
||||||
|
const userMap = new Map(
|
||||||
|
users.map((u) => [u.id, typeof u.toJSON === 'function' ? u.toJSON() : u])
|
||||||
|
);
|
||||||
|
|
||||||
|
return bets.map((bet) => {
|
||||||
|
const betJson = typeof bet.toJSON === 'function' ? bet.toJSON() : bet;
|
||||||
|
betJson.participants = betJson.participants.map((p) => ({
|
||||||
|
...p,
|
||||||
|
user: userMap.get(p.user) || null,
|
||||||
|
}));
|
||||||
|
if (betJson.winner) {
|
||||||
|
betJson.winner = userMap.get(betJson.winner) || null;
|
||||||
|
}
|
||||||
|
return betJson;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DataStitchingService;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
const { ValidationError, ConflictError } = require('../shared/errors');
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
class FriendsService {
|
||||||
|
constructor({
|
||||||
|
friendshipsRepository,
|
||||||
|
friendRequestsRepository,
|
||||||
|
usersRepository,
|
||||||
|
sequelize,
|
||||||
|
}) {
|
||||||
|
this.friendshipsRepository = friendshipsRepository;
|
||||||
|
this.friendRequestsRepository = friendRequestsRepository;
|
||||||
|
this.usersRepository = usersRepository;
|
||||||
|
this.sequelize = sequelize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFriendsByUserId(userId) {
|
||||||
|
return await this.friendshipsRepository.findByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPendingRequests(userId) {
|
||||||
|
return await this.friendRequestsRepository.findPendingByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendFriendRequest(fromUserId, identifier) {
|
||||||
|
let toUser;
|
||||||
|
|
||||||
|
if (typeof identifier === 'number' || !isNaN(identifier)) {
|
||||||
|
toUser = await this.usersRepository.findById(parseInt(identifier, 10));
|
||||||
|
} else {
|
||||||
|
toUser = await this.usersRepository.findOne({
|
||||||
|
[Op.or]: [{ name: identifier }, { email: identifier }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!toUser) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'User not found with provided name or email'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toUser.id === fromUserId) {
|
||||||
|
throw new ValidationError('Cannot send friend request to yourself');
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendshipExists = await this.friendshipsRepository.exists(
|
||||||
|
fromUserId,
|
||||||
|
toUser.id
|
||||||
|
);
|
||||||
|
if (friendshipExists) {
|
||||||
|
throw new ConflictError('You are already friends with this user');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRequest = await this.friendRequestsRepository.findExisting(
|
||||||
|
fromUserId,
|
||||||
|
toUser.id
|
||||||
|
);
|
||||||
|
if (existingRequest) {
|
||||||
|
throw new ConflictError('Friend request already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.friendRequestsRepository.create(fromUserId, toUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptFriendRequest(requestId, userId) {
|
||||||
|
const request = await this.friendRequestsRepository.findById(requestId);
|
||||||
|
|
||||||
|
if (request.toUserId !== userId) {
|
||||||
|
throw new ValidationError('You can only accept requests sent to you');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.status !== 'PENDING') {
|
||||||
|
throw new ValidationError('This request has already been processed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = await this.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
await this.friendRequestsRepository.updateStatus(requestId, 'ACCEPTED');
|
||||||
|
await this.friendshipsRepository.create(request.fromUserId, request.toUserId);
|
||||||
|
await this.friendRequestsRepository.delete(requestId);
|
||||||
|
await t.commit();
|
||||||
|
|
||||||
|
return { message: 'Friend request accepted successfully' };
|
||||||
|
} catch (error) {
|
||||||
|
await t.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectFriendRequest(requestId, userId) {
|
||||||
|
const request = await this.friendRequestsRepository.findById(requestId);
|
||||||
|
|
||||||
|
if (request.toUserId !== userId && request.fromUserId !== userId) {
|
||||||
|
throw new ValidationError('You can only manage your own requests');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.friendRequestsRepository.delete(requestId);
|
||||||
|
return { message: 'Friend request rejected successfully' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FriendsService;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
const { ValidationError } = require('../shared/errors');
|
||||||
|
|
||||||
|
class GroupsService {
|
||||||
|
constructor({ groupsRepository, usersRepository }) {
|
||||||
|
this.groupsRepository = groupsRepository;
|
||||||
|
this.usersRepository = usersRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllGroups() {
|
||||||
|
return await this.groupsRepository.findAll({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: this.usersRepository.User,
|
||||||
|
as: 'members',
|
||||||
|
through: { attributes: [] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupById(groupId) {
|
||||||
|
return await this.groupsRepository.findById(groupId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createGroup(groupData) {
|
||||||
|
const { name, members } = groupData;
|
||||||
|
|
||||||
|
if (!name || name.trim() === '') {
|
||||||
|
throw new ValidationError('Group name is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!members || !Array.isArray(members) || members.length === 0) {
|
||||||
|
throw new ValidationError('At least one member is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const userId of members) {
|
||||||
|
await this.usersRepository.findById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = await this.groupsRepository.create({ name });
|
||||||
|
return await this.groupsRepository.addMembers(group.id, members);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGroup(groupId, updateData) {
|
||||||
|
const { name } = updateData;
|
||||||
|
|
||||||
|
if (name && name.trim() === '') {
|
||||||
|
throw new ValidationError('Group name cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.groupsRepository.update(groupId, { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGroup(groupId) {
|
||||||
|
return await this.groupsRepository.delete(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMembersToGroup(groupId, userIds) {
|
||||||
|
if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
|
||||||
|
throw new ValidationError('User IDs are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const userId of userIds) {
|
||||||
|
await this.usersRepository.findById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.groupsRepository.addMembers(groupId, userIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeMemberFromGroup(groupId, userId) {
|
||||||
|
await this.usersRepository.findById(userId);
|
||||||
|
return await this.groupsRepository.removeMember(groupId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GroupsService;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const { ValidationError } = require('../shared/errors');
|
||||||
|
|
||||||
|
class UsersService {
|
||||||
|
constructor({ usersRepository }) {
|
||||||
|
this.usersRepository = usersRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllUsers() {
|
||||||
|
return await this.usersRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserById(id) {
|
||||||
|
return await this.usersRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserProfile(id, updateData) {
|
||||||
|
const { name, email, avatarUrl } = updateData;
|
||||||
|
|
||||||
|
if (name && name.trim() === '') {
|
||||||
|
throw new ValidationError('Name cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email && email.trim() === '') {
|
||||||
|
throw new ValidationError('Email cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataToUpdate = {};
|
||||||
|
if (name !== undefined) dataToUpdate.name = name;
|
||||||
|
if (email !== undefined) dataToUpdate.email = email;
|
||||||
|
if (avatarUrl !== undefined) dataToUpdate.avatarUrl = avatarUrl;
|
||||||
|
|
||||||
|
return await this.usersRepository.update(id, dataToUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UsersService;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class AppError extends Error {
|
||||||
|
constructor(message, statusCode, isOperational = true, errors = null) {
|
||||||
|
super(message);
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.isOperational = isOperational;
|
||||||
|
this.errors = errors;
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AppError;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
const AppError = require('./AppError');
|
||||||
|
const config = require('../../config');
|
||||||
|
|
||||||
|
class ValidationError extends AppError {
|
||||||
|
constructor(message, errors = null) {
|
||||||
|
super(message, config.HTTP_STATUS.BAD_REQUEST, true, errors);
|
||||||
|
this.name = 'ValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotFoundError extends AppError {
|
||||||
|
constructor(message = 'Resource not found') {
|
||||||
|
super(message, config.HTTP_STATUS.NOT_FOUND, true);
|
||||||
|
this.name = 'NotFoundError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConflictError extends AppError {
|
||||||
|
constructor(message, errors = null) {
|
||||||
|
super(message, config.HTTP_STATUS.CONFLICT, true, errors);
|
||||||
|
this.name = 'ConflictError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnauthorizedError extends AppError {
|
||||||
|
constructor(message = 'Unauthorized') {
|
||||||
|
super(message, config.HTTP_STATUS.UNAUTHORIZED, true);
|
||||||
|
this.name = 'UnauthorizedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ForbiddenError extends AppError {
|
||||||
|
constructor(message = 'Forbidden') {
|
||||||
|
super(message, config.HTTP_STATUS.FORBIDDEN, true);
|
||||||
|
this.name = 'ForbiddenError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseError extends AppError {
|
||||||
|
constructor(message = 'Database operation failed', originalError = null) {
|
||||||
|
super(message, config.HTTP_STATUS.INTERNAL_SERVER_ERROR, false);
|
||||||
|
this.name = 'DatabaseError';
|
||||||
|
this.originalError = originalError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
AppError,
|
||||||
|
ValidationError,
|
||||||
|
NotFoundError,
|
||||||
|
ConflictError,
|
||||||
|
UnauthorizedError,
|
||||||
|
ForbiddenError,
|
||||||
|
DatabaseError,
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
class ApiResponse {
|
||||||
|
constructor(success, statusCode, data = null, message = null, errors = null) {
|
||||||
|
this.success = success;
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.data = data;
|
||||||
|
this.message = message;
|
||||||
|
this.errors = errors;
|
||||||
|
this.timestamp = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
const response = {
|
||||||
|
success: this.success,
|
||||||
|
timestamp: this.timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.message) response.message = this.message;
|
||||||
|
if (this.data !== null) response.data = this.data;
|
||||||
|
if (this.errors) response.errors = this.errors;
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResponseHandler {
|
||||||
|
static success(res, data, message = 'Success', statusCode = config.HTTP_STATUS.OK) {
|
||||||
|
const response = new ApiResponse(true, statusCode, data, message);
|
||||||
|
return res.status(statusCode).json(response.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
static created(res, data, message = 'Resource created successfully') {
|
||||||
|
return this.success(res, data, message, config.HTTP_STATUS.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
static error(
|
||||||
|
res,
|
||||||
|
message,
|
||||||
|
statusCode = config.HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
||||||
|
errors = null
|
||||||
|
) {
|
||||||
|
const response = new ApiResponse(false, statusCode, null, message, errors);
|
||||||
|
return res.status(statusCode).json(response.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
static badRequest(res, message = 'Bad request', errors = null) {
|
||||||
|
return this.error(res, message, config.HTTP_STATUS.BAD_REQUEST, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
static notFound(res, message = 'Resource not found') {
|
||||||
|
return this.error(res, message, config.HTTP_STATUS.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
static unauthorized(res, message = 'Unauthorized') {
|
||||||
|
return this.error(res, message, config.HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
static forbidden(res, message = 'Forbidden') {
|
||||||
|
return this.error(res, message, config.HTTP_STATUS.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static conflict(res, message = 'Resource conflict', errors = null) {
|
||||||
|
return this.error(res, message, config.HTTP_STATUS.CONFLICT, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
static internalError(res, message = 'Internal server error', error = null) {
|
||||||
|
if (error && config.isDevelopment) {
|
||||||
|
console.error('Internal Error:', error);
|
||||||
|
}
|
||||||
|
return this.error(res, message, config.HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ResponseHandler;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
const Joi = require('joi');
|
||||||
|
const config = require('../../../config');
|
||||||
|
|
||||||
|
const participantSchema = Joi.object({
|
||||||
|
user: Joi.number().integer().positive().required(),
|
||||||
|
role: Joi.string()
|
||||||
|
.valid(...Object.values(config.PARTICIPANT_ROLES))
|
||||||
|
.required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createBetSchema = Joi.object({
|
||||||
|
title: Joi.string().trim().min(1).max(200).required().messages({
|
||||||
|
'string.empty': 'Bet title is required',
|
||||||
|
'string.max': 'Bet title must not exceed 200 characters',
|
||||||
|
}),
|
||||||
|
description: Joi.string().trim().max(1000).optional().allow(''),
|
||||||
|
stake: Joi.string().trim().min(1).required().messages({
|
||||||
|
'string.empty': 'Bet stake is required',
|
||||||
|
}),
|
||||||
|
participants: Joi.array().items(participantSchema).min(1).required().messages({
|
||||||
|
'array.min': 'At least one participant is required',
|
||||||
|
}),
|
||||||
|
isPublic: Joi.boolean().optional().default(false),
|
||||||
|
groupId: Joi.string().optional().allow(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateBetSchema = Joi.object({
|
||||||
|
title: Joi.string().trim().min(1).max(200).optional(),
|
||||||
|
description: Joi.string().trim().max(1000).optional().allow(''),
|
||||||
|
stake: Joi.string().trim().min(1).optional(),
|
||||||
|
status: Joi.string()
|
||||||
|
.valid(...Object.values(config.BET_STATUS))
|
||||||
|
.optional(),
|
||||||
|
endDate: Joi.date().optional(),
|
||||||
|
winner: Joi.number().integer().positive().optional().allow(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
const declareWinnerSchema = Joi.object({
|
||||||
|
winnerId: Joi.number().integer().positive().required().messages({
|
||||||
|
'number.base': 'Winner ID must be a number',
|
||||||
|
'any.required': 'Winner ID is required',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createBetSchema,
|
||||||
|
updateBetSchema,
|
||||||
|
declareWinnerSchema,
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
const sendFriendRequestSchema = Joi.object({
|
||||||
|
identifier: Joi.alternatives().try(Joi.number().integer().positive(), Joi.string().min(1)).required().messages({
|
||||||
|
'any.required': 'User identifier (ID, name, or email) is required',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendFriendRequestSchema,
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
const createGroupSchema = Joi.object({
|
||||||
|
name: Joi.string().trim().min(1).max(100).required().messages({
|
||||||
|
'string.empty': 'Group name is required',
|
||||||
|
'string.min': 'Group name must be at least 1 character',
|
||||||
|
'string.max': 'Group name must not exceed 100 characters',
|
||||||
|
}),
|
||||||
|
members: Joi.array().items(Joi.number().integer().positive()).min(1).required().messages({
|
||||||
|
'array.min': 'At least one member is required',
|
||||||
|
'array.base': 'Members must be an array',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateGroupSchema = Joi.object({
|
||||||
|
name: Joi.string().trim().min(1).max(100).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const addMembersSchema = Joi.object({
|
||||||
|
userIds: Joi.array().items(Joi.number().integer().positive()).min(1).required().messages({
|
||||||
|
'array.min': 'At least one user ID is required',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createGroupSchema,
|
||||||
|
updateGroupSchema,
|
||||||
|
addMembersSchema,
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
const updateUserSchema = Joi.object({
|
||||||
|
name: Joi.string().trim().min(1).max(100).optional(),
|
||||||
|
email: Joi.string().email().optional(),
|
||||||
|
avatarUrl: Joi.string().uri().optional().allow('', null),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
updateUserSchema,
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Questo helper necessita del modello User da Sequelize
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const { User } = sequelize.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funzione helper per arricchire i documenti delle scommesse da MongoDB con i dati utente da PostgreSQL.
|
||||||
|
* @param {Array} bets - Un array di documenti di scommesse Mongoose.
|
||||||
|
* @returns {Array} Un array di oggetti scommessa con i dettagli utente popolati.
|
||||||
|
*/
|
||||||
|
const populateBetsWithUsers = async (bets) => {
|
||||||
|
if (!bets || bets.length === 0) return [];
|
||||||
|
|
||||||
|
// Raccoglie tutti gli ID utente unici da tutte le scommesse
|
||||||
|
const userIds = new Set();
|
||||||
|
bets.forEach(bet => {
|
||||||
|
bet.participants.forEach(p => userIds.add(p.user));
|
||||||
|
if (bet.winner) {
|
||||||
|
userIds.add(bet.winner);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userIds.size === 0) return bets.map(b => b.toJSON());
|
||||||
|
|
||||||
|
// Recupera tutti gli utenti necessari da PostgreSQL con una singola query
|
||||||
|
const users = await User.findAll({ where: { id: Array.from(userIds) } });
|
||||||
|
const userMap = new Map(users.map(u => [u.id, u.toJSON()]));
|
||||||
|
|
||||||
|
// Unisce i dati utente negli oggetti scommessa
|
||||||
|
return bets.map(bet => {
|
||||||
|
const betJson = bet.toJSON();
|
||||||
|
betJson.participants = betJson.participants.map(p => ({
|
||||||
|
...p,
|
||||||
|
user: userMap.get(p.user) || null
|
||||||
|
}));
|
||||||
|
if (betJson.winner) {
|
||||||
|
betJson.winner = userMap.get(betJson.winner) || null;
|
||||||
|
}
|
||||||
|
return betJson;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { populateBetsWithUsers };
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: scommetti-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: amici_scommesse
|
||||||
|
POSTGRES_USER: admin
|
||||||
|
POSTGRES_PASSWORD: admin
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./backend-node/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- scommetti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U admin -d amici_scommesse"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# MongoDB Database
|
||||||
|
mongodb:
|
||||||
|
image: mongo:7
|
||||||
|
container_name: scommetti-mongodb
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: root
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: root@servovio
|
||||||
|
MONGO_INITDB_DATABASE: amici-scommesse
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
networks:
|
||||||
|
- scommetti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Backend Node.js
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend-node
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: scommetti-backend
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
PORT: 3000
|
||||||
|
POSTGRES_URI: postgres://admin:admin@postgres:5432/amici_scommesse
|
||||||
|
MONGO_URI: mongodb://mongodb:27017/amici-scommesse
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./backend-node:/app
|
||||||
|
- /app/node_modules
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
mongodb:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- scommetti-network
|
||||||
|
command: npm start
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Frontend Angular
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend-angular
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: scommetti-frontend
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
ports:
|
||||||
|
- "4200:4200"
|
||||||
|
volumes:
|
||||||
|
- ./frontend-angular:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/.angular
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- scommetti-network
|
||||||
|
command: npm run dev
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
mongodb_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
scommetti-network:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
{
|
||||||
|
"configVersion": 2,
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "_fe_analyzer_shared",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-91.0.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "analyzer",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/analyzer-8.4.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "args",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/args-2.7.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "async",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/async-2.13.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bloc",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/bloc-8.1.4",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bloc_test",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/bloc_test-9.1.7",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "boolean_selector",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "characters",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/characters-1.4.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cli_config",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/cli_config-0.2.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/clock-1.1.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "collection",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/collection-1.19.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "convert",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/convert-3.1.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "coverage",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/coverage-1.15.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "crypto",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/crypto-3.0.7",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cupertino_icons",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/cupertino_icons-1.0.8",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "diff_match_patch",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/diff_match_patch-0.4.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equatable",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/equatable-2.0.8",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fake_async",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "file",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/file-7.0.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter",
|
||||||
|
"rootUri": "file:///root/development/flutter/packages/flutter",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_bloc",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/flutter_bloc-8.1.6",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_lints",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/flutter_lints-4.0.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_test",
|
||||||
|
"rootUri": "file:///root/development/flutter/packages/flutter_test",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_web_plugins",
|
||||||
|
"rootUri": "file:///root/development/flutter/packages/flutter_web_plugins",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_server_client",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "glob",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/glob-2.1.3",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "go_router",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/go_router-14.8.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "http",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/http-1.6.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "http_multi_server",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "http_parser",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/http_parser-4.1.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "intl",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/intl-0.19.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "io",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/io-1.0.5",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "js",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/js-0.7.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "leak_tracker",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "leak_tracker_flutter_testing",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "leak_tracker_testing",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lints",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/lints-4.0.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "logging",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/logging-1.3.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "matcher",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/matcher-0.12.17",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "material_color_utilities",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/meta-1.17.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mime",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/mime-2.0.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mocktail",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/mocktail-1.0.4",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nested",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/nested-1.0.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "node_preamble",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/node_preamble-2.0.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "package_config",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/package_config-2.2.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "path",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/path-1.9.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pool",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/pool-1.5.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/provider-6.1.5+1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pub_semver",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/pub_semver-2.2.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf-1.4.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf_packages_handler",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "2.17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf_static",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf_static-1.1.3",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf_web_socket",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sky_engine",
|
||||||
|
"rootUri": "file:///root/development/flutter/bin/cache/pkg/sky_engine",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_map_stack_trace",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_maps",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/source_maps-0.10.13",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_span",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/source_span-1.10.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stack_trace",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/stack_trace-1.12.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_channel",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/stream_channel-2.1.4",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "string_scanner",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/string_scanner-1.4.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "term_glyph",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/term_glyph-1.2.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/test-1.26.3",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test_api",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/test_api-0.7.7",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test_core",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/test_core-0.6.12",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "typed_data",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/typed_data-1.4.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vector_math",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/vector_math-2.2.0",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vm_service",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/vm_service-15.0.2",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "watcher",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/watcher-1.2.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/web-1.1.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_socket",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/web_socket-1.0.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_socket_channel",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webkit_inspection_protocol",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.2.1",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yaml",
|
||||||
|
"rootUri": "file:///root/.pub-cache/hosted/pub.dev/yaml-3.1.3",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "scomettiapp",
|
||||||
|
"rootUri": "../",
|
||||||
|
"packageUri": "lib/",
|
||||||
|
"languageVersion": "3.5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generator": "pub",
|
||||||
|
"generatorVersion": "3.10.8",
|
||||||
|
"flutterRoot": "file:///root/development/flutter",
|
||||||
|
"flutterVersion": "3.38.9",
|
||||||
|
"pubCache": "file:///root/.pub-cache"
|
||||||
|
}
|
||||||
@@ -0,0 +1,674 @@
|
|||||||
|
{
|
||||||
|
"roots": [
|
||||||
|
"scomettiapp"
|
||||||
|
],
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "scomettiapp",
|
||||||
|
"version": "1.0.0+1",
|
||||||
|
"dependencies": [
|
||||||
|
"bloc",
|
||||||
|
"cupertino_icons",
|
||||||
|
"equatable",
|
||||||
|
"flutter",
|
||||||
|
"flutter_bloc",
|
||||||
|
"go_router",
|
||||||
|
"http",
|
||||||
|
"intl"
|
||||||
|
],
|
||||||
|
"devDependencies": [
|
||||||
|
"bloc_test",
|
||||||
|
"flutter_lints",
|
||||||
|
"flutter_test",
|
||||||
|
"mocktail"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mocktail",
|
||||||
|
"version": "1.0.4",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"matcher",
|
||||||
|
"test_api"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bloc_test",
|
||||||
|
"version": "9.1.7",
|
||||||
|
"dependencies": [
|
||||||
|
"bloc",
|
||||||
|
"diff_match_patch",
|
||||||
|
"meta",
|
||||||
|
"mocktail",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_lints",
|
||||||
|
"version": "4.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"lints"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_test",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"clock",
|
||||||
|
"collection",
|
||||||
|
"fake_async",
|
||||||
|
"flutter",
|
||||||
|
"leak_tracker_flutter_testing",
|
||||||
|
"matcher",
|
||||||
|
"meta",
|
||||||
|
"path",
|
||||||
|
"stack_trace",
|
||||||
|
"stream_channel",
|
||||||
|
"test_api",
|
||||||
|
"vector_math"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "intl",
|
||||||
|
"version": "0.19.0",
|
||||||
|
"dependencies": [
|
||||||
|
"clock",
|
||||||
|
"meta",
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cupertino_icons",
|
||||||
|
"version": "1.0.8",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bloc",
|
||||||
|
"version": "8.1.4",
|
||||||
|
"dependencies": [
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_bloc",
|
||||||
|
"version": "8.1.6",
|
||||||
|
"dependencies": [
|
||||||
|
"bloc",
|
||||||
|
"flutter",
|
||||||
|
"provider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"characters",
|
||||||
|
"collection",
|
||||||
|
"material_color_utilities",
|
||||||
|
"meta",
|
||||||
|
"sky_engine",
|
||||||
|
"vector_math"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test_api",
|
||||||
|
"version": "0.7.7",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"boolean_selector",
|
||||||
|
"collection",
|
||||||
|
"meta",
|
||||||
|
"source_span",
|
||||||
|
"stack_trace",
|
||||||
|
"stream_channel",
|
||||||
|
"string_scanner",
|
||||||
|
"term_glyph"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "matcher",
|
||||||
|
"version": "0.12.17",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"meta",
|
||||||
|
"stack_trace",
|
||||||
|
"term_glyph",
|
||||||
|
"test_api"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "collection",
|
||||||
|
"version": "1.19.1",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta",
|
||||||
|
"version": "1.17.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "diff_match_patch",
|
||||||
|
"version": "0.4.1",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lints",
|
||||||
|
"version": "4.0.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vector_math",
|
||||||
|
"version": "2.2.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_channel",
|
||||||
|
"version": "2.1.4",
|
||||||
|
"dependencies": [
|
||||||
|
"async"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stack_trace",
|
||||||
|
"version": "1.12.1",
|
||||||
|
"dependencies": [
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "path",
|
||||||
|
"version": "1.9.1",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "leak_tracker_flutter_testing",
|
||||||
|
"version": "3.0.10",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"leak_tracker",
|
||||||
|
"leak_tracker_testing",
|
||||||
|
"matcher",
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fake_async",
|
||||||
|
"version": "1.3.3",
|
||||||
|
"dependencies": [
|
||||||
|
"clock",
|
||||||
|
"collection"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"version": "1.1.2",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sky_engine",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "material_color_utilities",
|
||||||
|
"version": "0.11.1",
|
||||||
|
"dependencies": [
|
||||||
|
"collection"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "characters",
|
||||||
|
"version": "1.4.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "leak_tracker_testing",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"dependencies": [
|
||||||
|
"leak_tracker",
|
||||||
|
"matcher",
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "leak_tracker",
|
||||||
|
"version": "11.0.2",
|
||||||
|
"dependencies": [
|
||||||
|
"clock",
|
||||||
|
"collection",
|
||||||
|
"meta",
|
||||||
|
"path",
|
||||||
|
"vm_service"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "term_glyph",
|
||||||
|
"version": "1.2.2",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "boolean_selector",
|
||||||
|
"version": "2.1.2",
|
||||||
|
"dependencies": [
|
||||||
|
"source_span",
|
||||||
|
"string_scanner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equatable",
|
||||||
|
"version": "2.0.8",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "async",
|
||||||
|
"version": "2.13.0",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "string_scanner",
|
||||||
|
"version": "1.4.1",
|
||||||
|
"dependencies": [
|
||||||
|
"source_span"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "go_router",
|
||||||
|
"version": "14.8.1",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"flutter",
|
||||||
|
"flutter_web_plugins",
|
||||||
|
"logging",
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flutter_web_plugins",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "http",
|
||||||
|
"version": "1.6.0",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"http_parser",
|
||||||
|
"meta",
|
||||||
|
"web"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "http_parser",
|
||||||
|
"version": "4.1.2",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"source_span",
|
||||||
|
"string_scanner",
|
||||||
|
"typed_data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "typed_data",
|
||||||
|
"version": "1.4.0",
|
||||||
|
"dependencies": [
|
||||||
|
"collection"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_span",
|
||||||
|
"version": "1.10.1",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"path",
|
||||||
|
"term_glyph"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "logging",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider",
|
||||||
|
"version": "6.1.5+1",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"flutter",
|
||||||
|
"nested"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nested",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vm_service",
|
||||||
|
"version": "15.0.2",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pool",
|
||||||
|
"version": "1.5.2",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"stack_trace"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"version": "1.26.3",
|
||||||
|
"dependencies": [
|
||||||
|
"analyzer",
|
||||||
|
"async",
|
||||||
|
"boolean_selector",
|
||||||
|
"collection",
|
||||||
|
"coverage",
|
||||||
|
"http_multi_server",
|
||||||
|
"io",
|
||||||
|
"js",
|
||||||
|
"matcher",
|
||||||
|
"node_preamble",
|
||||||
|
"package_config",
|
||||||
|
"path",
|
||||||
|
"pool",
|
||||||
|
"shelf",
|
||||||
|
"shelf_packages_handler",
|
||||||
|
"shelf_static",
|
||||||
|
"shelf_web_socket",
|
||||||
|
"source_span",
|
||||||
|
"stack_trace",
|
||||||
|
"stream_channel",
|
||||||
|
"test_api",
|
||||||
|
"test_core",
|
||||||
|
"typed_data",
|
||||||
|
"web_socket_channel",
|
||||||
|
"webkit_inspection_protocol",
|
||||||
|
"yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test_core",
|
||||||
|
"version": "0.6.12",
|
||||||
|
"dependencies": [
|
||||||
|
"analyzer",
|
||||||
|
"args",
|
||||||
|
"async",
|
||||||
|
"boolean_selector",
|
||||||
|
"collection",
|
||||||
|
"coverage",
|
||||||
|
"frontend_server_client",
|
||||||
|
"glob",
|
||||||
|
"io",
|
||||||
|
"meta",
|
||||||
|
"package_config",
|
||||||
|
"path",
|
||||||
|
"pool",
|
||||||
|
"source_map_stack_trace",
|
||||||
|
"source_maps",
|
||||||
|
"source_span",
|
||||||
|
"stack_trace",
|
||||||
|
"stream_channel",
|
||||||
|
"test_api",
|
||||||
|
"vm_service",
|
||||||
|
"yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_server_client",
|
||||||
|
"version": "4.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf_packages_handler",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"dependencies": [
|
||||||
|
"path",
|
||||||
|
"shelf",
|
||||||
|
"shelf_static"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "node_preamble",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_map_stack_trace",
|
||||||
|
"version": "2.1.2",
|
||||||
|
"dependencies": [
|
||||||
|
"path",
|
||||||
|
"source_maps",
|
||||||
|
"stack_trace"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_maps",
|
||||||
|
"version": "0.10.13",
|
||||||
|
"dependencies": [
|
||||||
|
"source_span"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yaml",
|
||||||
|
"version": "3.1.3",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"source_span",
|
||||||
|
"string_scanner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webkit_inspection_protocol",
|
||||||
|
"version": "1.2.1",
|
||||||
|
"dependencies": [
|
||||||
|
"logging"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf_static",
|
||||||
|
"version": "1.1.3",
|
||||||
|
"dependencies": [
|
||||||
|
"convert",
|
||||||
|
"http_parser",
|
||||||
|
"mime",
|
||||||
|
"path",
|
||||||
|
"shelf"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "package_config",
|
||||||
|
"version": "2.2.0",
|
||||||
|
"dependencies": [
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "io",
|
||||||
|
"version": "1.0.5",
|
||||||
|
"dependencies": [
|
||||||
|
"meta",
|
||||||
|
"path",
|
||||||
|
"string_scanner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "http_multi_server",
|
||||||
|
"version": "3.2.2",
|
||||||
|
"dependencies": [
|
||||||
|
"async"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "convert",
|
||||||
|
"version": "3.1.2",
|
||||||
|
"dependencies": [
|
||||||
|
"typed_data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "js",
|
||||||
|
"version": "0.7.2",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "glob",
|
||||||
|
"version": "2.1.3",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"collection",
|
||||||
|
"file",
|
||||||
|
"path",
|
||||||
|
"string_scanner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "file",
|
||||||
|
"version": "7.0.1",
|
||||||
|
"dependencies": [
|
||||||
|
"meta",
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf_web_socket",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"shelf",
|
||||||
|
"stream_channel",
|
||||||
|
"web_socket_channel"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shelf",
|
||||||
|
"version": "1.4.2",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"collection",
|
||||||
|
"http_parser",
|
||||||
|
"path",
|
||||||
|
"stack_trace",
|
||||||
|
"stream_channel"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_socket_channel",
|
||||||
|
"version": "3.0.3",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"crypto",
|
||||||
|
"stream_channel",
|
||||||
|
"web",
|
||||||
|
"web_socket"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_socket",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"dependencies": [
|
||||||
|
"web"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "crypto",
|
||||||
|
"version": "3.0.7",
|
||||||
|
"dependencies": [
|
||||||
|
"typed_data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "args",
|
||||||
|
"version": "2.7.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "coverage",
|
||||||
|
"version": "1.15.0",
|
||||||
|
"dependencies": [
|
||||||
|
"args",
|
||||||
|
"cli_config",
|
||||||
|
"glob",
|
||||||
|
"logging",
|
||||||
|
"meta",
|
||||||
|
"package_config",
|
||||||
|
"path",
|
||||||
|
"source_maps",
|
||||||
|
"stack_trace",
|
||||||
|
"vm_service",
|
||||||
|
"yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cli_config",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"dependencies": [
|
||||||
|
"args",
|
||||||
|
"yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "analyzer",
|
||||||
|
"version": "8.4.1",
|
||||||
|
"dependencies": [
|
||||||
|
"_fe_analyzer_shared",
|
||||||
|
"collection",
|
||||||
|
"convert",
|
||||||
|
"crypto",
|
||||||
|
"glob",
|
||||||
|
"meta",
|
||||||
|
"package_config",
|
||||||
|
"path",
|
||||||
|
"pub_semver",
|
||||||
|
"source_span",
|
||||||
|
"watcher",
|
||||||
|
"yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_fe_analyzer_shared",
|
||||||
|
"version": "91.0.0",
|
||||||
|
"dependencies": [
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pub_semver",
|
||||||
|
"version": "2.2.0",
|
||||||
|
"dependencies": [
|
||||||
|
"collection"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "watcher",
|
||||||
|
"version": "1.2.1",
|
||||||
|
"dependencies": [
|
||||||
|
"async",
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configVersion": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.38.9
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
/// Exception thrown when an API request fails
|
||||||
|
class ApiException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final int? statusCode;
|
||||||
|
|
||||||
|
ApiException(this.message, [this.statusCode]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ApiException: $message (Status: $statusCode)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP API client for making network requests
|
||||||
|
class ApiClient {
|
||||||
|
final http.Client _httpClient;
|
||||||
|
final String baseUrl;
|
||||||
|
final Duration timeout;
|
||||||
|
|
||||||
|
ApiClient({
|
||||||
|
http.Client? httpClient,
|
||||||
|
required this.baseUrl,
|
||||||
|
this.timeout = const Duration(seconds: 30),
|
||||||
|
}) : _httpClient = httpClient ?? http.Client();
|
||||||
|
|
||||||
|
/// Make a GET request
|
||||||
|
Future<Map<String, dynamic>> get(
|
||||||
|
String endpoint, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
}) async {
|
||||||
|
final uri = _buildUri(endpoint, queryParameters);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _httpClient
|
||||||
|
.get(
|
||||||
|
uri,
|
||||||
|
headers: _buildHeaders(headers),
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on TimeoutException {
|
||||||
|
throw ApiException('Request timeout');
|
||||||
|
} catch (e) {
|
||||||
|
throw ApiException('Network error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a POST request
|
||||||
|
Future<Map<String, dynamic>> post(
|
||||||
|
String endpoint, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Map<String, dynamic>? body,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
}) async {
|
||||||
|
final uri = _buildUri(endpoint, queryParameters);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _httpClient
|
||||||
|
.post(
|
||||||
|
uri,
|
||||||
|
headers: _buildHeaders(headers),
|
||||||
|
body: body != null ? jsonEncode(body) : null,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on TimeoutException {
|
||||||
|
throw ApiException('Request timeout');
|
||||||
|
} catch (e) {
|
||||||
|
throw ApiException('Network error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a PUT request
|
||||||
|
Future<Map<String, dynamic>> put(
|
||||||
|
String endpoint, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Map<String, dynamic>? body,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
}) async {
|
||||||
|
final uri = _buildUri(endpoint, queryParameters);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _httpClient
|
||||||
|
.put(
|
||||||
|
uri,
|
||||||
|
headers: _buildHeaders(headers),
|
||||||
|
body: body != null ? jsonEncode(body) : null,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on TimeoutException {
|
||||||
|
throw ApiException('Request timeout');
|
||||||
|
} catch (e) {
|
||||||
|
throw ApiException('Network error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a DELETE request
|
||||||
|
Future<Map<String, dynamic>> delete(
|
||||||
|
String endpoint, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
}) async {
|
||||||
|
final uri = _buildUri(endpoint, queryParameters);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _httpClient
|
||||||
|
.delete(
|
||||||
|
uri,
|
||||||
|
headers: _buildHeaders(headers),
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on TimeoutException {
|
||||||
|
throw ApiException('Request timeout');
|
||||||
|
} catch (e) {
|
||||||
|
throw ApiException('Network error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build URI with query parameters
|
||||||
|
Uri _buildUri(String endpoint, Map<String, dynamic>? queryParameters) {
|
||||||
|
final path = endpoint.startsWith('/') ? endpoint : '/$endpoint';
|
||||||
|
final uri = Uri.parse('$baseUrl$path');
|
||||||
|
|
||||||
|
if (queryParameters != null && queryParameters.isNotEmpty) {
|
||||||
|
return uri.replace(
|
||||||
|
queryParameters: queryParameters.map(
|
||||||
|
(key, value) => MapEntry(key, value.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build request headers
|
||||||
|
Map<String, String> _buildHeaders(Map<String, String>? customHeaders) {
|
||||||
|
final headers = <String, String>{
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (customHeaders != null) {
|
||||||
|
headers.addAll(customHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle HTTP response
|
||||||
|
Map<String, dynamic> _handleResponse(http.Response response) {
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
if (response.body.isEmpty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
} catch (e) {
|
||||||
|
throw ApiException('Invalid JSON response');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle error responses
|
||||||
|
String errorMessage = 'Request failed';
|
||||||
|
try {
|
||||||
|
final errorBody = jsonDecode(response.body);
|
||||||
|
errorMessage = errorBody['message'] ?? errorBody['error'] ?? errorMessage;
|
||||||
|
} catch (_) {
|
||||||
|
errorMessage = response.body.isNotEmpty
|
||||||
|
? response.body
|
||||||
|
: 'Status code: ${response.statusCode}';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ApiException(errorMessage, response.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispose resources
|
||||||
|
void dispose() {
|
||||||
|
_httpClient.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'user.dart';
|
||||||
|
|
||||||
|
enum BetStatus {
|
||||||
|
active('ACTIVE'),
|
||||||
|
awaitingValidation('AWAITING_VALIDATION'),
|
||||||
|
completed('COMPLETED'),
|
||||||
|
expired('EXPIRED');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
const BetStatus(this.value);
|
||||||
|
|
||||||
|
static BetStatus fromString(String value) {
|
||||||
|
return BetStatus.values.firstWhere(
|
||||||
|
(e) => e.value == value,
|
||||||
|
orElse: () => BetStatus.active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BetRole {
|
||||||
|
participant('PARTICIPANT'),
|
||||||
|
observer('OBSERVER');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
const BetRole(this.value);
|
||||||
|
|
||||||
|
static BetRole fromString(String value) {
|
||||||
|
return BetRole.values.firstWhere(
|
||||||
|
(e) => e.value == value,
|
||||||
|
orElse: () => BetRole.observer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BetParticipant extends Equatable {
|
||||||
|
final User user;
|
||||||
|
final BetRole role;
|
||||||
|
|
||||||
|
const BetParticipant({
|
||||||
|
required this.user,
|
||||||
|
required this.role,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory BetParticipant.fromJson(Map<String, dynamic> json) {
|
||||||
|
return BetParticipant(
|
||||||
|
user: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||||
|
role: BetRole.fromString(json['role'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'user': user.toJson(),
|
||||||
|
'role': role.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [user, role];
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bet extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final List<BetParticipant> participants;
|
||||||
|
final BetStatus status;
|
||||||
|
final DateTime startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
final bool isPublic;
|
||||||
|
final String? groupId;
|
||||||
|
final String stake;
|
||||||
|
final User? winner;
|
||||||
|
|
||||||
|
const Bet({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.participants,
|
||||||
|
required this.status,
|
||||||
|
required this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
required this.isPublic,
|
||||||
|
this.groupId,
|
||||||
|
required this.stake,
|
||||||
|
this.winner,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Bet.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Bet(
|
||||||
|
id: json['id'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
participants: (json['participants'] as List)
|
||||||
|
.map((p) => BetParticipant.fromJson(p as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
status: BetStatus.fromString(json['status'] as String),
|
||||||
|
startDate: DateTime.parse(json['startDate'] as String),
|
||||||
|
endDate: json['endDate'] != null
|
||||||
|
? DateTime.parse(json['endDate'] as String)
|
||||||
|
: null,
|
||||||
|
isPublic: json['isPublic'] as bool,
|
||||||
|
groupId: json['groupId'] as String?,
|
||||||
|
stake: json['stake'] as String,
|
||||||
|
winner: json['winner'] != null
|
||||||
|
? User.fromJson(json['winner'] as Map<String, dynamic>)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'participants': participants.map((p) => p.toJson()).toList(),
|
||||||
|
'status': status.value,
|
||||||
|
'startDate': startDate.toIso8601String(),
|
||||||
|
'endDate': endDate?.toIso8601String(),
|
||||||
|
'isPublic': isPublic,
|
||||||
|
'groupId': groupId,
|
||||||
|
'stake': stake,
|
||||||
|
'winner': winner?.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
participants,
|
||||||
|
status,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
isPublic,
|
||||||
|
groupId,
|
||||||
|
stake,
|
||||||
|
winner,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'user.dart';
|
||||||
|
|
||||||
|
enum FriendRequestStatus {
|
||||||
|
pending('PENDING');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
const FriendRequestStatus(this.value);
|
||||||
|
|
||||||
|
static FriendRequestStatus fromString(String value) {
|
||||||
|
return FriendRequestStatus.values.firstWhere(
|
||||||
|
(e) => e.value == value,
|
||||||
|
orElse: () => FriendRequestStatus.pending,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FriendRequest extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final User fromUser;
|
||||||
|
final User toUser;
|
||||||
|
final FriendRequestStatus status;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const FriendRequest({
|
||||||
|
required this.id,
|
||||||
|
required this.fromUser,
|
||||||
|
required this.toUser,
|
||||||
|
required this.status,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FriendRequest.fromJson(Map<String, dynamic> json) {
|
||||||
|
return FriendRequest(
|
||||||
|
id: json['id'] as String,
|
||||||
|
fromUser: User.fromJson(json['fromUser'] as Map<String, dynamic>),
|
||||||
|
toUser: User.fromJson(json['toUser'] as Map<String, dynamic>),
|
||||||
|
status: FriendRequestStatus.fromString(json['status'] as String),
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'fromUser': fromUser.toJson(),
|
||||||
|
'toUser': toUser.toJson(),
|
||||||
|
'status': status.value,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, fromUser, toUser, status, createdAt];
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'user.dart';
|
||||||
|
|
||||||
|
class Group extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final List<User> members;
|
||||||
|
|
||||||
|
const Group({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.members,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Group.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Group(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
members: (json['members'] as List)
|
||||||
|
.map((m) => User.fromJson(m as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'members': members.map((m) => m.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Group copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
List<User>? members,
|
||||||
|
}) {
|
||||||
|
return Group(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
members: members ?? this.members,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, name, members];
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class User extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String email;
|
||||||
|
final String avatarUrl;
|
||||||
|
final double score;
|
||||||
|
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.email,
|
||||||
|
required this.avatarUrl,
|
||||||
|
required this.score,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory User.fromJson(Map<String, dynamic> json) {
|
||||||
|
return User(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
email: json['email'] as String,
|
||||||
|
avatarUrl: json['avatarUrl'] as String,
|
||||||
|
score: (json['score'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'email': email,
|
||||||
|
'avatarUrl': avatarUrl,
|
||||||
|
'score': score,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
User copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? email,
|
||||||
|
String? avatarUrl,
|
||||||
|
double? score,
|
||||||
|
}) {
|
||||||
|
return User(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
email: email ?? this.email,
|
||||||
|
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||||
|
score: score ?? this.score,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, name, email, avatarUrl, score];
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// App route paths
|
||||||
|
class AppRoutes {
|
||||||
|
static const String home = '/';
|
||||||
|
static const String matchDetails = '/match/:id';
|
||||||
|
static const String bookmakerDetails = '/bookmaker/:id';
|
||||||
|
static const String settings = '/settings';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route configuration for Navigator 2.0
|
||||||
|
class AppRouterDelegate extends RouterDelegate<RouteInformation>
|
||||||
|
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteInformation> {
|
||||||
|
@override
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey;
|
||||||
|
|
||||||
|
String _currentPath = AppRoutes.home;
|
||||||
|
String? _matchId;
|
||||||
|
String? _bookmakerId;
|
||||||
|
|
||||||
|
AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
String get currentPath => _currentPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RouteInformation? get currentConfiguration {
|
||||||
|
if (_matchId != null) {
|
||||||
|
return RouteInformation(
|
||||||
|
uri: Uri.parse('/match/$_matchId'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_bookmakerId != null) {
|
||||||
|
return RouteInformation(
|
||||||
|
uri: Uri.parse('/bookmaker/$_bookmakerId'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return RouteInformation(uri: Uri.parse(_currentPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setNewRoutePath(RouteInformation configuration) async {
|
||||||
|
final uri = configuration.uri;
|
||||||
|
|
||||||
|
if (uri.pathSegments.isEmpty) {
|
||||||
|
_currentPath = AppRoutes.home;
|
||||||
|
_matchId = null;
|
||||||
|
_bookmakerId = null;
|
||||||
|
} else if (uri.pathSegments.length == 2) {
|
||||||
|
if (uri.pathSegments[0] == 'match') {
|
||||||
|
_currentPath = AppRoutes.matchDetails;
|
||||||
|
_matchId = uri.pathSegments[1];
|
||||||
|
} else if (uri.pathSegments[0] == 'bookmaker') {
|
||||||
|
_currentPath = AppRoutes.bookmakerDetails;
|
||||||
|
_bookmakerId = uri.pathSegments[1];
|
||||||
|
}
|
||||||
|
} else if (uri.pathSegments.length == 1) {
|
||||||
|
if (uri.pathSegments[0] == 'settings') {
|
||||||
|
_currentPath = AppRoutes.settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Navigator(
|
||||||
|
key: navigatorKey,
|
||||||
|
pages: [
|
||||||
|
const MaterialPage(
|
||||||
|
key: ValueKey('HomePage'),
|
||||||
|
child: Placeholder(), // Replace with actual HomePage
|
||||||
|
),
|
||||||
|
if (_matchId != null)
|
||||||
|
MaterialPage(
|
||||||
|
key: ValueKey('MatchDetailsPage-$_matchId'),
|
||||||
|
child: Placeholder(), // Replace with MatchDetailsPage
|
||||||
|
),
|
||||||
|
if (_bookmakerId != null)
|
||||||
|
MaterialPage(
|
||||||
|
key: ValueKey('BookmakerDetailsPage-$_bookmakerId'),
|
||||||
|
child: Placeholder(), // Replace with BookmakerDetailsPage
|
||||||
|
),
|
||||||
|
if (_currentPath == AppRoutes.settings)
|
||||||
|
const MaterialPage(
|
||||||
|
key: ValueKey('SettingsPage'),
|
||||||
|
child: Placeholder(), // Replace with SettingsPage
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onPopPage: (route, result) {
|
||||||
|
if (!route.didPop(result)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation state after pop
|
||||||
|
if (_matchId != null) {
|
||||||
|
_matchId = null;
|
||||||
|
notifyListeners();
|
||||||
|
} else if (_bookmakerId != null) {
|
||||||
|
_bookmakerId = null;
|
||||||
|
notifyListeners();
|
||||||
|
} else if (_currentPath == AppRoutes.settings) {
|
||||||
|
_currentPath = AppRoutes.home;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigate to match details
|
||||||
|
void navigateToMatchDetails(String matchId) {
|
||||||
|
_matchId = matchId;
|
||||||
|
_currentPath = AppRoutes.matchDetails;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigate to bookmaker details
|
||||||
|
void navigateToBookmakerDetails(String bookmakerId) {
|
||||||
|
_bookmakerId = bookmakerId;
|
||||||
|
_currentPath = AppRoutes.bookmakerDetails;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigate to settings
|
||||||
|
void navigateToSettings() {
|
||||||
|
_currentPath = AppRoutes.settings;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigate to home
|
||||||
|
void navigateToHome() {
|
||||||
|
_currentPath = AppRoutes.home;
|
||||||
|
_matchId = null;
|
||||||
|
_bookmakerId = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route information parser for Navigator 2.0
|
||||||
|
class AppRouteInformationParser extends RouteInformationParser<RouteInformation> {
|
||||||
|
@override
|
||||||
|
Future<RouteInformation> parseRouteInformation(
|
||||||
|
RouteInformation routeInformation,
|
||||||
|
) async {
|
||||||
|
return routeInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RouteInformation? restoreRouteInformation(RouteInformation configuration) {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "91.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.4.1"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.0"
|
||||||
|
bloc:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: bloc
|
||||||
|
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.1.4"
|
||||||
|
bloc_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: bloc_test
|
||||||
|
sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.7"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
cli_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_config
|
||||||
|
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
coverage:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: coverage
|
||||||
|
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.15.0"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.8"
|
||||||
|
diff_match_patch:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: diff_match_patch
|
||||||
|
sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
|
equatable:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_bloc:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_bloc
|
||||||
|
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.1.6"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
go_router:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: go_router
|
||||||
|
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.8.1"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.0"
|
||||||
|
io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.2"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.17"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.17.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
mocktail:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: mocktail
|
||||||
|
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
node_preamble:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: node_preamble
|
||||||
|
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
|
provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5+1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
|
shelf_packages_handler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_packages_handler
|
||||||
|
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
shelf_static:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_static
|
||||||
|
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_map_stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_map_stack_trace
|
||||||
|
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
source_maps:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_maps
|
||||||
|
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.13"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.1"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test
|
||||||
|
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.26.3"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.7"
|
||||||
|
test_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_core
|
||||||
|
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.12"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.0.2"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket
|
||||||
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
webkit_inspection_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webkit_inspection_protocol
|
||||||
|
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.9.0 <4.0.0"
|
||||||
|
flutter: ">=3.22.0"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: scomettiapp
|
||||||
|
description: A Flutter betting odds comparison application
|
||||||
|
publish_to: 'none'
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.5.0 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# State Management
|
||||||
|
flutter_bloc: ^8.1.6
|
||||||
|
bloc: ^8.1.4
|
||||||
|
equatable: ^2.0.5
|
||||||
|
|
||||||
|
# HTTP Client
|
||||||
|
http: ^1.2.2
|
||||||
|
|
||||||
|
# Material Design
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
|
|
||||||
|
# Utility
|
||||||
|
intl: ^0.19.0 # Per formattazione date
|
||||||
|
go_router: ^14.6.2 # Per Navigator 2.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^4.0.0
|
||||||
|
bloc_test: ^9.1.7
|
||||||
|
mocktail: ^1.0.4
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# assets:
|
||||||
|
# - images/
|
||||||
|
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
Reference in New Issue
Block a user