feat(ucp): NexusPBX v2.5.0 Portail Utilisateur isolé (Refs #4)

This commit is contained in:
sorcier committed 2026-11-11 18:47:25 +01:00
1 parent 7922e338e3
commit 72e43f1a9a
18 files changed
+3499 -62

No files matched your search

+342
View File
@@ -0,0 +1,342 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/Database.php';
require_once __DIR__ . '/../src/FirewallManager.php';
require_once __DIR__ . '/../src/AmiClient.php';
require_once __DIR__ . '/../src/ConfigGenerator.php';
require_once __DIR__ . '/../src/CdrManager.php';
require_once __DIR__ . '/../src/PhonebookManager.php';
require_once __DIR__ . '/../src/UcpAuth.php';
require_once __DIR__ . '/../src/UcpManager.php';
use App\Database;
use App\UcpAuth;
use App\UcpManager;
$action = trim((string)($_GET['action'] ?? $_POST['action'] ?? ''));
$isBinaryEndpoint = in_array($action, ['stream_voicemail', 'avatar'], true);
if (!$isBinaryEndpoint) {
header('Content-Type: application/json; charset=utf-8');
}
try {
$db = Database::getConnection();
UcpAuth::startSession();
if (!UcpAuth::isUcpEnabled($db)) {
http_response_code(503);
echo json_encode(['success' => false, 'message' => 'Le portail utilisateur UCP est actuellement désactivé par l’administrateur.']);
exit;
}
// Lecture du payload JSON si applicable
$payload = $_POST;
$contentType = (string)($_SERVER['CONTENT_TYPE'] ?? '');
if (str_contains($contentType, 'application/json')) {
$raw = file_get_contents('php://input');
$decoded = json_decode((string)$raw, true);
if (is_array($decoded)) {
$payload = $decoded;
}
}
// -------------------------------------------------------------------------
// 1. Authentification (routes publiques de session)
// -------------------------------------------------------------------------
if ($action === 'login') {
$username = (string)($payload['username'] ?? '');
$password = (string)($payload['password'] ?? '');
if (UcpAuth::login($db, $username, $password)) {
$user = UcpAuth::getCurrentUser($db);
echo json_encode([
'success' => true,
'username' => $user['username'] ?? $username,
'csrf_token' => UcpAuth::getCsrfToken(),
'message' => 'Connexion réussie.',
]);
} else {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Identifiant ou mot de passe incorrect.',
]);
}
exit;
}
if ($action === 'logout') {
UcpAuth::logout();
echo json_encode(['success' => true, 'message' => 'Déconnexion effectuée.']);
exit;
}
// -------------------------------------------------------------------------
// 2. Barrière d'authentification UCP obligatoire
// -------------------------------------------------------------------------
$currentUser = UcpAuth::getCurrentUser($db);
if (!$currentUser) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Session expirée ou non authentifiée.']);
exit;
}
// Protection CSRF sur toutes les actions modificatrices POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrfHeader = (string)($_SERVER['HTTP_X_CSRF_TOKEN'] ?? $payload['csrf_token'] ?? '');
if (!UcpAuth::verifyCsrfToken($csrfHeader)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Jeton de sécurité CSRF invalide. Rechargez la page.']);
exit;
}
}
$ucpUserId = (int)$currentUser['id'];
$allowedExts = UcpAuth::getAllowedExtensions($db, $ucpUserId);
switch ($action) {
case 'get_state':
$branding = $db->query('SELECT company_name, company_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(PDO::FETCH_ASSOC) ?: [];
echo json_encode([
'success' => true,
'data' => [
'user' => [
'id' => $ucpUserId,
'username' => (string)$currentUser['username'],
'display_name' => (string)$currentUser['display_name'],
'email' => (string)$currentUser['email'],
'last_login_at' => $currentUser['last_login_at'],
],
'branding' => [
'company_name' => (string)($branding['company_name'] ?? ''),
'company_logo' => (string)($branding['company_logo'] ?? ''),
],
'csrf_token' => UcpAuth::getCsrfToken(),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts),
'forward_targets' => UcpManager::getForwardTargets($db),
],
]);
break;
case 'save_call_features':
$ext = (string)($payload['extension'] ?? '');
UcpManager::setCallFeatures($db, $allowedExts, $ext, [
'dnd' => !empty($payload['dnd']),
'cf' => (string)($payload['cf'] ?? ''),
'cfu' => (string)($payload['cfu'] ?? ''),
]);
echo json_encode([
'success' => true,
'extensions' => UcpManager::getExtensionsState($db, $allowedExts),
'message' => "Paramètres d'appel du poste {$ext} appliqués en direct.",
]);
break;
case 'get_voicemails':
$ext = (string)($_GET['extension'] ?? $payload['extension'] ?? '');
if ($ext === '' && !empty($allowedExts)) {
$ext = (string)$allowedExts[0]['extension'];
}
$messages = $ext !== '' ? UcpManager::getVoicemails($allowedExts, $ext) : [];
echo json_encode([
'success' => true,
'messages' => $messages,
]);
break;
case 'stream_voicemail':
$ext = (string)($_GET['extension'] ?? '');
$folder = (string)($_GET['folder'] ?? '');
$msgId = (string)($_GET['id'] ?? '');
$download = !empty($_GET['download']);
$filePath = UcpManager::getVoicemailAudioPath($allowedExts, $ext, $folder, $msgId);
$size = filesize($filePath);
$fileExt = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$mime = ($fileExt === 'gsm') ? 'audio/gsm' : 'audio/wav';
while (ob_get_level()) {
ob_end_clean();
}
header("Content-Type: {$mime}");
header('Accept-Ranges: bytes');
header('Cache-Control: no-store, no-cache, must-revalidate');
if ($download) {
$dlName = "voicemail_{$ext}_{$msgId}.{$fileExt}";
header("Content-Disposition: attachment; filename=\"{$dlName}\"");
}
if (isset($_SERVER['HTTP_RANGE'])) {
[$param, $range] = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strtolower(trim($param)) === 'bytes') {
[$from, $to] = explode('-', $range, 2);
$from = intval($from);
$to = ($to === '') ? ($size - 1) : intval($to);
if ($from > $to || $from >= $size) {
http_response_code(416);
exit;
}
$length = $to - $from + 1;
http_response_code(206);
header("Content-Range: bytes {$from}-{$to}/{$size}");
header("Content-Length: {$length}");
$fp = fopen($filePath, 'rb');
fseek($fp, $from);
$remaining = $length;
while ($remaining > 0 && !feof($fp)) {
$chunk = min(8192, $remaining);
echo fread($fp, $chunk);
$remaining -= $chunk;
flush();
}
fclose($fp);
exit;
}
}
header("Content-Length: {$size}");
readfile($filePath);
exit;
case 'toggle_voicemail_read':
$ext = (string)($payload['extension'] ?? '');
$folder = (string)($payload['folder'] ?? '');
$msgId = (string)($payload['id'] ?? '');
UcpManager::moveVoicemailFolder($db, $allowedExts, $ext, $folder, $msgId);
echo json_encode([
'success' => true,
'messages' => UcpManager::getVoicemails($allowedExts, $ext),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts),
'message' => ($folder === 'INBOX') ? 'Message marqué comme lu (archivé).' : 'Message marqué comme non lu.',
]);
break;
case 'delete_voicemail':
$ext = (string)($payload['extension'] ?? '');
$folder = (string)($payload['folder'] ?? '');
$msgId = (string)($payload['id'] ?? '');
UcpManager::deleteVoicemail($db, $allowedExts, $ext, $folder, $msgId);
echo json_encode([
'success' => true,
'messages' => UcpManager::getVoicemails($allowedExts, $ext),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts),
'message' => 'Message vocal supprimé définitivement.',
]);
break;
case 'save_voicemail_prefs':
$ext = (string)($payload['extension'] ?? '');
$pin = (string)($payload['pin'] ?? '');
$email = (string)($payload['email'] ?? '');
UcpManager::updateVoicemailPrefs($db, $allowedExts, $ext, $pin, $email);
$freshAllowed = UcpAuth::getAllowedExtensions($db, $ucpUserId);
echo json_encode([
'success' => true,
'extensions' => UcpManager::getExtensionsState($db, $freshAllowed),
'message' => 'Préférences messagerie vocale enregistrées et appliquées.',
]);
break;
case 'get_personal_cdr':
$data = UcpManager::getPersonalCdr($db, $allowedExts, [
'direction' => $_GET['direction'] ?? 'all',
'disposition' => $_GET['disposition'] ?? 'all',
'period' => $_GET['period'] ?? 'all',
'search' => $_GET['search'] ?? '',
]);
echo json_encode([
'success' => true,
'data' => $data,
]);
break;
case 'get_phonebook':
echo json_encode([
'success' => true,
'contacts' => UcpManager::getPhonebookForUser($db, $allowedExts),
]);
break;
case 'save_my_email':
$ext = (string)($payload['extension'] ?? '');
$email = (string)($payload['email'] ?? '');
UcpManager::updateMyContactEmail($db, $allowedExts, $ext, $email);
echo json_encode([
'success' => true,
'contacts' => UcpManager::getPhonebookForUser($db, $allowedExts),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts),
'message' => "E-mail annuaire du poste {$ext} mis à jour.",
]);
break;
case 'upload_my_avatar':
$ext = (string)($_POST['extension'] ?? '');
if (empty($_FILES['avatar'])) {
throw new InvalidArgumentException("Aucun fichier image reçu.");
}
$avatarUrl = UcpManager::uploadMyAvatar($db, $allowedExts, $ext, $_FILES['avatar']);
echo json_encode([
'success' => true,
'photo_url' => $avatarUrl,
'contacts' => UcpManager::getPhonebookForUser($db, $allowedExts),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts),
'message' => "Photo de profil du poste {$ext} mise à jour.",
]);
break;
case 'avatar':
$file = (string)($_GET['file'] ?? '');
$path = UcpManager::resolveAvatarPath($file);
if (!$path) {
http_response_code(404);
exit;
}
$extName = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$mime = match ($extName) {
'png' => 'image/png',
'webp' => 'image/webp',
default => 'image/jpeg',
};
while (ob_get_level()) {
ob_end_clean();
}
header("Content-Type: {$mime}");
header('Cache-Control: public, max-age=3600');
readfile($path);
exit;
case 'change_password':
$cur = (string)($payload['current_password'] ?? '');
$new = (string)($payload['new_password'] ?? '');
UcpAuth::updateOwnPassword($db, $ucpUserId, $cur, $new);
echo json_encode([
'success' => true,
'message' => 'Mot de passe UCP modifié avec succès.',
]);
break;
default:
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Action UCP inconnue.']);
break;
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
if ($isBinaryEndpoint) {
header('Content-Type: text/plain; charset=utf-8');
echo $e->getMessage();
} else {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
} catch (\Throwable $e) {
http_response_code(500);
if ($isBinaryEndpoint) {
header('Content-Type: text/plain; charset=utf-8');
echo 'Erreur serveur.';
} else {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
}