nis2-agile/application/controllers/AuthController.php
DevEnv nis2-agile 7080695d06 [FEAT] Ruolo Consulente + Wizard Registrazione v2
- register.html: step 0 scelta profilo (Azienda / Consulente)
- onboarding.html: wizard 4-step con P.IVA obbligatoria (auto-fetch CertiSource)
- companies.html: nuova dashboard consulente con cards aziende e compliance score
- common.js: org-switcher sidebar + role labels corretti per consulente
- login.html: routing post-login (consulente → companies.html)
- api.js: isConsultant(), setUserRole(), register con user_type
- AuthController: user_type=consultant → role=consultant in users table
- OnboardingController: multi-org per consulente, duplicate VAT check
- 005_consultant_support.sql: aggiunge 'consultant' a user_organizations.role ENUM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 08:53:30 +01:00

294 lines
9.6 KiB
PHP

<?php
/**
* NIS2 Agile - Auth Controller
*
* Gestisce registrazione, login, JWT tokens, profilo utente.
*/
require_once __DIR__ . '/BaseController.php';
require_once APP_PATH . '/services/RateLimitService.php';
class AuthController extends BaseController
{
/**
* POST /api/auth/register
*/
public function register(): void
{
// Rate limiting
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
RateLimitService::check("register:{$ip}", RATE_LIMIT_AUTH_REGISTER);
RateLimitService::increment("register:{$ip}");
$this->validateRequired(['email', 'password', 'full_name']);
$email = strtolower(trim($this->getParam('email')));
$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';
// Validazione email
if (!$this->validateEmail($email)) {
$this->jsonError('Formato email non valido', 400, 'INVALID_EMAIL');
}
// Validazione password
$passwordErrors = $this->validatePassword($password);
if (!empty($passwordErrors)) {
$this->jsonError(
implode('. ', $passwordErrors),
400,
'WEAK_PASSWORD'
);
}
// Verifica email duplicata
$existing = Database::fetchOne('SELECT id FROM users WHERE email = ?', [$email]);
if ($existing) {
$this->jsonError('Email già registrata', 409, 'EMAIL_EXISTS');
}
// Crea utente
$userId = Database::insert('users', [
'email' => $email,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'full_name' => $fullName,
'phone' => $phone,
'role' => $role,
'is_active' => 1,
]);
// Genera tokens
$accessToken = $this->generateJWT($userId);
$refreshToken = $this->generateRefreshToken($userId);
// Audit log
$this->currentUser = ['id' => $userId];
$this->logAudit('user_registered', 'user', $userId, ['user_type' => $userType]);
$this->jsonSuccess([
'user' => [
'id' => $userId,
'email' => $email,
'full_name' => $fullName,
'role' => $role,
],
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => JWT_EXPIRES_IN,
], 'Registrazione completata', 201);
}
/**
* POST /api/auth/login
*/
public function login(): void
{
// Rate limiting
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
RateLimitService::check("login:{$ip}", RATE_LIMIT_AUTH_LOGIN);
RateLimitService::increment("login:{$ip}");
$this->validateRequired(['email', 'password']);
$email = strtolower(trim($this->getParam('email')));
$password = $this->getParam('password');
// Trova utente
$user = Database::fetchOne(
'SELECT * FROM users WHERE email = ? AND is_active = 1',
[$email]
);
if (!$user || !password_verify($password, $user['password_hash'])) {
$this->jsonError('Credenziali non valide', 401, 'INVALID_CREDENTIALS');
}
// Aggiorna ultimo login
Database::update('users', [
'last_login_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$user['id']]);
// Genera tokens
$accessToken = $this->generateJWT((int) $user['id']);
$refreshToken = $this->generateRefreshToken((int) $user['id']);
// Carica organizzazioni
$organizations = Database::fetchAll(
'SELECT uo.organization_id, uo.role, uo.is_primary, o.name, o.sector, o.entity_type
FROM user_organizations uo
JOIN organizations o ON o.id = uo.organization_id
WHERE uo.user_id = ? AND o.is_active = 1',
[$user['id']]
);
$this->currentUser = $user;
$this->logAudit('user_login', 'user', (int) $user['id']);
$this->jsonSuccess([
'user' => [
'id' => (int) $user['id'],
'email' => $user['email'],
'full_name' => $user['full_name'],
'role' => $user['role'],
'preferred_language' => $user['preferred_language'],
],
'organizations' => $organizations,
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => JWT_EXPIRES_IN,
], 'Login effettuato');
}
/**
* POST /api/auth/logout
*/
public function logout(): void
{
$this->requireAuth();
// Invalida tutti i refresh token dell'utente
Database::delete('refresh_tokens', 'user_id = ?', [$this->getCurrentUserId()]);
$this->logAudit('user_logout', 'user', $this->getCurrentUserId());
$this->jsonSuccess(null, 'Logout effettuato');
}
/**
* POST /api/auth/refresh
*/
public function refresh(): void
{
$this->validateRequired(['refresh_token']);
$refreshToken = $this->getParam('refresh_token');
$hashedToken = hash('sha256', $refreshToken);
// Verifica refresh token
$tokenRecord = Database::fetchOne(
'SELECT * FROM refresh_tokens WHERE token = ? AND expires_at > NOW()',
[$hashedToken]
);
if (!$tokenRecord) {
$this->jsonError('Refresh token non valido o scaduto', 401, 'INVALID_REFRESH_TOKEN');
}
// Elimina vecchio token
Database::delete('refresh_tokens', 'id = ?', [$tokenRecord['id']]);
// Genera nuovi tokens
$userId = (int) $tokenRecord['user_id'];
$accessToken = $this->generateJWT($userId);
$newRefreshToken = $this->generateRefreshToken($userId);
$this->jsonSuccess([
'access_token' => $accessToken,
'refresh_token' => $newRefreshToken,
'expires_in' => JWT_EXPIRES_IN,
], 'Token rinnovato');
}
/**
* GET /api/auth/me
*/
public function me(): void
{
$this->requireAuth();
$user = $this->getCurrentUser();
// Carica organizzazioni
$organizations = Database::fetchAll(
'SELECT uo.organization_id, uo.role as org_role, uo.is_primary,
o.name, o.sector, o.entity_type, o.subscription_plan
FROM user_organizations uo
JOIN organizations o ON o.id = uo.organization_id
WHERE uo.user_id = ? AND o.is_active = 1',
[$user['id']]
);
$this->jsonSuccess([
'id' => (int) $user['id'],
'email' => $user['email'],
'full_name' => $user['full_name'],
'phone' => $user['phone'],
'role' => $user['role'],
'preferred_language' => $user['preferred_language'],
'last_login_at' => $user['last_login_at'],
'created_at' => $user['created_at'],
'organizations' => $organizations,
]);
}
/**
* PUT /api/auth/profile
*/
public function updateProfile(): void
{
$this->requireAuth();
$updates = [];
if ($this->hasParam('full_name')) {
$updates['full_name'] = trim($this->getParam('full_name'));
}
if ($this->hasParam('phone')) {
$updates['phone'] = $this->getParam('phone');
}
if ($this->hasParam('preferred_language')) {
$lang = $this->getParam('preferred_language');
if (in_array($lang, ['it', 'en', 'fr', 'de'])) {
$updates['preferred_language'] = $lang;
}
}
if (empty($updates)) {
$this->jsonError('Nessun campo da aggiornare', 400, 'NO_UPDATES');
}
Database::update('users', $updates, 'id = ?', [$this->getCurrentUserId()]);
$this->logAudit('profile_updated', 'user', $this->getCurrentUserId(), $updates);
$this->jsonSuccess($updates, 'Profilo aggiornato');
}
/**
* POST /api/auth/change-password
*/
public function changePassword(): void
{
$this->requireAuth();
$this->validateRequired(['current_password', 'new_password']);
$currentPassword = $this->getParam('current_password');
$newPassword = $this->getParam('new_password');
// Verifica password attuale
if (!password_verify($currentPassword, $this->currentUser['password_hash'])) {
$this->jsonError('Password attuale non corretta', 400, 'WRONG_PASSWORD');
}
// Validazione nuova password
$errors = $this->validatePassword($newPassword);
if (!empty($errors)) {
$this->jsonError(implode('. ', $errors), 400, 'WEAK_PASSWORD');
}
Database::update('users', [
'password_hash' => password_hash($newPassword, PASSWORD_DEFAULT),
], 'id = ?', [$this->getCurrentUserId()]);
// Invalida tutti i refresh token (force re-login)
Database::delete('refresh_tokens', 'user_id = ?', [$this->getCurrentUserId()]);
$this->logAudit('password_changed', 'user', $this->getCurrentUserId());
$this->jsonSuccess(null, 'Password modificata. Effettua nuovamente il login.');
}
}