bp/app/Controllers/Profile.php

379 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Controllers;
use App\Models\UserModel;
use App\Models\OrganizationModel;
use App\Models\OrganizationUserModel;
use App\Services\AccessService;
/**
* ProfileController - Управление профилем пользователя
*/
class Profile extends BaseController
{
protected UserModel $userModel;
protected OrganizationModel $orgModel;
public function __construct()
{
$this->userModel = new UserModel();
$this->orgModel = new OrganizationModel();
}
/**
* Главная страница профиля
*/
public function index()
{
$userId = $this->getCurrentUserId();
$user = $this->userModel->find($userId);
return $this->renderTwig('profile/index', [
'title' => 'Профиль',
'user' => $user,
'active_tab' => 'general',
]);
}
/**
* Вкладка "Организации"
*/
public function organizations()
{
$userId = $this->getCurrentUserId();
$user = $this->userModel->find($userId);
$currentOrgId = $this->session->get('active_org_id');
// Получаем все организации пользователя
$orgUserModel = $this->getOrgUserModel();
$memberships = $orgUserModel->where('user_id', $userId)->findAll();
$orgIds = array_column($memberships, 'organization_id');
$organizations = [];
if (!empty($orgIds)) {
$organizations = $this->orgModel->whereIn('id', $orgIds)->findAll();
}
// Объединяем данные
$orgList = [];
foreach ($organizations as $org) {
// Находим соответствующий membership
$membership = null;
foreach ($memberships as $m) {
if ($m['organization_id'] == $org['id']) {
$membership = $m;
break;
}
}
$orgList[] = [
'id' => $org['id'],
'name' => $org['name'],
'type' => $org['type'],
'role' => $membership['role'] ?? 'guest',
'status' => $membership['status'] ?? 'active',
'joined_at' => $membership['joined_at'] ?? null,
'is_owner' => ($membership['role'] ?? '') === 'owner',
'is_current_org' => ((int) $org['id'] === (int) $currentOrgId),
];
}
return $this->renderTwig('profile/organizations', [
'title' => 'Мои организации',
'user' => $user,
'organizations' => $orgList,
'active_tab' => 'organizations',
]);
}
/**
* Вкладка "Безопасность"
*/
public function security()
{
$userId = $this->getCurrentUserId();
$user = $this->userModel->find($userId);
// Получаем список активных сессий
$sessions = $this->getUserSessions($userId);
return $this->renderTwig('profile/security', [
'title' => 'Безопасность',
'user' => $user,
'active_tab' => 'security',
'sessions' => $sessions,
'currentSessionId' => session_id(),
]);
}
/**
* Получение списка активных сессий пользователя
*/
protected function getUserSessions(int $userId): array
{
$db = \Config\Database::connect();
// Получаем сессии из таблицы ci_sessions (требует настройки DatabaseHandler)
// Также получаем remember-токены
$rememberTokens = $db->table('remember_tokens')
->where('user_id', $userId)
->where('expires_at >', date('Y-m-d H:i:s'))
->get()
->getResultArray();
$sessions = [];
// Добавляем remember-токены как сессии
foreach ($rememberTokens as $token) {
$sessions[] = [
'id' => 'remember_' . $token['id'],
'type' => 'remember',
'device' => $this->parseUserAgent($token['user_agent'] ?? ''),
'ip_address' => $token['ip_address'] ?? 'Unknown',
'created_at' => $token['created_at'],
'expires_at' => $token['expires_at'],
'is_current' => false,
];
}
return $sessions;
}
/**
* Парсинг User Agent для получения информации об устройстве
*/
protected function parseUserAgent(string $userAgent): string
{
if (empty($userAgent)) {
return 'Неизвестное устройство';
}
// Определяем браузер
$browser = 'Unknown';
if (preg_match('/Firefox\/([0-9.]+)/', $userAgent, $matches)) {
$browser = 'Firefox';
} elseif (preg_match('/Chrome\/([0-9.]+)/', $userAgent, $matches)) {
$browser = 'Chrome';
} elseif (preg_match('/Safari\/([0-9.]+)/', $userAgent, $matches)) {
$browser = 'Safari';
} elseif (preg_match('/MSIE\s+([0-9.]+)/', $userAgent, $matches) || preg_match('/Trident\/([0-9.]+)/', $userAgent, $matches)) {
$browser = 'Internet Explorer';
} elseif (preg_match('/Edg\/([0-9.]+)/', $userAgent, $matches)) {
$browser = 'Edge';
}
// Определяем ОС
$os = 'Unknown OS';
if (preg_match('/Windows/', $userAgent)) {
$os = 'Windows';
} elseif (preg_match('/Mac OS X/', $userAgent)) {
$os = 'macOS';
} elseif (preg_match('/Linux/', $userAgent)) {
$os = 'Linux';
} elseif (preg_match('/Android/', $userAgent)) {
$os = 'Android';
} elseif (preg_match('/iPhone|iPad|iPod/', $userAgent)) {
$os = 'iOS';
}
return "{$browser} на {$os}";
}
/**
* Завершение конкретной сессии
*/
public function revokeSession()
{
$userId = $this->getCurrentUserId();
$sessionId = $this->request->getPost('session_id');
if (empty($sessionId)) {
return redirect()->to('/profile/security')->with('error', 'Сессия не найдена');
}
$db = \Config\Database::connect();
// Проверяем, что сессия принадлежит пользователю
if (strpos($sessionId, 'remember_') === 0) {
// Это remember-токен
$tokenId = (int) str_replace('remember_', '', $sessionId);
$token = $db->table('remember_tokens')
->where('id', $tokenId)
->where('user_id', $userId)
->get()
->getRowArray();
if ($token) {
$db->table('remember_tokens')->where('id', $tokenId)->delete();
log_message('info', "User {$userId} revoked remember token {$tokenId}");
}
}
return redirect()->to('/profile/security')->with('success', 'Сессия завершена');
}
/**
* Завершение всех сессий (кроме текущей)
*/
public function revokeAllSessions()
{
$userId = $this->getCurrentUserId();
// Удаляем все remember-токены
$db = \Config\Database::connect();
$db->table('remember_tokens')->where('user_id', $userId)->delete();
// Регенерируем текущую сессию
$this->session->regenerate(true);
log_message('info', "User {$userId} revoked all sessions");
return redirect()->to('/profile/security')->with(
'success',
'Все сессии на других устройствах завершены. Вы остались авторизованы на текущем устройстве.'
);
}
/**
* Обновление имени пользователя
*/
public function updateName()
{
$userId = $this->getCurrentUserId();
$name = trim($this->request->getPost('name'));
if (empty($name)) {
$this->session->setFlashdata('error', 'Имя обязательно для заполнения');
return redirect()->to('/profile');
}
if (strlen($name) < 3) {
$this->session->setFlashdata('error', 'Имя должно содержать минимум 3 символа');
return redirect()->to('/profile');
}
$this->userModel->update($userId, ['name' => $name]);
$this->session->set('name', $name);
$this->session->setFlashdata('success', 'Имя успешно обновлено');
return redirect()->to('/profile');
}
/**
* Загрузка аватара
*/
public function uploadAvatar()
{
$userId = $this->getCurrentUserId();
$file = $this->request->getFile('avatar');
if (!$file || !$file->isValid()) {
$this->session->setFlashdata('error', 'Ошибка загрузки файла');
return redirect()->to('/profile');
}
// Валидация
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$maxSize = 2 * 1024 * 1024; // 2MB
if (!in_array($file->getMimeType(), $allowedTypes)) {
$this->session->setFlashdata('error', 'Разрешены только файлы JPG, PNG и GIF');
return redirect()->to('/profile');
}
if ($file->getSize() > $maxSize) {
$this->session->setFlashdata('error', 'Максимальный размер файла - 2 МБ');
return redirect()->to('/profile');
}
// Создаём директорию для аватаров если нет
$uploadPath = ROOTPATH . 'public/uploads/avatars';
if (!is_dir($uploadPath)) {
mkdir($uploadPath, 0755, true);
}
// Генерируем уникальное имя файла
$extension = $file->getClientExtension();
$newFileName = 'avatar_' . $userId . '_' . time() . '.' . $extension;
// Перемещаем файл
$file->move($uploadPath, $newFileName);
// Удаляем старый аватар если был
$user = $this->userModel->find($userId);
if (!empty($user['avatar']) && file_exists($uploadPath . '/' . $user['avatar'])) {
@unlink($uploadPath . '/' . $user['avatar']);
}
// Обновляем путь к аватару в базе
$this->userModel->update($userId, ['avatar' => $newFileName]);
$this->session->setFlashdata('success', 'Аватар успешно загружен');
return redirect()->to('/profile');
}
/**
* Смена пароля
*/
public function changePassword()
{
$userId = $this->getCurrentUserId();
$user = $this->userModel->find($userId);
$currentPassword = $this->request->getPost('current_password');
$newPassword = $this->request->getPost('new_password');
$confirmPassword = $this->request->getPost('confirm_password');
// Валидация
if (empty($currentPassword)) {
$this->session->setFlashdata('error', 'Введите текущий пароль');
return redirect()->to('/profile/security');
}
if (empty($newPassword)) {
$this->session->setFlashdata('error', 'Введите новый пароль');
return redirect()->to('/profile/security');
}
if (strlen($newPassword) < 6) {
$this->session->setFlashdata('error', 'Новый пароль должен содержать минимум 6 символов');
return redirect()->to('/profile/security');
}
if ($newPassword !== $confirmPassword) {
$this->session->setFlashdata('error', 'Пароли не совпадают');
return redirect()->to('/profile/security');
}
// Проверяем текущий пароль
if (!password_verify($currentPassword, $user['password'])) {
$this->session->setFlashdata('error', 'Неверный текущий пароль');
return redirect()->to('/profile/security');
}
// Обновляем пароль
$this->userModel->update($userId, ['password' => $newPassword]);
// Завершаем все сессии пользователя (кроме текущей)
$this->endAllUserSessions($userId);
$this->session->setFlashdata('success', 'Пароль успешно изменён. Для безопасности вы будете разлогинены на всех устройствах.');
return redirect()->to('/logout');
}
/**
* Завершение всех сессий пользователя
*/
private function endAllUserSessions(int $userId): void
{
// Удаляем все remember-токены пользователя
$db = \Config\Database::connect();
$db->table('remember_tokens')->where('user_id', $userId)->delete();
// Регенерируем ID текущей сессии
$this->session->regenerate(true);
}
}