Files
ProxyPanel/app/Http/Controllers/Admin/UserController.php
BrettonYe 2d61434ff0 Add Hysteria2 support
- Update services and controllers for new node support;
- Update relate views and language files;
- Improve client config generation and node type handling;
2026-01-30 20:04:32 +08:00

362 lines
15 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\Http\Controllers\Admin;
use App\Events\UserVNetTasks;
use App\Helpers\ProxyConfig;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserStoreRequest;
use App\Http\Requests\Admin\UserUpdateRequest;
use App\Jobs\VNet\GetUser;
use App\Models\Level;
use App\Models\Node;
use App\Models\Order;
use App\Models\User;
use App\Models\UserGroup;
use App\Models\UserHourlyDataFlow;
use App\Models\UserOauth;
use App\Services\ProxyService;
use App\Utils\Helpers;
use App\Utils\IP;
use Arr;
use Exception;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Log;
use Spatie\Permission\Models\Role;
use Str;
class UserController extends Controller
{
use ProxyConfig;
public function index(Request $request): View
{
$query = User::with(['subscribe:user_id,code']);
foreach (['id', 'port', 'status', 'enable', 'user_group_id', 'level'] as $field) {
$request->whenFilled($field, function ($value) use ($query, $field) {
$query->where($field, $value);
});
}
foreach (['username', 'wechat', 'qq'] as $field) {
$request->whenFilled($field, function ($value) use ($query, $field) {
$query->where($field, 'like', "%$value%");
});
}
// 流量使用超过90%的用户
$request->whenFilled('largeTraffic', function () use ($query) {
$query->whereIn('status', [0, 1])->whereRaw('(u + d)/transfer_enable >= 0.9');
});
// 临近过期提醒
$request->whenFilled('expireWarning', function () use ($query) {
$query->whereBetween('expired_at', [date('Y-m-d'), date('Y-m-d', strtotime(sysConfig('expire_days').' days'))]);
});
// 当前在线
$request->whenFilled('online', function () use ($query) {
$query->where('t', '>=', strtotime('-10 minutes'));
});
// 不活跃用户
$request->whenFilled('unActive', function () use ($query) {
$query->whereBetween('t', [1, strtotime('-'.sysConfig('expire_days').' days')])->whereEnable(1);
});
// 付费服务中的用户
$request->whenFilled('paying', function () use ($query) {
$payingUser = Order::whereStatus(2)->whereNotNull('goods_id')->whereIsExpire(0)->where('amount', '>', 0)->pluck('user_id')->unique();
$query->whereIn('id', $payingUser);
});
// 1小时内流量异常用户
$request->whenFilled('flowAbnormal', function () use ($query) {
$query->whereIn('id', (new UserHourlyDataFlow)->trafficAbnormal());
});
return view('admin.user.index', [
'userList' => $query->sortable(['id' => 'desc'])->paginate(15)->appends($request->except('page')),
'userGroups' => UserGroup::pluck('name', 'id'),
'levels' => Level::orderBy('level')->pluck('name', 'level'),
]);
}
public function store(UserStoreRequest $request): JsonResponse
{
$data = $request->validated();
Arr::forget($data, 'roles');
$data['password'] = $data['password'] ?? Str::random();
$data['port'] = $data['port'] ?? Helpers::getPort();
$data['passwd'] = $data['passwd'] ?? Str::random();
$data['vmess_id'] = $data['vmess_id'] ?: Str::uuid();
$data['transfer_enable'] *= GiB;
$data['expired_at'] = $data['expired_at'] ?? date('Y-m-d', strtotime('next year'));
$data['remark'] = str_replace(['atob', 'eval'], '', $data['remark'] ?? '');
$data['reg_ip'] = IP::getClientIp();
$data['reset_time'] = $data['reset_time'] > date('Y-m-d') ? $data['reset_time'] : null;
$user = User::create($data);
$roles = $request->input('roles');
try {
$editor = auth()->user();
if ($roles && ($editor->can('give roles') || (in_array('Super Admin', $roles, true) && $editor->hasRole('Super Admin')))) {
// 编辑用户权限, 只有超级管理员才有赋予超级管理的权限
$user->assignRole($roles);
}
if ($user) {
Helpers::addUserTrafficModifyLog($user->id, 0, $data['transfer_enable'], trans('Manually add in dashboard.'));
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.add')])]);
}
} catch (Exception $e) {
Log::error(trans('common.error_action_item', ['action' => trans('common.add'), 'attribute' => trans('model.user.attribute')]).': '.$e->getMessage());
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')]).', '.$e->getMessage()]);
}
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.add')])]);
}
public function create(): View
{
return view('admin.user.info', $this->getUserViewData());
}
public function edit(User $user): View
{
return view('admin.user.info', [...$this->getUserViewData(), 'user' => $user->load('inviter:id,username')]);
}
/**
* 获取用户创建/编辑页面的共享数据.
*/
private function getUserViewData(): array
{
$editor = auth()->user();
$roles = null;
if ($editor->hasRole('Super Admin')) { // 超级管理员直接获取全部角色
$roles = Role::pluck('description', 'name');
}
if ($editor->can('give roles')) { // 有权者只能获得已有角色,防止权限泛滥
$roles = $editor->roles()->pluck('description', 'name');
}
return [
'levels' => Level::orderBy('level')->pluck('name', 'level'),
'userGroups' => UserGroup::orderBy('id')->pluck('name', 'id'),
'roles' => $roles,
...$this->proxyConfigOptions(),
];
}
public function destroy(User $user): JsonResponse
{
if ($user->id === 1) {
return response()->json(['status' => 'fail', 'message' => trans('admin.user.admin_deletion')]);
}
try {
if ($user->delete()) {
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.delete')])]);
}
} catch (Exception $e) {
Log::error(trans('common.error_action_item', ['action' => trans('common.delete'), 'attribute' => trans('model.user.attribute')]).': '.$e->getMessage());
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.delete')]).', '.$e->getMessage()]);
}
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.delete')])]);
}
public function batchAddUsers(): JsonResponse
{
try {
for ($i = 0; $i < (int) request('amount', 1); $i++) {
$user = Helpers::addUser(Str::random(8).'@auto.generate', Str::random(), MiB * sysConfig('default_traffic'), (int) sysConfig('default_days'));
Helpers::addUserTrafficModifyLog($user->id, 0, $user->transfer_enable, trans('Batch generate user accounts in dashboard.'));
}
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.generate')])]);
} catch (Exception $e) {
Log::error(trans('common.error_action_item', ['action' => trans('common.generate'), 'attribute' => trans('model.user.attribute')]).': '.$e->getMessage());
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.generate')]).', '.$e->getMessage()]);
}
}
public function switchToUser(User $user): JsonResponse
{
// 存储当前管理员ID并将当前登录信息改成要切换的用户的身份信息
session()->put('admin', auth()->id());
session()->put('user', $user->id);
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('admin.user.info.switch')])]);
}
public function resetTraffic(User $user): JsonResponse
{
try {
if ($user->update(['u' => 0, 'd' => 0])) {
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.reset')])]);
}
} catch (Exception $e) {
Log::error(trans('common.error_action_item', ['action' => trans('common.reset'), 'attribute' => trans('model.user.usable_traffic')]).': '.$e->getMessage());
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.reset').', '.$e->getMessage()])]);
}
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.reset')])]);
}
public function update(UserUpdateRequest $request, User $user): JsonResponse
{
$data = $request->validated();
Arr::forget($data, ['roles', 'password']);
$data['passwd'] = $request->input('passwd') ?? Str::random();
$data['vmess_id'] = $data['vmess_id'] ?: Str::uuid();
$data['transfer_enable'] *= GiB;
$data['enable'] = $data['status'] < 0 ? 0 : $data['enable'];
$data['expired_at'] = $data['expired_at'] ?? date('Y-m-d', strtotime('next year'));
if (! empty($data['remark'])) {
$data['remark'] = str_replace(['atob', 'eval'], '', $data['remark']);
}
// 只有超级管理员才能赋予超级管理员
$roles = $request->input('roles');
try {
if (isset($roles)) {
$editor = auth()->user();
if ($editor->can('give roles') || $editor->hasRole('Super Admin')
|| (in_array('Super Admin', $roles, true) && auth()->user()->hasRole('Super Admin'))) {
$user->syncRoles($roles);
}
} else {
$user->roles()->detach();
}
// Input checking for dummy
if ($data['enable'] === '1') {
if ($data['status'] === '-1' || $data['transfer_enable'] === 0 || $data['expired_at'] < date('Y-m-d')) {
$data['enable'] = 0;
}
}
// 非演示环境才可以修改管理员密码
$password = $request->input('password');
if (! empty($password) && (config('app.env') !== 'demo' || $user->id !== 1)) {
$data['password'] = $password;
}
if ($user->transfer_enable !== (int) $data['transfer_enable']) {
Helpers::addUserTrafficModifyLog($user->id, $user->transfer_enable, $data['transfer_enable'], trans('Manually edit in dashboard.'));
}
if ($user->update($data)) {
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('common.edit')])]);
}
} catch (Exception $e) {
Log::error(trans('common.error_action_item', ['action' => trans('common.edit'), 'attribute' => trans('model.user.attribute')]).': '.$e->getMessage());
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit').', '.$e->getMessage()])]);
}
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('common.edit')])]);
}
public function handleUserCredit(Request $request, User $user): JsonResponse
{
$amount = $request->input('amount');
if (empty($amount)) {
return response()->json(['status' => 'fail', 'message' => trans('common.error_item', ['attribute' => trans('user.recharge')])]);
}
// 加减余额
if ($user->updateCredit($amount)) {
Helpers::addUserCreditLog($user->id, null, $user->credit - $amount, $user->credit, $amount, $request->input('description') ?? 'Manually edit in dashboard.'); // 写入余额变动日志
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('user.recharge')])]);
}
return response()->json(['status' => 'fail', 'message' => trans('common.failed_item', ['attribute' => trans('user.recharge')])]);
}
public function export(User $user): View
{
return view('admin.user.export', [
'user' => $user,
'nodeList' => Node::whereStatus(1)->orderByDesc('sort')->orderBy('id')->paginate(15)->appends(\request('page')),
]);
}
public function exportProxyConfig(Request $request, User $user, ProxyService $proxyService): JsonResponse
{
$proxyService->setUser($user);
$node = Node::findOrFail($request->input('id'));
return response()->json(['status' => 'success', 'data' => $proxyService->getUserProxyConfig($node, $request->input('type') !== 'text')]);
}
public function oauth(Request $request): View
{
$query = UserOauth::with('user:id,username');
// 用户名过滤
$request->whenFilled('username', function ($value) use ($query) {
$query->whereHas('user', function ($userQuery) use ($value) {
$userQuery->where('username', 'like', "%$value%");
});
});
// 类型过滤
$request->whenFilled('type', function ($value) use ($query) {
$query->where('type', $value);
});
return view('admin.user.oauth', [
'list' => $query->paginate(15)->appends(\request('page')),
]);
}
public function VNetInfo(User $user): JsonResponse
{
// 获取用户关联的 VNet 节点
$nodes = $user->nodes()->whereType(4)->get();
// 立即发送节点列表信息给前端
broadcast(new UserVNetTasks('check', ['nodeList' => $nodes->pluck('name', 'id')->toArray()], $user->id));
// 创建 GetUser 实例
$getUser = new GetUser;
// 异步检查用户在各节点的可用性
foreach ($nodes as $node) {
dispatch(static function () use ($user, $node, $getUser) {
$ret = ['nodeId' => $node->id, 'available' => false];
try {
// 发送请求检查用户是否在该节点上
$userList = $getUser->list($node);
if ($userList && is_array($userList)) {
// 检查用户是否在返回的列表中
$ret['available'] = in_array($user->id, $userList, true);
}
} catch (Exception $e) {
Log::warning('【用户列表】获取失败(推送地址:'.($node->server ?: $node->ips()[0]).':'.$node->push_port.''.$e->getMessage());
}
// 广播检查结果
broadcast(new UserVNetTasks('check', $ret, $user->id));
});
}
return response()->json(['status' => 'success', 'message' => trans('common.success_item', ['attribute' => trans('admin.user.connection_test')])]);
}
}