[BACKEND] Completa backend: validate-invite, lookup-piva, ruoli, SIM-06
AuthController: - register() accetta `role` diretto (compliance_manager, org_admin, auditor, board_member, consultant) - Aggiunto validateInvite() → POST /api/auth/validate-invite (no auth) OnboardingController: - Aggiunto lookupPiva() → POST /api/onboarding/lookup-piva (no auth, rate limit 10/min) usato da register.html per P.IVA lookup pre-login Router (index.php): - Aggiunto POST:validateInvite e POST:lookupPiva api.js: - register() invia sia `role` che `user_type` per retrocompatibilità simulate-nis2.php: - SIM-06: B2B provisioning via X-Provision-Secret → org + JWT + API Key - Filtro NIS2_SIM=SIM06 via goto per skip SIM-01→05 indipendenti - readEnvValue() helper per leggere PROVISION_SECRET da .env register.html: - lookupPiva usa /onboarding/lookup-piva (endpoint pubblico) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e4e7d94043
commit
ab0e3755f4
@@ -45,8 +45,19 @@ class AuthController extends BaseController
|
||||
$password = $this->getParam('password');
|
||||
$fullName = trim($this->getParam('full_name'));
|
||||
$phone = $this->getParam('phone');
|
||||
$userType = $this->getParam('user_type', 'azienda'); // 'azienda' | 'consultant'
|
||||
$role = ($userType === 'consultant') ? 'consultant' : 'employee';
|
||||
|
||||
// Supporta sia `role` diretto (nuovo register.html) che `user_type` legacy
|
||||
$validRoles = ['super_admin', 'org_admin', 'compliance_manager', 'board_member', 'auditor', 'employee', 'consultant'];
|
||||
$roleParam = trim($this->getParam('role', ''));
|
||||
$userType = $this->getParam('user_type', 'azienda');
|
||||
|
||||
if ($roleParam && in_array($roleParam, $validRoles, true) && $roleParam !== 'super_admin') {
|
||||
$role = $roleParam;
|
||||
} elseif ($userType === 'consultant') {
|
||||
$role = 'consultant';
|
||||
} else {
|
||||
$role = 'employee';
|
||||
}
|
||||
|
||||
// Validazione email
|
||||
if (!$this->validateEmail($email)) {
|
||||
@@ -319,4 +330,44 @@ class AuthController extends BaseController
|
||||
|
||||
$this->jsonSuccess(null, 'Password modificata. Effettua nuovamente il login.');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/validate-invite
|
||||
*
|
||||
* Valida un codice invito B2B e restituisce piano e metadati.
|
||||
* Nessuna autenticazione richiesta — usato dalla pagina register.html
|
||||
* prima della registrazione per mostrare l'anteprima del piano.
|
||||
*
|
||||
* Body: { "invite_token": "inv_xxxx..." }
|
||||
* Response: { valid: true, plan: "professional", duration_months: 12, ... }
|
||||
*/
|
||||
public function validateInvite(): void
|
||||
{
|
||||
$token = trim($this->getParam('invite_token', ''));
|
||||
|
||||
if (!$token) {
|
||||
$this->jsonError('invite_token mancante', 400, 'MISSING_TOKEN');
|
||||
}
|
||||
|
||||
require_once APP_PATH . '/controllers/InviteController.php';
|
||||
$result = InviteController::resolveInvite($token);
|
||||
|
||||
if (!$result['valid']) {
|
||||
$this->jsonError($result['error'], 422, $result['code'] ?? 'INVALID_INVITE');
|
||||
}
|
||||
|
||||
$inv = $result['invite'];
|
||||
|
||||
$this->jsonSuccess([
|
||||
'valid' => true,
|
||||
'plan' => $inv['plan'],
|
||||
'duration_months' => (int) $inv['duration_months'],
|
||||
'expires_at' => $inv['expires_at'],
|
||||
'remaining_uses' => (int)$inv['max_uses'] - (int)$inv['used_count'],
|
||||
'channel' => $inv['channel'],
|
||||
'label' => $inv['label'],
|
||||
'restrict_vat' => $inv['restrict_vat'] ? true : false,
|
||||
'restrict_email' => $inv['restrict_email'] ? true : false,
|
||||
], 'Invito valido');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,52 @@ class OnboardingController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/onboarding/lookup-piva
|
||||
*
|
||||
* Lookup P.IVA pubblico senza autenticazione — usato dalla pagina register.html
|
||||
* per pre-compilare il nome azienda prima che l'utente abbia un account.
|
||||
*
|
||||
* Rate limiting soft: 10 req/min per IP (file-based).
|
||||
* Restituisce solo company_name e sector (nessun dato sensibile).
|
||||
*/
|
||||
public function lookupPiva(): void
|
||||
{
|
||||
// Soft rate limiting (no auth)
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$ip = trim(explode(',', $ip)[0]);
|
||||
RateLimitService::check("piva_lookup:{$ip}", [['max' => 10, 'window_seconds' => 60]]);
|
||||
RateLimitService::increment("piva_lookup:{$ip}");
|
||||
|
||||
$vatNumber = trim($this->getParam('vat_number', ''));
|
||||
$vatNumber = preg_replace('/^IT/i', '', $vatNumber);
|
||||
$vatNumber = preg_replace('/\s+/', '', $vatNumber);
|
||||
|
||||
if (!preg_match('/^\d{11}$/', $vatNumber)) {
|
||||
$this->jsonError('Formato P.IVA non valido (11 cifre)', 400, 'INVALID_VAT');
|
||||
}
|
||||
|
||||
try {
|
||||
$visuraService = new VisuraService();
|
||||
$data = $visuraService->fetchFromCertiSource($vatNumber);
|
||||
|
||||
if (empty($data) || empty($data['company_name'])) {
|
||||
$this->jsonError('Azienda non trovata', 404, 'COMPANY_NOT_FOUND');
|
||||
}
|
||||
|
||||
// Restituisce solo i campi necessari per il pre-fill del form
|
||||
$this->jsonSuccess([
|
||||
'company_name' => $data['company_name'],
|
||||
'sector' => $data['sector'] ?? null,
|
||||
'nis2_entity_type'=> $data['nis2_entity_type'] ?? null,
|
||||
], 'Azienda trovata');
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('[PIVA_LOOKUP_ERROR] ' . $e->getMessage());
|
||||
$this->jsonError('Errore nel recupero dati', 500, 'LOOKUP_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/onboarding/complete
|
||||
* Completa l'onboarding: crea organizzazione e aggiorna profilo utente
|
||||
|
||||
Reference in New Issue
Block a user