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

+196
View File
@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace App;
use PDO;
use InvalidArgumentException;
class UcpAuth
{
public const SESSION_NAME = 'NEXUS_UCP_SESS';
public static function startSession(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_name(self::SESSION_NAME);
session_start([
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'cookie_path' => '/',
]);
}
if (empty($_SESSION['ucp_csrf'])) {
$_SESSION['ucp_csrf'] = bin2hex(random_bytes(24));
}
}
public static function getCsrfToken(): string
{
self::startSession();
return (string)$_SESSION['ucp_csrf'];
}
public static function verifyCsrfToken(string $token): bool
{
self::startSession();
$expected = (string)($_SESSION['ucp_csrf'] ?? '');
return $expected !== '' && hash_equals($expected, $token);
}
public static function isUcpEnabled(PDO $db): bool
{
try {
$val = $db->query('SELECT ucp_enabled FROM settings WHERE id = 1')->fetchColumn();
return $val === false ? true : ((int)$val === 1);
} catch (\Throwable) {
return true;
}
}
public static function getCurrentUser(PDO $db): ?array
{
self::startSession();
if (empty($_SESSION['ucp_user_id']) || !self::isUcpEnabled($db)) {
return null;
}
$stmt = $db->prepare('
SELECT id, username, display_name, email, extensions_json, enabled, last_login_at, last_login_ip
FROM ucp_users
WHERE id = :id
LIMIT 1
');
$stmt->execute([':id' => (int)$_SESSION['ucp_user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || (int)$user['enabled'] !== 1) {
self::logout();
return null;
}
return $user;
}
public static function isAuthenticated(PDO $db): bool
{
return self::getCurrentUser($db) !== null;
}
public static function login(PDO $db, string $username, string $password): bool
{
self::startSession();
if (!self::isUcpEnabled($db)) {
return false;
}
$cleanUser = trim($username);
$stmt = $db->prepare('
SELECT id, username, display_name, password_hash, enabled
FROM ucp_users
WHERE username = :u
LIMIT 1
');
$stmt->execute([':u' => $cleanUser]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && (int)$user['enabled'] === 1 && password_verify($password, (string)$user['password_hash'])) {
session_regenerate_id(true);
$_SESSION['ucp_user_id'] = (int)$user['id'];
$_SESSION['ucp_username'] = (string)$user['username'];
if (empty($_SESSION['ucp_csrf'])) {
$_SESSION['ucp_csrf'] = bin2hex(random_bytes(24));
}
$ip = FirewallManager::resolveClientIp();
$db->prepare('UPDATE ucp_users SET last_login_at = CURRENT_TIMESTAMP, last_login_ip = :ip WHERE id = :id')
->execute([':ip' => $ip, ':id' => (int)$user['id']]);
return true;
}
self::logAuthFailure($cleanUser);
return false;
}
public static function logAuthFailure(string $username): void
{
$clientIp = FirewallManager::resolveClientIp();
$safeUser = preg_replace('/[^a-zA-Z0-9._@\-]/', '', $username) ?: 'vide';
$logMsg = date('[Y-m-d H:i:s] ') . "[NEXUS-UCP-AUTH-FAIL]: {$clientIp} (username: {$safeUser})\n";
@file_put_contents(FirewallManager::UCP_SECURITY_LOG, $logMsg, FILE_APPEND | LOCK_EX);
@chmod(FirewallManager::UCP_SECURITY_LOG, 0664);
}
public static function logout(): void
{
self::startSession();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
session_destroy();
}
/**
* Retourne la liste fraîche des extensions autorisées et actives pour un utilisateur UCP.
*/
public static function getAllowedExtensions(PDO $db, int $ucpUserId): array
{
$stmt = $db->prepare('SELECT extensions_json, enabled FROM ucp_users WHERE id = :id LIMIT 1');
$stmt->execute([':id' => $ucpUserId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || (int)$row['enabled'] !== 1) {
return [];
}
$linked = json_decode((string)($row['extensions_json'] ?? '[]'), true);
if (!is_array($linked) || empty($linked)) {
return [];
}
$linkedSet = array_values(array_unique(array_map('strval', $linked)));
$extRows = $db->query('
SELECT id, extension, name, transport, encryption, voicemail_enabled, voicemail_email, voicemail_pin, ring_time
FROM extensions
WHERE enabled = 1
ORDER BY extension ASC
')->fetchAll(PDO::FETCH_ASSOC) ?: [];
$allowed = [];
foreach ($extRows as $ext) {
if (in_array((string)$ext['extension'], $linkedSet, true)) {
$allowed[] = $ext;
}
}
return $allowed;
}
public static function updateOwnPassword(PDO $db, int $ucpUserId, string $currentPassword, string $newPassword): void
{
if (strlen($newPassword) < 6) {
throw new InvalidArgumentException("Le nouveau mot de passe doit contenir au moins 6 caractères.");
}
$stmt = $db->prepare('SELECT password_hash FROM ucp_users WHERE id = :id LIMIT 1');
$stmt->execute([':id' => $ucpUserId]);
$hash = $stmt->fetchColumn();
if (!$hash || !password_verify($currentPassword, (string)$hash)) {
throw new InvalidArgumentException("Le mot de passe actuel est incorrect.");
}
$db->prepare('UPDATE ucp_users SET password_hash = :p, updated_at = CURRENT_TIMESTAMP WHERE id = :id')
->execute([
':p' => password_hash($newPassword, PASSWORD_DEFAULT),
':id' => $ucpUserId,
]);
}
}