diff --git a/app/Http/Controllers/Admin/AdminAuthController.php b/app/Http/Controllers/Admin/AdminAuthController.php new file mode 100644 index 0000000..640dfe1 --- /dev/null +++ b/app/Http/Controllers/Admin/AdminAuthController.php @@ -0,0 +1,147 @@ +session()->get('admin_login_via_hidden')) { + return redirect()->route('admin.dashboard'); + } + + return view('admin.auth.login', [ + 'loginSuffix' => self::LOGIN_SUFFIX, + ]); + } + + /** + * 处理站长隐藏登录请求。 + */ + public function store(AdminLoginRequest $request): RedirectResponse + { + $validated = $request->validated(); + $siteOwner = User::query()->find(self::SITE_OWNER_ID); + + // 只有 id=1 的站长账号允许通过该入口进入后台 + if (! $siteOwner || $siteOwner->username !== $validated['username']) { + return back() + ->withInput($request->safe()->only(['username'])) + ->withErrors(['username' => '该入口仅限站长账号使用。']); + } + + if (! $this->passwordMatches($siteOwner, $validated['password'])) { + return back() + ->withInput($request->safe()->only(['username'])) + ->withErrors(['password' => '账号或密码错误。']); + } + + // 若当前已有其他账号占用会话,先退出后再切换为站长会话 + if (Auth::check() && Auth::id() !== $siteOwner->id) { + Auth::logout(); + } + + Auth::login($siteOwner); + $request->session()->regenerate(); + $request->session()->put('admin_login_via_hidden', true); + + // 复用主登录的会话登记逻辑,保证后台入口也会更新登录痕迹 + $this->recordAdminLogin($siteOwner, (string) $request->ip()); + + return redirect()->route('admin.dashboard')->with('success', '站长后台登录成功。'); + } + + /** + * 校验站长密码,兼容旧库 MD5 并自动升级为 bcrypt。 + */ + private function passwordMatches(User $siteOwner, string $plainPassword): bool + { + try { + if (Hash::check($plainPassword, $siteOwner->password)) { + return true; + } + } catch (\RuntimeException $exception) { + // 旧库非 bcrypt 密码会在这里抛异常,后续继续走 MD5 兼容逻辑 + } + + if (md5($plainPassword) !== $siteOwner->password) { + return false; + } + + // 兼容老密码登录成功后,立即升级为 Laravel 默认哈希 + $siteOwner->forceFill([ + 'password' => Hash::make($plainPassword), + ])->save(); + + return true; + } + + /** + * 记录站长通过隐藏入口登录后的访问痕迹。 + */ + private function recordAdminLogin(User $siteOwner, string $ip): void + { + // 登录成功后补齐访问次数、IP 与时间,保持与前台登录统计一致 + $siteOwner->increment('visit_num'); + $siteOwner->update([ + 'previous_ip' => $siteOwner->last_ip, + 'last_ip' => $ip, + 'log_time' => now(), + 'in_time' => now(), + ]); + + \App\Models\IpLog::create([ + 'ip' => $ip, + 'sdate' => now(), + 'uuname' => $siteOwner->username, + ]); + + try { + $wechatService = new \App\Services\WechatBot\WechatNotificationService; + $wechatService->notifyAdminOnline($siteOwner); + $wechatService->notifyFriendsOnline($siteOwner); + $wechatService->notifySpouseOnline($siteOwner); + } catch (\Exception $exception) { + // 机器人通知异常不影响站长进入后台,但需要落日志便于排查 + Log::error('Hidden admin login notification failed', ['error' => $exception->getMessage()]); + } + } +} diff --git a/app/Http/Requests/AdminLoginRequest.php b/app/Http/Requests/AdminLoginRequest.php new file mode 100644 index 0000000..8eb0d0c --- /dev/null +++ b/app/Http/Requests/AdminLoginRequest.php @@ -0,0 +1,60 @@ +|string> + */ + public function rules(): array + { + return [ + 'username' => ['required', 'string', 'max:255'], + 'password' => ['required', 'string', 'min:1'], + 'captcha' => ['required', 'captcha'], + ]; + } + + /** + * 获取验证失败时展示的中文提示。 + * + * @return array + */ + public function messages(): array + { + return [ + 'username.required' => '必须填写站长账号。', + 'password.required' => '必须填写登录密码。', + 'password.min' => '登录密码格式不正确。', + 'captcha.required' => '必须填写验证码。', + 'captcha.captcha' => '验证码不正确。', + ]; + } +} diff --git a/resources/views/admin/auth/login.blade.php b/resources/views/admin/auth/login.blade.php new file mode 100644 index 0000000..2e8dc21 --- /dev/null +++ b/resources/views/admin/auth/login.blade.php @@ -0,0 +1,561 @@ +{{-- 文件功能:站长隐藏登录页 --}} +@php + $systemName = \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '和平聊吧'; +@endphp + + + + + + + + 站长后台入口 - {{ $systemName }} + + + + + + + +
+
+
+
Hidden Admin Entry
+
{{ $systemName }}
+
+ +
+
Trust / Authority / Private Access
+

+ Owner + Console +

+ +

+ 这是一个独立于聊天室首页的后台登录入口。这里只做站长控制台访问,不承载普通用户登录,不会在登录成功后打开聊天室页面。 +

+
+ +
+
+
入口后缀
+
{{ $loginSuffix }}
+
保留隐藏后缀,不在首页暴露。这个入口只用于后台控制台访问。
+
+ +
+
权限约束
+
id=1
+
只有站长主账号可以从这里登录,其他账号即使密码正确也会被拒绝。
+
+ +
+
登录去向
+
Admin
+
验证通过后直接进入后台首页,不跳聊天室大厅,不走“登录即注册”。
+
+
+
+ +
+
+
+
+
Owner Authentication
+

登录后台控制台

+

输入站长账号、密码和验证码后进入后台。

+
+
Restricted
+
+ +
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ +
+ + 站长登录验证码 +
+
+ + @if (session('success')) +
+ {{ session('success') }} +
+ @endif + + @if ($errors->any()) + + @endif + + +
+ +
+ 提示:点击右侧验证码图片可刷新。如果页面仍显示旧样式,直接强制刷新浏览器缓存即可。 +
+
+
+
+ + + + + diff --git a/routes/web.php b/routes/web.php index fdd2182..f149299 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ route('admin.dashboard'); + } + return redirect()->route('rooms.index'); } @@ -24,6 +30,10 @@ Route::get('/', function () { return view('index', compact('rooms')); })->name('home'); +// 站长隐藏登录入口(仅 id=1 可使用,成功后直接进入后台控制台) +Route::get('/lkddi', [AdminAuthController::class, 'create'])->name('admin.login'); +Route::post('/lkddi', [AdminAuthController::class, 'store'])->name('admin.login.store'); + // 处理登录/自动注册请求 Route::post('/login', [AuthController::class, 'login'])->name('login.post'); diff --git a/tests/Feature/ChatControllerTest.php b/tests/Feature/ChatControllerTest.php index 62c4077..94b3c15 100644 --- a/tests/Feature/ChatControllerTest.php +++ b/tests/Feature/ChatControllerTest.php @@ -45,6 +45,21 @@ class ChatControllerTest extends TestCase $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username)); } + /** + * 测试主干默认聊天室页面不会渲染虚拟形象挂载点和配置。 + */ + public function test_room_view_does_not_render_avatar_widget_or_config_by_default(): void + { + $room = Room::create(['room_name' => 'avguard']); + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('chat.room', $room->id)); + + $response->assertOk(); + $response->assertDontSee('chat-avatar-widget'); + $response->assertDontSee('chatAvatarWidget'); + } + /** * 测试用户可以发送普通文本消息。 */ diff --git a/tests/Feature/Feature/AdminAuthControllerTest.php b/tests/Feature/Feature/AdminAuthControllerTest.php new file mode 100644 index 0000000..4695ce9 --- /dev/null +++ b/tests/Feature/Feature/AdminAuthControllerTest.php @@ -0,0 +1,115 @@ +get('/lkddi'); + + $response->assertOk() + ->assertSee('站长后台入口') + ->assertSee('/lkddi'); + } + + /** + * 验证 id=1 站长可以通过隐藏入口登录并进入后台首页。 + */ + public function test_site_owner_can_login_via_hidden_admin_entry(): void + { + $siteOwner = User::factory()->create([ + 'id' => 1, + 'username' => 'site-owner', + 'password' => Hash::make('secret-owner'), + ]); + + $response = $this->post('/lkddi', [ + 'username' => 'site-owner', + 'password' => 'secret-owner', + 'captcha' => '1234', + ]); + + $response->assertRedirect(route('admin.dashboard')); + $response->assertSessionHas('admin_login_via_hidden', true); + $this->assertAuthenticatedAs($siteOwner); + } + + /** + * 验证非 id=1 账号即使密码正确,也不能使用隐藏入口。 + */ + public function test_non_site_owner_cannot_login_via_hidden_admin_entry(): void + { + User::factory()->create([ + 'username' => 'manager', + 'password' => Hash::make('secret-manager'), + ]); + + $response = $this->from('/lkddi')->post('/lkddi', [ + 'username' => 'manager', + 'password' => 'secret-manager', + 'captcha' => '1234', + ]); + + $response->assertRedirect('/lkddi'); + $response->assertSessionHasErrors('username'); + $this->assertGuest(); + } + + /** + * 验证通过隐藏入口登录的站长访问首页时不会被送去聊天室大厅。 + */ + public function test_hidden_admin_login_keeps_homepage_redirecting_to_dashboard(): void + { + $siteOwner = User::factory()->create([ + 'id' => 1, + 'username' => 'site-owner', + 'password' => Hash::make('secret-owner'), + ]); + + $this->actingAs($siteOwner)->withSession([ + 'admin_login_via_hidden' => true, + ]); + + $response = $this->get('/'); + + $response->assertRedirect(route('admin.dashboard')); + } +}