Files
ave-cms/adminx/modules/Users/Controller.php
T
2026-08-05 21:40:21 +03:00

345 lines
12 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/Users/Controller.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Adminx\Users;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\AdminAssets;
use App\Common\AuditLog;
use App\Common\Auth;
use App\Common\Controller as BaseController;
use App\Common\Permission;
use App\Adminx\Support\BulkActionExecutor;
use App\Adminx\Support\Roles;
use App\Helpers\Request;
/**
* CRUD пользователей. Список — server-render (таблица AdminKit), создание/правка —
* ajax из drawer-формы по единому контракту success/error (§3).
*/
class Controller extends BaseController
{
/** GET /users */
public function index(array $params = [])
{
if (!Permission::check('view_users')) {
return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403);
}
AdminAssets::addStyle($this->base() . '/modules/Users/assets/users.css', 50);
AdminAssets::addScript($this->base() . '/modules/Users/assets/users.js', 50);
$search = Request::getStr('q');
$role = Request::getStr('role');
return $this->render('@users/list.twig', [
'users' => Model::all($search, $role),
'roles' => Roles::map(),
'stats' => Model::stats(),
'q' => $search,
'role' => $role,
'can_manage' => Permission::check('manage_users'),
'current_id' => Auth::id(),
]);
}
/** GET /users/{id} — данные пользователя (JSON, для формы правки). */
public function show(array $params = [])
{
if (!Permission::check('view_users')) {
return $this->error('Недостаточно прав', array(), 403);
}
$user = Model::find($params['id'] ?? 0);
if (!$user) {
return $this->error('Пользователь не найден', [], 404);
}
return $this->success('', ['data' => [
'id' => (int) $user->id,
'name' => $user->name,
'email' => $user->email,
'login' => $user->login,
'phone' => $user->phone,
'role' => $user->role,
'is_active' => (int) $user->is_active,
'must_change_password' => (int) $user->must_change_password,
'password_changed_at' => $user->password_changed_at,
'created_at' => $user->created_at,
'updated_at' => $user->updated_at,
]]);
}
/** POST /users — создать. */
public function store(array $params = [])
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
$data = $this->input();
$errors = $this->validate($data, 0, true);
if ($errors) {
return $this->error('Проверьте поля формы', $errors);
}
$id = Model::create($data);
AuditLog::record('user.created', array(
'actor_id' => Auth::id(), 'target_type' => 'user', 'target_id' => $id,
));
return $this->success('Пользователь создан', ['redirect' => $this->base() . '/users']);
}
/** POST /users/{id} — обновить. */
public function update(array $params = [])
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
$id = (int) ($params['id'] ?? 0);
$user = Model::find($id);
if (!$user) {
return $this->error('Пользователь не найден', [], 404);
}
$data = $this->input();
if ($id === Auth::id()) {
$data['role'] = (string) $user->role;
$data['is_active'] = true;
}
$errors = $this->validate($data, $id, false);
if ($errors) {
return $this->error('Проверьте поля формы', $errors);
}
Model::update($id, $data);
AuditLog::record('user.updated', array(
'actor_id' => Auth::id(), 'target_type' => 'user', 'target_id' => $id,
'meta' => array(
'password_changed' => $data['password'] !== '',
'must_change_password' => !empty($data['must_change_password']),
),
));
return $this->success('Изменения сохранены', ['redirect' => $this->base() . '/users']);
}
/** POST /users/{id}/toggle — вкл/выкл активность. */
public function toggle(array $params = [])
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
$id = (int) ($params['id'] ?? 0);
if (!Model::find($id)) {
return $this->error('Пользователь не найден', [], 404);
}
if ($id === Auth::id()) {
return $this->error('Нельзя отключить собственную учётную запись', [], 422);
}
$active = Model::toggle($id);
AuditLog::record('user.status_updated', array(
'actor_id' => Auth::id(), 'target_type' => 'user', 'target_id' => $id,
'meta' => array('is_active' => $active),
));
return $this->success($active ? 'Пользователь включён' : 'Пользователь отключён', [
'data' => ['is_active' => $active],
]);
}
/** POST /users/{id}/delete — удалить. */
public function destroy(array $params = [])
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
$id = (int) ($params['id'] ?? 0);
if (!Model::find($id)) {
return $this->error('Пользователь не найден', [], 404);
}
if ($id === Auth::id()) {
return $this->error('Нельзя удалить самого себя', [], 422);
}
Model::delete($id);
AuditLog::record('user.deleted', array(
'actor_id' => Auth::id(), 'target_type' => 'user', 'target_id' => $id,
));
return $this->success('Пользователь удалён', ['redirect' => $this->base() . '/users']);
}
/** GET /users/{id}/security — active browser sessions and failed logins. */
public function security(array $params = array())
{
if (!Permission::check('manage_users')) {
return $this->error('Недостаточно прав', array(), 403);
}
$id = (int) (isset($params['id']) ? $params['id'] : 0);
$user = Model::find($id);
if (!$user) { return $this->error('Пользователь не найден', array(), 404); }
$data = Model::security($id);
$data['password_changed_at'] = isset($user->password_changed_at) ? (string) $user->password_changed_at : '';
$data['must_change_password'] = !empty($user->must_change_password);
$data['is_self'] = $id === (int) Auth::id();
return $this->success('', array('data' => $data));
}
/** POST /users/{id}/sessions/{session}/revoke. */
public function revokeSession(array $params = array())
{
if (($response = $this->guard()) !== null) { return $response; }
$id = (int) (isset($params['id']) ? $params['id'] : 0);
$result = Model::revokeSession($id, isset($params['session']) ? $params['session'] : 0);
if (empty($result['removed'])) { return $this->error('Сессия не найдена', array(), 404); }
AuditLog::record('user.session_revoked', array(
'actor_id' => Auth::id(), 'target_type' => 'user', 'target_id' => $id,
'meta' => array('session_id' => (int) $params['session']),
));
if (!empty($result['current'])) {
Auth::logout();
return $this->success('Текущая сессия завершена', array('redirect' => $this->base() . '/login'));
}
return $this->success('Сессия завершена');
}
/** POST /users/{id}/sessions/revoke-others. */
public function revokeOtherSessions(array $params = array())
{
if (($response = $this->guard()) !== null) { return $response; }
$id = (int) (isset($params['id']) ? $params['id'] : 0);
if (!Model::find($id)) { return $this->error('Пользователь не найден', array(), 404); }
$preserve = $id === (int) Auth::id() ? Auth::currentBrowserTokenHash() : '';
$count = Model::revokeSessions($id, $preserve);
AuditLog::record('user.sessions_revoked', array(
'actor_id' => Auth::id(), 'target_type' => 'user', 'target_id' => $id,
'meta' => array('count' => $count, 'current_preserved' => $preserve !== ''),
));
return $this->success($preserve !== '' ? 'Остальные сессии завершены' : 'Все сессии завершены', array(
'data' => array('count' => $count),
));
}
public function bulk(array $params = array())
{
if (($response = $this->guard()) !== null) {
return $response;
}
$action = Request::postStr('action', '');
$currentId = (int) Auth::id();
$handler = function ($id) use ($action, $currentId) {
if ((int) $id === $currentId || !Model::find($id)) {
return false;
}
Model::setActive($id, $action === 'activate');
return true;
};
try {
$result = BulkActionExecutor::execute($action, Request::post('ids', array()), array(
'activate' => $handler,
'deactivate' => $handler,
), 200);
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), array(), 422);
}
AuditLog::record('user.bulk_status_updated', array(
'actor_id' => Auth::id(),
'target_type' => 'user',
'meta' => array(
'action' => $action,
'requested' => $result['requested'],
'done' => $result['done'],
'skipped' => $result['skipped'],
),
));
return $this->success('Состояние пользователей изменено', array('data' => $result));
}
// ------------------------------------------------------------------ //
/** CSRF + право на изменение. */
protected function guard()
{
return $this->guardPermission('manage_users');
}
protected function input()
{
return [
'name' => trim(Request::postStr('name')),
'email' => trim(Request::postStr('email')),
'login' => trim(Request::postStr('login')),
'phone' => trim(Request::postStr('phone')),
'role' => Request::postStr('role'),
'password' => (string) Request::post('password', ''),
'is_active' => Request::postBool('is_active', false),
'must_change_password' => Request::postBool('must_change_password', false),
];
}
/** @return array field => message */
protected function validate(array $data, $id, $isCreate)
{
$errors = [];
if ($data['name'] === '') {
$errors['name'] = 'Укажите имя.';
}
if ($data['email'] === '') {
$errors['email'] = 'Укажите email.';
} elseif (!preg_match('/^[^@\s]+@[^@\s]+$/', $data['email'])) {
//-- Допускаем локальные адреса вида user@local (демо/дев), которые
//-- FILTER_VALIDATE_EMAIL отвергает из-за отсутствия TLD.
$errors['email'] = 'Некорректный email.';
} elseif (Model::emailExists($data['email'], (int) $id)) {
$errors['email'] = 'Этот email уже занят.';
}
if ($data['login'] !== '' && Model::loginExists($data['login'], (int) $id)) {
$errors['login'] = 'Этот логин уже занят.';
}
if (!in_array($data['role'], Roles::codes(), true)) {
$errors['role'] = 'Выберите роль.';
}
if ($isCreate && mb_strlen($data['password'], 'UTF-8') < 8) {
$errors['password'] = 'Пароль не короче 8 символов.';
} elseif (!$isCreate && $data['password'] !== '' && mb_strlen($data['password'], 'UTF-8') < 8) {
$errors['password'] = 'Пароль не короче 8 символов.';
}
return $errors;
}
}