功能:window.chatBanner 全局大卡片公共组件

前端:
- window.chatBanner.show(options) 全局 API,完全自定义:
  icon/title/name/body/sub/gradient/titleColor/autoClose/buttons
- window.chatBanner.close(id) 关闭指定 banner
- showFriendBanner / showAppointmentBanner 均改用 chatBanner 实现
- setupBannerNotification() 监听私有+房间频道的 BannerNotification 事件

后端:
- BannerNotification 事件(ShouldBroadcastNow),支持 user/room 双目标
- BannerBroadcastController(仅超级管理员路由,三层中间件保护)
- 内容字段 strip_tags 净化防 XSS,按钮 action 白名单校验

安全:
- window.chatBanner.show() 被人控制台调用只影响自己,无法推给他人
- HTTP 入口 POST /admin/banner/broadcast 仅超管可访问
This commit is contained in:
2026-03-01 01:28:23 +08:00
parent 0f0691d037
commit 5c53b8cf2f
4 changed files with 429 additions and 163 deletions

View File

@@ -0,0 +1,82 @@
<?php
/**
* 文件功能:管理员大卡片通知广播控制器
*
* 仅超级管理员chat.level:super 中间件保护)可调用此接口,
* 通过 BannerNotification 事件向指定用户或房间推送自定义大卡通知。
*
* 安全保证:
* - 路由被 ['chat.auth', 'chat.has_position', 'chat.level:super'] 三层中间件保护
* - 普通用户无权访问此接口,无法伪造对他人的广播
* - options 中的用户输入字段在后端经过 strip_tags 清洗
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers\Admin;
use App\Events\BannerNotification;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BannerBroadcastController extends Controller
{
/**
* 向指定目标广播大卡片通知。
*
* 请求参数:
* - target: 'user' | 'room'
* - target_id: 用户名 房间 ID
* - options: window.chatBanner.show() 参数相同的对象
* - icon, title, name, body, sub, gradient(array), titleColor, autoClose, buttons(array)
*/
public function send(Request $request): JsonResponse
{
$validated = $request->validate([
'target' => ['required', 'in:user,room'],
'target_id' => ['required'],
'options' => ['required', 'array'],
'options.icon' => ['nullable', 'string', 'max:20'],
'options.title' => ['nullable', 'string', 'max:50'],
'options.name' => ['nullable', 'string', 'max:100'],
'options.body' => ['nullable', 'string', 'max:500'],
'options.sub' => ['nullable', 'string', 'max:200'],
'options.gradient' => ['nullable', 'array', 'max:5'],
'options.titleColor' => ['nullable', 'string', 'max:30'],
'options.autoClose' => ['nullable', 'integer', 'min:0', 'max:30000'],
'options.buttons' => ['nullable', 'array', 'max:4'],
]);
// 对可能包含用户输入的字段进行 HTML 净化(防 XSS
$opts = $validated['options'];
foreach (['title', 'name', 'body', 'sub'] as $field) {
if (isset($opts[$field])) {
$opts[$field] = strip_tags($opts[$field], '<b><strong><em><span><br>');
}
}
// 按钮 label 不允许 HTML
if (! empty($opts['buttons'])) {
$opts['buttons'] = array_map(function ($btn) {
$btn['label'] = strip_tags($btn['label'] ?? '');
$btn['color'] = preg_replace('/[^a-z0-9#(),\s.%rgba\/]/i', '', $btn['color'] ?? '#10b981');
// action 只允许预定义值,防止注入任意 JS
$btn['action'] = in_array($btn['action'] ?? '', ['close', 'add_friend', 'remove_friend', 'link'])
? $btn['action'] : 'close';
return $btn;
}, $opts['buttons']);
}
broadcast(new BannerNotification(
target: $validated['target'],
targetId: $validated['target_id'],
options: $opts,
));
return response()->json(['status' => 'success', 'message' => '广播已发送']);
}
}