netgescon-master/docs/07-API-INTEGRAZIONI.md
Pikappa2 480e7eafbd 🎯 NETGESCON - Setup iniziale repository completo
📋 Commit iniziale con:
-  Documentazione unificata in docs/
-  Codice Laravel in netgescon-laravel/
-  Script automazione in scripts/
-  Configurazione sync rsync
-  Struttura organizzata e pulita

🔄 Versione: 2025.07.19-1644
🎯 Sistema pronto per Git distribuito
2025-07-19 16:44:47 +02:00

1620 lines
48 KiB
Markdown

# 7. API E INTEGRAZIONI - GUIDA COMPLETA
## 📋 **INDICE CAPITOLO**
- [7.1 Architettura API NetGescon](#71-architettura-api-netgescon)
- [7.2 Autenticazione API](#72-autenticazione-api)
- [7.3 Endpoint Principali](#73-endpoint-principali)
- [7.4 Middleware API](#74-middleware-api)
- [7.5 Documentazione API](#75-documentazione-api)
- [7.6 Integrazioni Esterne](#76-integrazioni-esterne)
- [7.7 Rate Limiting](#77-rate-limiting)
- [7.8 Versioning API](#78-versioning-api)
- [7.9 Testing API](#79-testing-api)
- [7.10 Esempi Pratici](#710-esempi-pratici)
---
## 7.1 Architettura API NetGescon
### Struttura API RESTful
```
/api/v1/
├── auth/ # Autenticazione
│ ├── login # POST - Login utente
│ ├── logout # POST - Logout utente
│ ├── refresh # POST - Refresh token
│ └── me # GET - Dati utente corrente
├── stabili/ # Gestione stabili
│ ├── GET / # Lista stabili
│ ├── POST / # Crea stabile
│ ├── GET /{id} # Dettaglio stabile
│ ├── PUT /{id} # Aggiorna stabile
│ └── DELETE /{id} # Elimina stabile
├── unita/ # Unità immobiliari
│ ├── GET / # Lista unità
│ ├── POST / # Crea unità
│ ├── GET /{id} # Dettaglio unità
│ ├── PUT /{id} # Aggiorna unità
│ └── DELETE /{id} # Elimina unità
├── condomini/ # Gestione condomini
│ ├── GET / # Lista condomini
│ ├── POST / # Crea condomino
│ ├── GET /{id} # Dettaglio condomino
│ ├── PUT /{id} # Aggiorna condomino
│ └── DELETE /{id} # Elimina condomino
├── contabilita/ # Sistema contabile
│ ├── movimenti/ # Movimenti contabili
│ ├── bilanci/ # Bilanci
│ └── reports/ # Report finanziari
├── documenti/ # Gestione documenti
│ ├── GET / # Lista documenti
│ ├── POST / # Upload documento
│ ├── GET /{id} # Scarica documento
│ └── DELETE /{id} # Elimina documento
└── comunicazioni/ # Sistema comunicazioni
├── GET / # Lista comunicazioni
├── POST / # Invia comunicazione
└── GET /{id} # Dettaglio comunicazione
```
### Configurazione Routes API
```php
// routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\StabileController;
use App\Http\Controllers\Api\UnitaController;
use App\Http\Controllers\Api\CondominiController;
use App\Http\Controllers\Api\ContabilitaController;
use App\Http\Controllers\Api\DocumentiController;
use App\Http\Controllers\Api\ComunicazioniController;
// Gruppo API versione 1
Route::group(['prefix' => 'v1'], function () {
// Autenticazione (pubbliche)
Route::post('auth/login', [AuthController::class, 'login']);
Route::post('auth/register', [AuthController::class, 'register']);
Route::post('auth/forgot-password', [AuthController::class, 'forgotPassword']);
Route::post('auth/reset-password', [AuthController::class, 'resetPassword']);
// Route protette da autenticazione
Route::middleware('auth:sanctum')->group(function () {
// Autenticazione utente
Route::post('auth/logout', [AuthController::class, 'logout']);
Route::post('auth/refresh', [AuthController::class, 'refresh']);
Route::get('auth/me', [AuthController::class, 'me']);
// Stabili
Route::apiResource('stabili', StabileController::class);
Route::get('stabili/{id}/unita', [StabileController::class, 'getUnita']);
Route::get('stabili/{id}/condomini', [StabileController::class, 'getCondomini']);
// Unità immobiliari
Route::apiResource('unita', UnitaController::class);
Route::get('unita/{id}/proprietari', [UnitaController::class, 'getProprietari']);
// Condomini
Route::apiResource('condomini', CondominiController::class);
Route::get('condomini/{id}/unita', [CondominiController::class, 'getUnita']);
// Contabilità
Route::prefix('contabilita')->group(function () {
Route::apiResource('movimenti', ContabilitaController::class);
Route::get('bilanci', [ContabilitaController::class, 'getBilanci']);
Route::get('reports', [ContabilitaController::class, 'getReports']);
});
// Documenti
Route::apiResource('documenti', DocumentiController::class);
Route::post('documenti/{id}/upload', [DocumentiController::class, 'upload']);
Route::get('documenti/{id}/download', [DocumentiController::class, 'download']);
// Comunicazioni
Route::apiResource('comunicazioni', ComunicazioniController::class);
Route::post('comunicazioni/{id}/read', [ComunicazioniController::class, 'markAsRead']);
});
});
```
---
## 7.2 Autenticazione API
### Laravel Sanctum Setup
```php
// config/sanctum.php
<?php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
'guard' => ['web'],
'expiration' => null,
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
];
```
### Auth Controller
```php
// app/Http/Controllers/Api/AuthController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Login utente
*/
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required|string|min:6',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation Error',
'errors' => $validator->errors()
], 400);
}
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json([
'success' => false,
'message' => 'Invalid credentials'
], 401);
}
if (!$user->is_active) {
return response()->json([
'success' => false,
'message' => 'Account not active'
], 401);
}
$token = $user->createToken('api-token')->plainTextToken;
// Aggiorna ultimo login
$user->last_login_at = now();
$user->save();
return response()->json([
'success' => true,
'message' => 'Login successful',
'data' => [
'user' => $user,
'token' => $token,
'token_type' => 'Bearer'
]
]);
}
/**
* Logout utente
*/
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'success' => true,
'message' => 'Logout successful'
]);
}
/**
* Informazioni utente corrente
*/
public function me(Request $request)
{
$user = $request->user();
$user->load('roles', 'permissions');
return response()->json([
'success' => true,
'data' => [
'user' => $user,
'roles' => $user->roles->pluck('name'),
'permissions' => $user->getAllPermissions()->pluck('name')
]
]);
}
/**
* Refresh token
*/
public function refresh(Request $request)
{
$user = $request->user();
$user->currentAccessToken()->delete();
$token = $user->createToken('api-token')->plainTextToken;
return response()->json([
'success' => true,
'data' => [
'token' => $token,
'token_type' => 'Bearer'
]
]);
}
}
```
---
## 7.3 Endpoint Principali
### Stabili API Controller
```php
// app/Http/Controllers/Api/StabileController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Stabile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class StabileController extends Controller
{
/**
* Lista stabili
*/
public function index(Request $request)
{
$query = Stabile::with(['comune', 'unita_immobiliari']);
// Filtri
if ($request->has('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('denominazione', 'like', "%{$search}%")
->orWhere('indirizzo', 'like', "%{$search}%")
->orWhere('codice_fiscale', 'like', "%{$search}%");
});
}
if ($request->has('comune_id')) {
$query->where('comune_id', $request->comune_id);
}
// Paginazione
$per_page = $request->get('per_page', 15);
$stabili = $query->paginate($per_page);
return response()->json([
'success' => true,
'data' => $stabili
]);
}
/**
* Crea stabile
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'denominazione' => 'required|string|max:255',
'indirizzo' => 'required|string|max:255',
'codice_fiscale' => 'required|string|size:16|unique:stabili',
'comune_id' => 'required|exists:comuni_italiani,id',
'amministratore_id' => 'required|exists:users,id',
'data_nomina' => 'required|date',
'piano_terra' => 'required|integer|min:0',
'piano_primo' => 'required|integer|min:0',
'piano_secondo' => 'required|integer|min:0',
'piano_terzo' => 'required|integer|min:0',
'piano_quarto' => 'required|integer|min:0',
'piano_quinto' => 'required|integer|min:0',
'piano_sesto' => 'required|integer|min:0',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation Error',
'errors' => $validator->errors()
], 400);
}
$stabile = Stabile::create($request->all());
$stabile->load(['comune', 'amministratore']);
return response()->json([
'success' => true,
'message' => 'Stabile created successfully',
'data' => $stabile
], 201);
}
/**
* Dettaglio stabile
*/
public function show($id)
{
$stabile = Stabile::with([
'comune',
'amministratore',
'unita_immobiliari',
'condomini'
])->find($id);
if (!$stabile) {
return response()->json([
'success' => false,
'message' => 'Stabile not found'
], 404);
}
return response()->json([
'success' => true,
'data' => $stabile
]);
}
/**
* Aggiorna stabile
*/
public function update(Request $request, $id)
{
$stabile = Stabile::find($id);
if (!$stabile) {
return response()->json([
'success' => false,
'message' => 'Stabile not found'
], 404);
}
$validator = Validator::make($request->all(), [
'denominazione' => 'sometimes|required|string|max:255',
'indirizzo' => 'sometimes|required|string|max:255',
'codice_fiscale' => 'sometimes|required|string|size:16|unique:stabili,codice_fiscale,' . $id,
'comune_id' => 'sometimes|required|exists:comuni_italiani,id',
'amministratore_id' => 'sometimes|required|exists:users,id',
'data_nomina' => 'sometimes|required|date',
'piano_terra' => 'sometimes|required|integer|min:0',
'piano_primo' => 'sometimes|required|integer|min:0',
'piano_secondo' => 'sometimes|required|integer|min:0',
'piano_terzo' => 'sometimes|required|integer|min:0',
'piano_quarto' => 'sometimes|required|integer|min:0',
'piano_quinto' => 'sometimes|required|integer|min:0',
'piano_sesto' => 'sometimes|required|integer|min:0',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation Error',
'errors' => $validator->errors()
], 400);
}
$stabile->update($request->all());
$stabile->load(['comune', 'amministratore']);
return response()->json([
'success' => true,
'message' => 'Stabile updated successfully',
'data' => $stabile
]);
}
/**
* Elimina stabile
*/
public function destroy($id)
{
$stabile = Stabile::find($id);
if (!$stabile) {
return response()->json([
'success' => false,
'message' => 'Stabile not found'
], 404);
}
$stabile->delete();
return response()->json([
'success' => true,
'message' => 'Stabile deleted successfully'
]);
}
/**
* Unità immobiliari di uno stabile
*/
public function getUnita($id)
{
$stabile = Stabile::with(['unita_immobiliari.proprietari'])->find($id);
if (!$stabile) {
return response()->json([
'success' => false,
'message' => 'Stabile not found'
], 404);
}
return response()->json([
'success' => true,
'data' => $stabile->unita_immobiliari
]);
}
/**
* Condomini di uno stabile
*/
public function getCondomini($id)
{
$stabile = Stabile::with(['condomini'])->find($id);
if (!$stabile) {
return response()->json([
'success' => false,
'message' => 'Stabile not found'
], 404);
}
return response()->json([
'success' => true,
'data' => $stabile->condomini
]);
}
}
```
---
## 7.4 Middleware API
### Rate Limiting Middleware
```php
// app/Http/Middleware/ApiRateLimit.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class ApiRateLimit
{
public function handle(Request $request, Closure $next, $maxAttempts = 60, $decayMinutes = 1): Response
{
$key = $this->resolveRequestSignature($request);
if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
return response()->json([
'success' => false,
'message' => 'Too many requests. Please try again later.',
'retry_after' => RateLimiter::availableIn($key)
], 429);
}
RateLimiter::hit($key, $decayMinutes * 60);
$response = $next($request);
return $response->header('X-RateLimit-Limit', $maxAttempts)
->header('X-RateLimit-Remaining', RateLimiter::remainingAttempts($key, $maxAttempts))
->header('X-RateLimit-Reset', RateLimiter::availableIn($key));
}
protected function resolveRequestSignature(Request $request): string
{
if ($user = $request->user()) {
return sha1('api|' . $user->getAuthIdentifier());
}
return sha1('api|' . $request->ip());
}
}
```
### API Response Middleware
```php
// app/Http/Middleware/ApiResponse.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiResponse
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Aggiungi headers comuni
$response->headers->set('Content-Type', 'application/json');
$response->headers->set('X-API-Version', 'v1');
$response->headers->set('X-Powered-By', 'NetGescon API');
return $response;
}
}
```
---
## 7.5 Documentazione API
### OpenAPI/Swagger Setup
```php
// config/l5-swagger.php
<?php
return [
'default' => 'default',
'documentations' => [
'default' => [
'api' => [
'title' => 'NetGescon API Documentation',
'version' => '1.0.0',
'description' => 'API completa per la gestione condominiale NetGescon',
],
'routes' => [
'api' => 'api/documentation',
],
'paths' => [
'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true),
'docs_json' => 'api-docs.json',
'docs_yaml' => 'api-docs.yaml',
'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'),
'annotations' => [
base_path('app'),
],
],
],
],
'defaults' => [
'routes' => [
'docs' => 'docs',
'oauth2_callback' => 'api/oauth2-callback',
'middleware' => [
'api' => [],
'asset' => [],
'docs' => [],
'oauth2_callback' => [],
],
'group_options' => [],
],
'paths' => [
'docs' => storage_path('api-docs'),
'views' => base_path('resources/views/vendor/l5-swagger'),
'base' => env('L5_SWAGGER_BASE_PATH', null),
'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'),
'excludes' => [],
],
'scanOptions' => [
'analyser' => null,
'analysis' => null,
'processors' => [],
'pattern' => null,
'exclude' => [],
],
'securityDefinitions' => [
'securitySchemes' => [
'sanctum' => [
'type' => 'apiKey',
'description' => 'Enter token in format (Bearer <token>)',
'name' => 'Authorization',
'in' => 'header',
],
],
'security' => [
[
'sanctum' => []
],
],
],
],
];
```
### Annotazioni Swagger
```php
// app/Http/Controllers/Api/StabileController.php con annotazioni
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Stabile;
use Illuminate\Http\Request;
/**
* @OA\Tag(
* name="Stabili",
* description="Gestione stabili condominiali"
* )
*/
class StabileController extends Controller
{
/**
* @OA\Get(
* path="/api/v1/stabili",
* summary="Lista stabili",
* description="Recupera lista paginata di tutti gli stabili",
* operationId="getStabili",
* tags={"Stabili"},
* security={{"sanctum":{}}},
* @OA\Parameter(
* name="page",
* in="query",
* description="Numero pagina",
* required=false,
* @OA\Schema(type="integer")
* ),
* @OA\Parameter(
* name="per_page",
* in="query",
* description="Elementi per pagina",
* required=false,
* @OA\Schema(type="integer")
* ),
* @OA\Parameter(
* name="search",
* in="query",
* description="Termine di ricerca",
* required=false,
* @OA\Schema(type="string")
* ),
* @OA\Response(
* response=200,
* description="Lista stabili recuperata con successo",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="data", type="object")
* )
* ),
* @OA\Response(
* response=401,
* description="Non autorizzato",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=false),
* @OA\Property(property="message", type="string", example="Unauthenticated")
* )
* )
* )
*/
public function index(Request $request)
{
// Implementazione...
}
/**
* @OA\Post(
* path="/api/v1/stabili",
* summary="Crea stabile",
* description="Crea un nuovo stabile condominiale",
* operationId="createStabile",
* tags={"Stabili"},
* security={{"sanctum":{}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"denominazione", "indirizzo", "codice_fiscale", "comune_id", "amministratore_id", "data_nomina"},
* @OA\Property(property="denominazione", type="string", example="Condominio Rossi"),
* @OA\Property(property="indirizzo", type="string", example="Via Roma 123"),
* @OA\Property(property="codice_fiscale", type="string", example="12345678901234567890"),
* @OA\Property(property="comune_id", type="integer", example=1),
* @OA\Property(property="amministratore_id", type="integer", example=1),
* @OA\Property(property="data_nomina", type="string", format="date", example="2024-01-01"),
* @OA\Property(property="piano_terra", type="integer", example=2),
* @OA\Property(property="piano_primo", type="integer", example=4),
* @OA\Property(property="piano_secondo", type="integer", example=4),
* @OA\Property(property="piano_terzo", type="integer", example=0),
* @OA\Property(property="piano_quarto", type="integer", example=0),
* @OA\Property(property="piano_quinto", type="integer", example=0),
* @OA\Property(property="piano_sesto", type="integer", example=0)
* )
* ),
* @OA\Response(
* response=201,
* description="Stabile creato con successo",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="Stabile created successfully"),
* @OA\Property(property="data", type="object")
* )
* ),
* @OA\Response(
* response=400,
* description="Errore validazione",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=false),
* @OA\Property(property="message", type="string", example="Validation Error"),
* @OA\Property(property="errors", type="object")
* )
* )
* )
*/
public function store(Request $request)
{
// Implementazione...
}
}
```
---
## 7.6 Integrazioni Esterne
### Configurazione Servizi Esterni
```php
// config/services.php
<?php
return [
// Servizi esistenti...
'agenzia_entrate' => [
'base_url' => env('AGENZIA_ENTRATE_BASE_URL', 'https://www.agenziaentrate.gov.it/ws'),
'api_key' => env('AGENZIA_ENTRATE_API_KEY'),
'timeout' => 30,
],
'registro_imprese' => [
'base_url' => env('REGISTRO_IMPRESE_BASE_URL', 'https://www.registroimprese.it/api'),
'api_key' => env('REGISTRO_IMPRESE_API_KEY'),
'timeout' => 30,
],
'poste_italiane' => [
'base_url' => env('POSTE_ITALIANE_BASE_URL', 'https://api.posteitaliane.it'),
'api_key' => env('POSTE_ITALIANE_API_KEY'),
'timeout' => 30,
],
'pec_provider' => [
'base_url' => env('PEC_PROVIDER_BASE_URL'),
'api_key' => env('PEC_PROVIDER_API_KEY'),
'timeout' => 30,
],
];
```
### Service per Integrazioni
```php
// app/Services/AgenziaEntrateService.php
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Exception;
class AgenziaEntrateService
{
protected $baseUrl;
protected $apiKey;
protected $timeout;
public function __construct()
{
$this->baseUrl = config('services.agenzia_entrate.base_url');
$this->apiKey = config('services.agenzia_entrate.api_key');
$this->timeout = config('services.agenzia_entrate.timeout');
}
/**
* Verifica codice fiscale
*/
public function verificaCodiceFiscale($codiceFiscale)
{
try {
$response = Http::timeout($this->timeout)
->withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
])
->get($this->baseUrl . '/verifica-cf', [
'codice_fiscale' => $codiceFiscale
]);
if ($response->successful()) {
return [
'success' => true,
'data' => $response->json()
];
}
return [
'success' => false,
'message' => 'Errore nella verifica: ' . $response->body()
];
} catch (Exception $e) {
Log::error('Errore AgenziaEntrateService::verificaCodiceFiscale', [
'codice_fiscale' => $codiceFiscale,
'error' => $e->getMessage()
]);
return [
'success' => false,
'message' => 'Errore di connessione al servizio'
];
}
}
/**
* Verifica partita IVA
*/
public function verificaPartitaIva($partitaIva)
{
try {
$response = Http::timeout($this->timeout)
->withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
])
->get($this->baseUrl . '/verifica-piva', [
'partita_iva' => $partitaIva
]);
if ($response->successful()) {
return [
'success' => true,
'data' => $response->json()
];
}
return [
'success' => false,
'message' => 'Errore nella verifica: ' . $response->body()
];
} catch (Exception $e) {
Log::error('Errore AgenziaEntrateService::verificaPartitaIva', [
'partita_iva' => $partitaIva,
'error' => $e->getMessage()
]);
return [
'success' => false,
'message' => 'Errore di connessione al servizio'
];
}
}
}
```
---
## 7.7 Rate Limiting
### Configurazione Rate Limiting
```php
// app/Providers/RouteServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
protected function configureRateLimiting()
{
// Rate limiting per API autenticata
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(60)->by($request->user()->id)
: Limit::perMinute(20)->by($request->ip());
});
// Rate limiting per login
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
// Rate limiting per operazioni pesanti
RateLimiter::for('heavy-operations', function (Request $request) {
return $request->user()
? Limit::perMinute(10)->by($request->user()->id)
: Limit::perMinute(2)->by($request->ip());
});
}
}
```
### Middleware Rate Limiting Custom
```php
// app/Http/Middleware/CustomRateLimit.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class CustomRateLimit
{
public function handle(Request $request, Closure $next, string $limiter = 'api'): Response
{
$key = $this->resolveRequestSignature($request);
// Controlla se l'utente ha superato il limite
if (RateLimiter::tooManyAttempts($key, $this->maxAttempts($limiter))) {
return $this->buildResponse($key, $this->maxAttempts($limiter));
}
// Incrementa il contatore
RateLimiter::hit($key, $this->decayMinutes($limiter) * 60);
$response = $next($request);
return $this->addHeaders($response, $key, $this->maxAttempts($limiter));
}
protected function resolveRequestSignature(Request $request): string
{
if ($user = $request->user()) {
return sha1($request->route()->getName() . '|' . $user->getAuthIdentifier());
}
return sha1($request->route()->getName() . '|' . $request->ip());
}
protected function maxAttempts(string $limiter): int
{
return match($limiter) {
'login' => 5,
'heavy-operations' => 10,
'api' => 60,
default => 20
};
}
protected function decayMinutes(string $limiter): int
{
return match($limiter) {
'login' => 1,
'heavy-operations' => 5,
'api' => 1,
default => 1
};
}
protected function buildResponse(string $key, int $maxAttempts): Response
{
$retryAfter = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => 'Too Many Attempts',
'retry_after' => $retryAfter,
'max_attempts' => $maxAttempts
], 429);
}
protected function addHeaders(Response $response, string $key, int $maxAttempts): Response
{
$response->headers->add([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => RateLimiter::remainingAttempts($key, $maxAttempts),
'X-RateLimit-Reset' => RateLimiter::availableIn($key),
]);
return $response;
}
}
```
---
## 7.8 Versioning API
### Versioning tramite URI
```php
// routes/api.php
Route::group(['prefix' => 'v1', 'namespace' => 'App\Http\Controllers\Api\V1'], function () {
// Route API v1
});
Route::group(['prefix' => 'v2', 'namespace' => 'App\Http\Controllers\Api\V2'], function () {
// Route API v2
});
```
### Versioning tramite Header
```php
// app/Http/Middleware/ApiVersioning.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiVersioning
{
public function handle(Request $request, Closure $next): Response
{
$version = $request->header('Accept-Version', 'v1');
// Valida versione
if (!in_array($version, ['v1', 'v2'])) {
return response()->json([
'success' => false,
'message' => 'Unsupported API version'
], 400);
}
// Aggiungi versione alla request
$request->merge(['api_version' => $version]);
$response = $next($request);
// Aggiungi header versione alla response
$response->headers->set('X-API-Version', $version);
return $response;
}
}
```
---
## 7.9 Testing API
### Test Feature API
```php
// tests/Feature/Api/StabileApiTest.php
<?php
namespace Tests\Feature\Api;
use App\Models\User;
use App\Models\Stabile;
use App\Models\ComuneItaliano;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class StabileApiTest extends TestCase
{
use RefreshDatabase, WithFaker;
protected $user;
protected $admin;
protected $token;
protected function setUp(): void
{
parent::setUp();
// Crea utente test
$this->user = User::factory()->create();
$this->admin = User::factory()->create();
$this->admin->assignRole('admin');
// Crea token per autenticazione
$this->token = $this->admin->createToken('test-token')->plainTextToken;
}
/** @test */
public function it_can_list_stabili()
{
// Arrange
Stabile::factory()->count(5)->create();
// Act
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get('/api/v1/stabili');
// Assert
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'data' => [
'*' => [
'id',
'denominazione',
'indirizzo',
'codice_fiscale',
'comune_id',
'amministratore_id',
'created_at',
'updated_at'
]
],
'links',
'meta'
]
]);
}
/** @test */
public function it_can_create_stabile()
{
// Arrange
$comune = ComuneItaliano::factory()->create();
$stabileData = [
'denominazione' => 'Condominio Test',
'indirizzo' => 'Via Test 123',
'codice_fiscale' => '12345678901234567890',
'comune_id' => $comune->id,
'amministratore_id' => $this->admin->id,
'data_nomina' => '2024-01-01',
'piano_terra' => 2,
'piano_primo' => 4,
'piano_secondo' => 4,
'piano_terzo' => 0,
'piano_quarto' => 0,
'piano_quinto' => 0,
'piano_sesto' => 0,
];
// Act
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->post('/api/v1/stabili', $stabileData);
// Assert
$response->assertStatus(201)
->assertJsonStructure([
'success',
'message',
'data' => [
'id',
'denominazione',
'indirizzo',
'codice_fiscale',
'comune_id',
'amministratore_id',
'created_at',
'updated_at'
]
]);
$this->assertDatabaseHas('stabili', [
'denominazione' => 'Condominio Test',
'codice_fiscale' => '12345678901234567890'
]);
}
/** @test */
public function it_validates_stabile_creation()
{
// Act
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->post('/api/v1/stabili', []);
// Assert
$response->assertStatus(400)
->assertJsonStructure([
'success',
'message',
'errors'
]);
}
/** @test */
public function it_requires_authentication()
{
// Act
$response = $this->withHeaders([
'Accept' => 'application/json',
])->get('/api/v1/stabili');
// Assert
$response->assertStatus(401);
}
/** @test */
public function it_can_show_stabile()
{
// Arrange
$stabile = Stabile::factory()->create();
// Act
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get('/api/v1/stabili/' . $stabile->id);
// Assert
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'id',
'denominazione',
'indirizzo',
'codice_fiscale',
'comune',
'amministratore',
'unita_immobiliari',
'condomini'
]
]);
}
/** @test */
public function it_can_update_stabile()
{
// Arrange
$stabile = Stabile::factory()->create();
$updateData = [
'denominazione' => 'Condominio Aggiornato'
];
// Act
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->put('/api/v1/stabili/' . $stabile->id, $updateData);
// Assert
$response->assertStatus(200)
->assertJsonStructure([
'success',
'message',
'data'
]);
$this->assertDatabaseHas('stabili', [
'id' => $stabile->id,
'denominazione' => 'Condominio Aggiornato'
]);
}
/** @test */
public function it_can_delete_stabile()
{
// Arrange
$stabile = Stabile::factory()->create();
// Act
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->delete('/api/v1/stabili/' . $stabile->id);
// Assert
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'Stabile deleted successfully'
]);
$this->assertSoftDeleted('stabili', [
'id' => $stabile->id
]);
}
/** @test */
public function it_handles_not_found_stabile()
{
// Act
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get('/api/v1/stabili/999');
// Assert
$response->assertStatus(404)
->assertJson([
'success' => false,
'message' => 'Stabile not found'
]);
}
}
```
---
## 7.10 Esempi Pratici
### Client JavaScript
```javascript
// js/api-client.js
class NetGesconApiClient {
constructor(baseUrl, token = null) {
this.baseUrl = baseUrl;
this.token = token;
this.headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
if (token) {
this.headers['Authorization'] = `Bearer ${token}`;
}
}
async login(email, password) {
try {
const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
this.token = data.data.token;
this.headers['Authorization'] = `Bearer ${this.token}`;
localStorage.setItem('netgescon_token', this.token);
}
return data;
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
async logout() {
try {
const response = await fetch(`${this.baseUrl}/api/v1/auth/logout`, {
method: 'POST',
headers: this.headers
});
if (response.ok) {
this.token = null;
delete this.headers['Authorization'];
localStorage.removeItem('netgescon_token');
}
return await response.json();
} catch (error) {
console.error('Logout error:', error);
throw error;
}
}
async getStabili(params = {}) {
try {
const queryString = new URLSearchParams(params).toString();
const url = `${this.baseUrl}/api/v1/stabili${queryString ? '?' + queryString : ''}`;
const response = await fetch(url, {
method: 'GET',
headers: this.headers
});
return await response.json();
} catch (error) {
console.error('Get stabili error:', error);
throw error;
}
}
async createStabile(data) {
try {
const response = await fetch(`${this.baseUrl}/api/v1/stabili`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(data)
});
return await response.json();
} catch (error) {
console.error('Create stabile error:', error);
throw error;
}
}
async updateStabile(id, data) {
try {
const response = await fetch(`${this.baseUrl}/api/v1/stabili/${id}`, {
method: 'PUT',
headers: this.headers,
body: JSON.stringify(data)
});
return await response.json();
} catch (error) {
console.error('Update stabile error:', error);
throw error;
}
}
async deleteStabile(id) {
try {
const response = await fetch(`${this.baseUrl}/api/v1/stabili/${id}`, {
method: 'DELETE',
headers: this.headers
});
return await response.json();
} catch (error) {
console.error('Delete stabile error:', error);
throw error;
}
}
}
// Utilizzo
const apiClient = new NetGesconApiClient('http://localhost:8000');
// Login
apiClient.login('admin@example.com', 'password')
.then(response => {
if (response.success) {
console.log('Login successful:', response.data.user);
}
});
// Recupera stabili
apiClient.getStabili({ page: 1, per_page: 10, search: 'condominio' })
.then(response => {
if (response.success) {
console.log('Stabili:', response.data);
}
});
```
### Client cURL
```bash
#!/bin/bash
# Variabili
API_BASE_URL="http://localhost:8000/api/v1"
TOKEN=""
# Login
login() {
local email=$1
local password=$2
response=$(curl -s -X POST "$API_BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{\"email\":\"$email\",\"password\":\"$password\"}")
echo "$response"
# Estrai token
TOKEN=$(echo "$response" | jq -r '.data.token')
if [ "$TOKEN" != "null" ]; then
echo "Login successful. Token: $TOKEN"
else
echo "Login failed"
fi
}
# Lista stabili
get_stabili() {
curl -s -X GET "$API_BASE_URL/stabili" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
}
# Crea stabile
create_stabile() {
local data=$1
curl -s -X POST "$API_BASE_URL/stabili" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "$data"
}
# Esempi di utilizzo
echo "=== LOGIN ==="
login "admin@example.com" "password"
echo -e "\n=== LISTA STABILI ==="
get_stabili | jq '.'
echo -e "\n=== CREA STABILE ==="
stabile_data='{
"denominazione": "Condominio Test",
"indirizzo": "Via Test 123",
"codice_fiscale": "12345678901234567890",
"comune_id": 1,
"amministratore_id": 1,
"data_nomina": "2024-01-01",
"piano_terra": 2,
"piano_primo": 4,
"piano_secondo": 4,
"piano_terzo": 0,
"piano_quarto": 0,
"piano_quinto": 0,
"piano_sesto": 0
}'
create_stabile "$stabile_data" | jq '.'
```
---
## 🔧 **CONFIGURAZIONE COMPLETATA**
### ✅ **Caratteristiche Implementate**
1. **Architettura RESTful** completa con versioning
2. **Autenticazione Sanctum** con token management
3. **Endpoint CRUD** per tutte le entità principali
4. **Middleware** per rate limiting e sicurezza
5. **Documentazione Swagger** automatica
6. **Testing** completo con Feature Tests
7. **Integrazioni esterne** configurabili
8. **Client JavaScript** e esempi cURL
### 🚀 **Prossimi Passi**
1. Implementare endpoint per contabilità
2. Aggiungere webhook per notifiche
3. Completare documentazione Swagger
4. Implementare cache per performance
5. Aggiungere monitoring e logging
### 📚 **Collegamenti Utili**
- **Capitolo 4**: Database e Strutture
- **Capitolo 5**: Interfaccia Universale
- **Capitolo 6**: Sistema Multi-Ruolo
- **Capitolo 8**: Frontend e UX
---
*Questo capitolo fornisce una base solida per l'implementazione di API RESTful complete per NetGescon, con focus su sicurezza, performance e documentazione.*