feat: delete

This commit is contained in:
hashbro
2026-09-03 01:29:50 +08:00
parent 44bcfe544c
commit 4c49a61149
5 changed files with 196 additions and 45 deletions
+52 -10
View File
@@ -4,11 +4,19 @@ namespace App\Http\Controllers\Agent;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
private const MAX_ATTEMPTS = 5;
private const DECAY_SECONDS = 60;
public function showLogin()
{
if (Auth::guard('agent')->check()) {
@@ -23,29 +31,58 @@ class AuthController extends Controller
return view('user.shell');
}
public function login(Request $request)
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'username' => 'required|string',
'password' => 'required|string',
]);
try {
$credentials = $request->validate([
'username' => 'required|string|min:2|max:64',
'password' => 'required|string|max:128',
], [
'username.required' => '请输入用户名',
'password.required' => '请输入密码',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => collect($e->errors())->flatten()->implode('<br>'),
]);
}
$throttleKey = $this->throttleKey($request);
if (RateLimiter::tooManyAttempts($throttleKey, self::MAX_ATTEMPTS)) {
$seconds = RateLimiter::availableIn($throttleKey);
return response()->json([
'code' => 1,
'msg' => '登陆失败次数过多,请'.$seconds.'秒后再重试',
]);
}
/** @var User|null $user */
$user = User::query()->where('username', $credentials['username'])->first();
if ($user && ! $user->isEnabled()) {
return back()->withErrors(['username' => '账号已禁用'])->onlyInput('username');
RateLimiter::hit($throttleKey, self::DECAY_SECONDS);
return response()->json(['code' => 1, 'msg' => '账号已禁用']);
}
if (Auth::guard('agent')->attempt(
if (! Auth::guard('agent')->attempt(
['username' => $credentials['username'], 'password' => $credentials['password']],
$request->boolean('remember')
)) {
$request->session()->regenerate();
RateLimiter::hit($throttleKey, self::DECAY_SECONDS);
return redirect()->intended(route('user.home'));
return response()->json(['code' => 1, 'msg' => '用户名或密码错误']);
}
return back()->withErrors(['username' => '用户名或密码错误'])->onlyInput('username');
RateLimiter::clear($throttleKey);
$request->session()->regenerate();
return response()->json([
'code' => 0,
'msg' => '登录成功',
'data' => route('user.home'),
]);
}
public function logout(Request $request)
@@ -56,4 +93,9 @@ class AuthController extends Controller
return redirect()->route('user.login');
}
private function throttleKey(Request $request): string
{
return Str::transliterate(Str::lower((string) $request->input('username')).'|'.$request->ip());
}
}
+11 -21
View File
@@ -176,7 +176,7 @@ https://admin.example.com/details/… # 共享模板(native 按需拉
https://admin.example.com/c/<ver>/show.htm # Nginx 静态别名 → channel/<ver>/details/show.html
```
在 **Admin 站和所有反代到同一 `public/` 的投放 / DGA 域名** 上都要配下面这段。放在 `location /` 以及 `include enable-php-*.conf` **之前**。漏配时 `/c/<ver>/show.htm` 会掉进 Laravel,一次落地页就占一个 PHP-FPM worker。
在 **Admin 站和所有反代到同一** `public/` **的投放 / DGA 域名** 上都要配下面这段。放在 `location /` 以及 `include enable-php-*.conf` **之前**。漏配时 `/c/<ver>/show.htm` 会掉进 Laravel,一次落地页就占一个 PHP-FPM worker。
`root` / `alias` 里的路径按实际站点根改(宝塔多为 `/www/wwwroot/coruna-lab/public`)。
@@ -445,7 +445,7 @@ REDIS_HOST=127.0.0.1
REDIS_PORT=6379
```
4. 改完后:
1. 改完后:
```bash
cd /www/wwwroot/coruna-lab
@@ -453,7 +453,7 @@ cd /www/wwwroot/coruna-lab
/www/server/php/82/bin/php artisan config:clear
```
5. 宝塔 **Supervisor** 常驻队列(换成 redis 后必须有,否则 PV 不记、通知不发):
1. 宝塔 **Supervisor** 常驻队列(换成 redis 后必须有,否则 PV 不记、通知不发):
```text
名称: coruna-queue
@@ -462,7 +462,7 @@ cd /www/wwwroot/coruna-lab
进程数量: 2
```
6. PHP-FPM(软件商店 → PHP 8.2 → 性能调整),4C8G 建议:
1. PHP-FPM(软件商店 → PHP 8.2 → 性能调整),4C8G 建议:
```ini
pm = dynamic
@@ -475,7 +475,9 @@ pm.max_requests = 500
`max_children` 不要开到 50+:每个 worker 约 40–80MB,再加 MySQL 会先 swap。相册解包仍可能短时占满几个 worker,属正常。
7. MySQL:8G 机器 `innodb_buffer_pool_size` 建议 1.5G–2.5G。
1. MySQL:8G 机器 `innodb_buffer_pool_size` 建议 1.5G–2.5G。
### 2.7 p7zip(C2 入库)
@@ -588,7 +590,7 @@ tail -n 80 /www/server/php/82/var/log/php-fpm.log
| 备份 | MySQL + `public/web` + `public/sync` + `public/channel` + `public/details` + `storage/app/channel-builder` + `storage/app/channel-builder-new` |
| 构建超时 | 加大 `CORUNA_CHANNEL_BUILDER_TIMEOUT` 与 PHP `max_execution_time` |
| 勿改线上 `XXBB_CHANNEL_C` | 改了会导致新版 DGA 域名与已装设备不一致;换战役需整体重部署策略 |
| 队列 worker(Redis 时) | `php artisan queue:work --sleep=1 --tries=3`(宝塔 Supervisor 常驻,见 §2.6) |
| 队列 worker(Redis 时) | `php artisan queue:work --sleep=1 --tries=3`(宝塔 Supervisor 常驻,见 §2.6) |
`QUEUE_CONNECTION=sync` 时无需 worker;换成 `redis` 后必须常驻 `queue:work`,否则 PV 记数、Telegram 通知、自动转账和相册解压都不会执行。自动转账仍由每分钟的 `coruna:auto-transfer` 扫描候选地址,真正的查余额 / 广播交给队列(同一地址 3 分钟内只跑一条,失败不重试,避免重复打款)。相册上报仍立刻 ACK,包先落到 `storage/app/c2/inbox/` 再解;解不开只写日志、不入库、不重试(与改队列前一致)。
@@ -646,8 +648,8 @@ PHP「禁用函数」含 `putenv`。在 PHP 8.2 设置里移除后重试。
/usr/bin/python3
```
2. 安全 → **系统加固**:排除 `/www/wwwroot/coruna-lab`;不要把系统 Python 标成「禁止执行」。
3. 加白后用同一条命令验证,必须打印 `ok`,不能再出现 `Tips from BT security`:
1. 安全 → **系统加固**:排除 `/www/wwwroot/coruna-lab`;不要把系统 Python 标成「禁止执行」。
2. 加白后用同一条命令验证,必须打印 `ok`,不能再出现 `Tips from BT security`:
```bash
sudo -u www /www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python -c 'import py7zr; print("ok")'
@@ -718,7 +720,7 @@ CLI 默认不限制;改网站用的 FPM `php.ini` 并以 `phpinfo()` 验证。
### 后台登录 HTTP 444
**444** 是 Nginx(宝塔防火墙 / 安全规则)直接掐连接,请求通常未进 PHP。查 Nginx/网站防火墙拦截日志,对管理 IP 或 `/admin` 放行后再试。
**444** 是 Nginx(宝塔防火墙 / 安全规则)直接掐连接,请求通常未进 PHP。查 Nginx/网站防火墙拦截日志,对管理 IP 或 `/admin`、`/user` 放行后再试。
### 后台登录方式(改造后)
@@ -749,15 +751,3 @@ chmod -R ug+rwx storage/framework/sessions
---
## 6. 分机部署(可选)
若要把静态产物拆到另一台纯静态机:
- 旧版:rsync `public/web` + `public/sync`
- 新版:rsync `public/channel` + `public/details`;静态机 Nginx 必须带 §1.3 的 `/c/<ver>/show.htm` `alias` 映射,不要把该路径反代回 PHP
- 或把 `CORUNA_ARTIFACT_ROOT` 指到共享盘,静态机 Nginx root 指向该盘
- seed / staged 状态仍放在 lab:`CORUNA_CHANNEL_STATE_ROOT`、`CORUNA_CHANNEL_NEW_STATE_ROOT`
同机时无需拆分,Admin `public/` 即静态根。
+49 -12
View File
@@ -6,6 +6,7 @@
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="{{ asset('static/layuiadmin/layui/css/layui.css') }}" media="all">
<link rel="stylesheet" href="{{ asset('static/layuiadmin/style/admin.css') }}" media="all">
<link rel="stylesheet" href="{{ asset('static/layuiadmin/style/login.css') }}" media="all">
@@ -17,38 +18,74 @@
<h2>运营后台</h2>
<p>Coruna Lab Agent Portal</p>
</div>
<form class="layadmin-user-login-box layadmin-user-login-body layui-form" method="post" action="{{ route('user.login.submit') }}">
<div class="layadmin-user-login-box layadmin-user-login-body layui-form">
@csrf
@if ($errors->any())
<div class="layui-form-item">
<div class="layui-bg-red" style="padding:8px 12px;border-radius:2px;">{{ $errors->first() }}</div>
</div>
@endif
<div class="layui-form-item">
<label class="layadmin-user-login-icon layui-icon layui-icon-username" for="LAY-user-login-username"></label>
<input type="text" name="username" id="LAY-user-login-username" value="{{ old('username') }}" lay-verify="required" placeholder="用户名" class="layui-input" autofocus>
<input type="text" name="username" id="LAY-user-login-username" lay-verify="required" placeholder="用户名" class="layui-input" autofocus autocomplete="username">
</div>
<div class="layui-form-item">
<label class="layadmin-user-login-icon layui-icon layui-icon-password" for="LAY-user-login-password"></label>
<input type="password" name="password" id="LAY-user-login-password" lay-verify="required" placeholder="密码" class="layui-input">
<input type="password" name="password" id="LAY-user-login-password" lay-verify="required" placeholder="密码" class="layui-input" autocomplete="current-password">
</div>
<div class="layui-form-item" style="margin-bottom: 20px;">
<input type="checkbox" name="remember" value="1" lay-skin="primary" title="记住密码">
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit type="submit">登 入</button>
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="LAY-agent-login-submit">登 入</button>
</div>
</form>
</div>
</div>
</div>
<script src="{{ asset('static/layuiadmin/layui/layui.js') }}"></script>
<script>
var postLoginUrl = @json(route('user.login.submit'));
if (window.parent !== window) {
window.parent.location.href = @json(route('user.login'));
}
layui.config({
base: @json(rtrim(asset('static/layuiadmin'), '/').'/')
}).extend({
index: 'lib/index'
}).use(['form'], function(){
layui.form.render();
}).use(['form'], function () {
var $ = layui.$, form = layui.form, layer = layui.layer;
form.render();
form.on('submit(LAY-agent-login-submit)', function (obj) {
var field = obj.field || {};
field._token = field._token || $('meta[name=csrf-token]').attr('content');
var loadIndex = layer.load(2);
$.ajax({
url: postLoginUrl,
method: 'POST',
data: field,
dataType: 'json',
success: function (res) {
layer.close(loadIndex);
layer.msg(res.msg || (res.code === 0 ? '登录成功' : '登录失败'), {
offset: '15px',
icon: res.code === 0 ? 1 : 2,
time: 1000
}, function () {
if (res.code === 0 && res.data) {
location.href = res.data;
}
});
},
error: function (xhr) {
layer.close(loadIndex);
var msg = '登录失败';
if (xhr.status === 419) msg = '页面已过期,请刷新后重试';
else if (xhr.responseJSON && xhr.responseJSON.msg) msg = xhr.responseJSON.msg;
else if (xhr.responseJSON && xhr.responseJSON.message) msg = xhr.responseJSON.message;
layer.msg(msg, { icon: 2 });
}
});
return false;
});
});
</script>
</body>
+6 -2
View File
@@ -59,7 +59,7 @@ class AdminAgentPortalTest extends TestCase
$this->post('/user/login', [
'username' => 'disabled',
'password' => 'secret12',
])->assertSessionHasErrors('username');
])->assertOk()->assertJson(['code' => 1, 'msg' => '账号已禁用']);
$this->assertGuest('agent');
}
@@ -76,7 +76,11 @@ class AdminAgentPortalTest extends TestCase
$this->post('/user/login', [
'username' => 'okagent',
'password' => 'secret12',
])->assertRedirect(route('user.home'));
])->assertOk()->assertJson([
'code' => 0,
'msg' => '登录成功',
'data' => route('user.home'),
]);
$this->assertAuthenticated('agent');
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class AgentLoginTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function ajax_login_succeeds_and_sets_session(): void
{
User::query()->create([
'username' => 'okagent',
'password' => 'secret12',
'status' => 1,
]);
$this->post('/user/login', [
'username' => 'okagent',
'password' => 'secret12',
])->assertOk()
->assertJson([
'code' => 0,
'msg' => '登录成功',
'data' => route('user.home'),
]);
$this->assertAuthenticated('agent');
}
#[Test]
public function ajax_login_rejects_bad_password(): void
{
User::query()->create([
'username' => 'okagent',
'password' => 'secret12',
'status' => 1,
]);
$this->post('/user/login', [
'username' => 'okagent',
'password' => 'wrongpass',
])->assertOk()
->assertJson(['code' => 1, 'msg' => '用户名或密码错误']);
$this->assertGuest('agent');
}
#[Test]
public function login_is_rate_limited_after_failures(): void
{
User::query()->create([
'username' => 'okagent',
'password' => 'secret12',
'status' => 1,
]);
for ($i = 0; $i < 5; $i++) {
$this->post('/user/login', [
'username' => 'okagent',
'password' => 'bad-password',
])->assertOk()->assertJsonPath('code', 1);
}
$response = $this->post('/user/login', [
'username' => 'okagent',
'password' => 'secret12',
])->assertOk()
->assertJsonPath('code', 1);
$this->assertStringContainsString('登陆失败次数过多', (string) $response->json('msg'));
}
}