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

48 KiB

7. API E INTEGRAZIONI - GUIDA COMPLETA

📋 INDICE CAPITOLO


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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

#!/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.