506 lines
18 KiB
PHP
506 lines
18 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\admin\controller\auth;
|
||
|
||
use ba\Random;
|
||
use Throwable;
|
||
use support\think\Db;
|
||
use support\validation\Validator;
|
||
use support\validation\ValidationException;
|
||
use app\common\controller\Backend;
|
||
use app\admin\model\Admin as AdminModel;
|
||
use support\Response;
|
||
use Webman\Http\Request;
|
||
|
||
class Admin extends Backend
|
||
{
|
||
protected ?object $model = null;
|
||
|
||
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip'];
|
||
|
||
protected array|string $quickSearchField = ['username', 'nickname'];
|
||
|
||
protected string|int|bool $dataLimit = 'parent';
|
||
|
||
protected string $dataLimitField = 'id';
|
||
|
||
protected function initController(Request $request): ?Response
|
||
{
|
||
$this->model = new AdminModel();
|
||
return null;
|
||
}
|
||
|
||
public function index(Request $request): Response
|
||
{
|
||
$response = $this->initializeBackend($request);
|
||
if ($response !== null) return $response;
|
||
|
||
if ($request->get('select') ?? $request->post('select')) {
|
||
$selectRes = $this->select($request);
|
||
if ($selectRes !== null) return $selectRes;
|
||
}
|
||
|
||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||
$query = $this->model
|
||
->withoutField('login_failure,password,salt')
|
||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||
->alias($alias)
|
||
->where($where);
|
||
|
||
// 仅返回“顶级角色组(pid=0)”下的管理员(用于远程下拉等场景)
|
||
$topGroup = $request->get('top_group') ?? $request->post('top_group');
|
||
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
|
||
$query = $query
|
||
->join('admin_group_access aga', $alias['admin'] . '.id = aga.uid')
|
||
->join('admin_group ag', 'aga.group_id = ag.id')
|
||
->where('ag.pid', 0)
|
||
->distinct(true);
|
||
}
|
||
|
||
$res = $query
|
||
->order($order)
|
||
->paginate($limit);
|
||
$items = $res->items();
|
||
return $this->success('', [
|
||
'list' => $items,
|
||
'total' => $res->total(),
|
||
'remark' => get_route_remark(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 远程下拉(重写:支持 top_group=1 仅返回顶级组管理员)
|
||
*/
|
||
protected function _select(): Response
|
||
{
|
||
if (empty($this->model)) {
|
||
return $this->success('', [
|
||
'list' => [],
|
||
'total' => 0,
|
||
]);
|
||
}
|
||
|
||
$pk = $this->model->getPk();
|
||
|
||
$fields = [$pk];
|
||
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', (string) $this->quickSearchField);
|
||
foreach ($quickSearchArr as $f) {
|
||
$f = trim((string) $f);
|
||
if ($f === '') continue;
|
||
$f = str_contains($f, '.') ? substr($f, strrpos($f, '.') + 1) : $f;
|
||
if ($f !== '' && !in_array($f, $fields, true)) {
|
||
$fields[] = $f;
|
||
}
|
||
}
|
||
|
||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||
$modelTable = strtolower($this->model->getTable());
|
||
$mainAlias = ($alias[$modelTable] ?? $modelTable) . '.';
|
||
|
||
// 联表时避免字段歧义:主表字段统一 select 为 "admin.xxx as xxx"
|
||
$selectFields = [];
|
||
foreach ($fields as $f) {
|
||
$f = trim((string) $f);
|
||
if ($f === '') continue;
|
||
$selectFields[] = $mainAlias . $f . ' as ' . $f;
|
||
}
|
||
|
||
// 联表时避免排序字段歧义:无前缀的字段默认加主表前缀
|
||
$qualifiedOrder = [];
|
||
if (is_array($order)) {
|
||
foreach ($order as $k => $v) {
|
||
$k = (string) $k;
|
||
$qualifiedOrder[str_contains($k, '.') ? $k : ($mainAlias . $k)] = $v;
|
||
}
|
||
}
|
||
|
||
$query = $this->model
|
||
->field($selectFields)
|
||
->alias($alias)
|
||
->where($where);
|
||
|
||
$topGroup = $this->request ? ($this->request->get('top_group') ?? $this->request->post('top_group')) : null;
|
||
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
|
||
$query = $query
|
||
->join('admin_group_access aga', $mainAlias . 'id = aga.uid')
|
||
->join('admin_group ag', 'aga.group_id = ag.id')
|
||
->where('ag.pid', 0)
|
||
->distinct(true);
|
||
}
|
||
|
||
$res = $query
|
||
->order($qualifiedOrder ?: $order)
|
||
->paginate($limit);
|
||
|
||
return $this->success('', [
|
||
'list' => $res->items(),
|
||
'total' => $res->total(),
|
||
]);
|
||
}
|
||
|
||
public function add(Request $request): Response
|
||
{
|
||
$response = $this->initializeBackend($request);
|
||
if ($response !== null) return $response;
|
||
|
||
if ($request->method() === 'POST') {
|
||
$data = $request->post();
|
||
if (!$data) {
|
||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||
}
|
||
$data = $this->normalizeSingleGroup($data);
|
||
if (!$this->hasSingleGroup($data['group_arr'] ?? null)) {
|
||
return $this->error(__('Please select exactly one role group'));
|
||
}
|
||
|
||
if ($this->modelValidate) {
|
||
try {
|
||
$rules = [
|
||
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username',
|
||
'nickname' => 'required|string',
|
||
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||
'email' => 'email|unique:admin,email',
|
||
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile',
|
||
'group_arr' => 'required|array',
|
||
];
|
||
$messages = [
|
||
'username.regex' => __('Please input correct username'),
|
||
'password.regex' => __('Please input correct password'),
|
||
];
|
||
Validator::make($data, $rules, $messages)->validate();
|
||
} catch (ValidationException $e) {
|
||
return $this->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
$passwd = $data['password'] ?? '';
|
||
$data = $this->excludeFields($data);
|
||
$creatorChannelId = $this->getCreatorChannelId();
|
||
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []);
|
||
if (!$this->auth->isSuperAdmin()) {
|
||
if ($creatorChannelId === null || $creatorChannelId === '') {
|
||
return $this->error(__('You have no permission'));
|
||
}
|
||
if ($groupChannelId === null || $groupChannelId === '') {
|
||
return $this->error(__('Selected role group is not bound to a channel'));
|
||
}
|
||
if ((string) $groupChannelId !== (string) $creatorChannelId) {
|
||
return $this->error(__('Selected role group channel does not match current account'));
|
||
}
|
||
$data['channel_id'] = $creatorChannelId;
|
||
$data['parent_admin_id'] = $this->auth->id;
|
||
} else {
|
||
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
|
||
}
|
||
$data['invite_code'] = $this->generateUniqueInviteCode();
|
||
$result = false;
|
||
if (!empty($data['group_arr'])) {
|
||
$authRes = $this->checkGroupAuth($data['group_arr']);
|
||
if ($authRes !== null) return $authRes;
|
||
}
|
||
$this->model->startTrans();
|
||
try {
|
||
$result = $this->model->save($data);
|
||
if (!empty($data['group_arr'])) {
|
||
$groupAccess = [];
|
||
foreach ($data['group_arr'] as $datum) {
|
||
$groupAccess[] = [
|
||
'uid' => $this->model->id,
|
||
'group_id' => $datum,
|
||
];
|
||
}
|
||
Db::name('admin_group_access')->insertAll($groupAccess);
|
||
}
|
||
$this->model->commit();
|
||
|
||
if (!empty($passwd)) {
|
||
$this->model->resetPassword($this->model->id, $passwd);
|
||
}
|
||
} catch (Throwable $e) {
|
||
$this->model->rollback();
|
||
return $this->error($e->getMessage());
|
||
}
|
||
if ($result !== false) {
|
||
return $this->success(__('Added successfully'));
|
||
}
|
||
return $this->error(__('No rows were added'));
|
||
}
|
||
|
||
return $this->error(__('Parameter error'));
|
||
}
|
||
|
||
public function edit(Request $request): Response
|
||
{
|
||
$response = $this->initializeBackend($request);
|
||
if ($response !== null) return $response;
|
||
|
||
$pk = $this->model->getPk();
|
||
$id = $request->get($pk) ?? $request->post($pk);
|
||
$row = $this->model->find($id);
|
||
if (!$row) {
|
||
return $this->error(__('Record not found'));
|
||
}
|
||
|
||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||
return $this->error(__('You have no permission'));
|
||
}
|
||
|
||
if ($request->method() === 'POST') {
|
||
$data = $request->post();
|
||
if (!$data) {
|
||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||
}
|
||
$data = $this->normalizeSingleGroup($data);
|
||
if (!$this->hasSingleGroup($data['group_arr'] ?? null)) {
|
||
return $this->error(__('Please select exactly one role group'));
|
||
}
|
||
|
||
$postedGroups = array_map('intval', $data['group_arr'] ?? []);
|
||
$rowGroups = array_map('intval', $row->group_arr ?? []);
|
||
sort($postedGroups);
|
||
sort($rowGroups);
|
||
|
||
// 当前管理员编辑自身时,不允许修改角色组
|
||
if ((int)$this->auth->id === (int)$id) {
|
||
$postedGroups = $data['group_arr'] ?? [];
|
||
if (!is_array($postedGroups)) {
|
||
$postedGroups = [];
|
||
}
|
||
$originGroups = $row->group_arr ?? [];
|
||
sort($postedGroups);
|
||
sort($originGroups);
|
||
if ($postedGroups !== $originGroups) {
|
||
return $this->error(__('You cannot modify your own management group!'));
|
||
}
|
||
}
|
||
|
||
if ($this->modelValidate) {
|
||
try {
|
||
$rules = [
|
||
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username,' . $id,
|
||
'nickname' => 'required|string',
|
||
'password' => 'nullable|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||
'email' => 'email|unique:admin,email,' . $id,
|
||
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile,' . $id,
|
||
'group_arr' => 'required|array',
|
||
];
|
||
$messages = [
|
||
'username.regex' => __('Please input correct username'),
|
||
'password.regex' => __('Please input correct password'),
|
||
];
|
||
Validator::make($data, $rules, $messages)->validate();
|
||
} catch (ValidationException $e) {
|
||
return $this->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
if ($this->auth->id == $data['id'] && ($data['status'] ?? '') == 'disable') {
|
||
return $this->error(__('Please use another administrator account to disable the current account!'));
|
||
}
|
||
|
||
if (!empty($data['password'])) {
|
||
$this->model->resetPassword($row->id, $data['password']);
|
||
}
|
||
|
||
$groupAccess = [];
|
||
if (!empty($data['group_arr'])) {
|
||
$checkGroups = [];
|
||
$rowGroupArr = $row->group_arr ?? [];
|
||
foreach ($data['group_arr'] as $datum) {
|
||
if (!in_array($datum, $rowGroupArr)) {
|
||
$checkGroups[] = $datum;
|
||
}
|
||
$groupAccess[] = [
|
||
'uid' => $id,
|
||
'group_id' => $datum,
|
||
];
|
||
}
|
||
$authRes = $this->checkGroupAuth($checkGroups);
|
||
if ($authRes !== null) return $authRes;
|
||
}
|
||
|
||
$data = $this->excludeFields($data);
|
||
unset($data['invite_code']);
|
||
$creatorChannelId = $this->getCreatorChannelId();
|
||
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []);
|
||
if (!$this->auth->isSuperAdmin()) {
|
||
if ($creatorChannelId === null || $creatorChannelId === '') {
|
||
return $this->error(__('You have no permission'));
|
||
}
|
||
if ($groupChannelId === null || $groupChannelId === '') {
|
||
return $this->error(__('Selected role group is not bound to a channel'));
|
||
}
|
||
if ((string) $groupChannelId !== (string) $creatorChannelId) {
|
||
return $this->error(__('Selected role group channel does not match current account'));
|
||
}
|
||
$data['channel_id'] = $creatorChannelId;
|
||
} else {
|
||
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
|
||
}
|
||
$result = false;
|
||
$this->model->startTrans();
|
||
try {
|
||
$result = $row->save($data);
|
||
Db::name('admin_group_access')
|
||
->where('uid', $id)
|
||
->delete();
|
||
if ($groupAccess) {
|
||
Db::name('admin_group_access')->insertAll($groupAccess);
|
||
}
|
||
$this->model->commit();
|
||
} catch (Throwable $e) {
|
||
$this->model->rollback();
|
||
return $this->error($e->getMessage());
|
||
}
|
||
if ($result !== false) {
|
||
return $this->success(__('Update successful'));
|
||
}
|
||
return $this->error(__('No rows updated'));
|
||
}
|
||
|
||
unset($row['salt'], $row['login_failure']);
|
||
$row['password'] = '';
|
||
return $this->success('', [
|
||
'row' => $row
|
||
]);
|
||
}
|
||
|
||
public function del(Request $request): Response
|
||
{
|
||
$response = $this->initializeBackend($request);
|
||
if ($response !== null) return $response;
|
||
|
||
$where = [];
|
||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||
if ($dataLimitAdminIds) {
|
||
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||
}
|
||
|
||
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
|
||
$ids = is_array($ids) ? $ids : [];
|
||
$where[] = [$this->model->getPk(), 'in', $ids];
|
||
$data = $this->model->where($where)->select();
|
||
|
||
$count = 0;
|
||
$this->model->startTrans();
|
||
try {
|
||
foreach ($data as $v) {
|
||
if ($v->id != $this->auth->id) {
|
||
$count += $v->delete();
|
||
Db::name('admin_group_access')
|
||
->where('uid', $v['id'])
|
||
->delete();
|
||
}
|
||
}
|
||
$this->model->commit();
|
||
} catch (Throwable $e) {
|
||
$this->model->rollback();
|
||
return $this->error($e->getMessage());
|
||
}
|
||
if ($count) {
|
||
return $this->success(__('Deleted successfully'));
|
||
}
|
||
return $this->error(__('No rows were deleted'));
|
||
}
|
||
|
||
/**
|
||
* 远程下拉(Admin 无自定义,走父类默认列表)
|
||
*/
|
||
public function select(Request $request): Response
|
||
{
|
||
return parent::select($request);
|
||
}
|
||
|
||
private function checkGroupAuth(array $groups): ?Response
|
||
{
|
||
if ($this->auth->isSuperAdmin()) {
|
||
return null;
|
||
}
|
||
$authGroups = $this->getManageableGroupIds();
|
||
foreach ($groups as $group) {
|
||
if (!in_array($group, $authGroups)) {
|
||
return $this->error(__('You have no permission to add an administrator to this group!'));
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private function getManageableGroupIds(): array
|
||
{
|
||
if ($this->auth->isSuperAdmin()) {
|
||
return Db::name('admin_group')->where('status', 1)->column('id');
|
||
}
|
||
$own = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
|
||
$children = $this->auth->getAdminChildGroups();
|
||
return array_values(array_unique(array_merge($own, $children)));
|
||
}
|
||
|
||
private function getCreatorChannelId(): mixed
|
||
{
|
||
$currentAdmin = Db::name('admin')
|
||
->field(['id', 'channel_id'])
|
||
->where('id', $this->auth->id)
|
||
->find();
|
||
if ($currentAdmin && !empty($currentAdmin['channel_id'])) {
|
||
return $currentAdmin['channel_id'];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* @param array<int|string> $groupIds
|
||
*/
|
||
private function resolveChannelIdFromPrimaryGroup(array $groupIds): mixed
|
||
{
|
||
if ($groupIds === []) {
|
||
return null;
|
||
}
|
||
$gid = $groupIds[0];
|
||
if ($gid === null || $gid === '') {
|
||
return null;
|
||
}
|
||
|
||
return Db::name('admin_group')->where('id', $gid)->value('channel_id');
|
||
}
|
||
|
||
private function generateUniqueInviteCode(): string
|
||
{
|
||
$tries = 0;
|
||
do {
|
||
$tries++;
|
||
$code = strtoupper(substr(Random::uuid(), 0, 8));
|
||
$exists = Db::name('admin')->where('invite_code', $code)->find();
|
||
} while ($exists && $tries < 20);
|
||
return $code;
|
||
}
|
||
|
||
private function normalizeSingleGroup(array $data): array
|
||
{
|
||
if (!array_key_exists('group_arr', $data)) {
|
||
return $data;
|
||
}
|
||
$group = $data['group_arr'];
|
||
if (is_array($group)) {
|
||
$data['group_arr'] = array_values(array_filter($group, static function ($item) {
|
||
return $item !== null && $item !== '';
|
||
}));
|
||
return $data;
|
||
}
|
||
if ($group === null || $group === '') {
|
||
$data['group_arr'] = [];
|
||
return $data;
|
||
}
|
||
$data['group_arr'] = [$group];
|
||
return $data;
|
||
}
|
||
|
||
private function hasSingleGroup(mixed $groups): bool
|
||
{
|
||
return is_array($groups) && count($groups) === 1;
|
||
}
|
||
}
|