fix: 全面安全审计修复
安全加固: - 系统配置 API 增加写入 key 白名单,防止任意配置注入 - ADMIN_TOKEN 最小长度要求 16 字符 - 补充安全响应头(X-Content-Type-Options, X-Frame-Options, Referrer-Policy) - /api/users/[id] 和 /api/limits 增加 token 鉴权 - console.error 敏感信息脱敏(config route) - 敏感值 mask 修复短值完全隐藏 输入校验: - admin 渠道接口校验 rate_multiplier > 0、sort_order >= 0、name 非空 - admin 订阅套餐接口校验 price > 0、validity_days > 0、sort_order >= 0 金额精度: - feeRate 字段精度从 Decimal(5,2) 提升到 Decimal(5,4) - calculatePayAmount 返回 string 避免 Number 中间转换精度丢失 - 支付宝查询订单增加金额有效性校验(isFinite && > 0) UI 统一: - 订阅管理「售卖」列改为 toggle switch 开关(与渠道管理一致) - 表单中 checkbox 改为 toggle switch - 列名统一为「启用售卖」,支持直接点击切换
This commit is contained in:
@@ -168,7 +168,7 @@ function buildText(locale: Locale) {
|
||||
colPrice: '价格',
|
||||
colOriginalPrice: '原价',
|
||||
colValidDays: '有效期',
|
||||
colEnabled: '售卖',
|
||||
colEnabled: '启用售卖',
|
||||
colGroupStatus: '分组状态',
|
||||
colActions: '操作',
|
||||
edit: '编辑',
|
||||
@@ -461,6 +461,25 @@ function SubscriptionsContent() {
|
||||
}
|
||||
};
|
||||
|
||||
/* --- toggle plan enabled --- */
|
||||
const handleToggleEnabled = async (plan: SubscriptionPlan) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/subscription-plans/${plan.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ for_sale: !plan.enabled }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setPlans((prev) => prev.map((p) => (p.id === plan.id ? { ...p, enabled: !p.enabled } : p)));
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
/* --- fetch user subs --- */
|
||||
const fetchSubs = async () => {
|
||||
if (!token || !subsUserId.trim()) return;
|
||||
@@ -705,20 +724,21 @@ function SubscriptionsContent() {
|
||||
: t.unitDay}
|
||||
</td>
|
||||
<td className={tdCls}>
|
||||
<span
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleEnabled(plan)}
|
||||
className={[
|
||||
'inline-block rounded-full px-2 py-0.5 text-xs font-medium',
|
||||
plan.enabled
|
||||
? isDark
|
||||
? 'bg-green-500/20 text-green-300'
|
||||
: 'bg-green-50 text-green-700'
|
||||
: isDark
|
||||
? 'bg-slate-700 text-slate-400'
|
||||
: 'bg-gray-100 text-gray-500',
|
||||
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors',
|
||||
plan.enabled ? 'bg-emerald-500' : isDark ? 'bg-slate-600' : 'bg-slate-300',
|
||||
].join(' ')}
|
||||
>
|
||||
{plan.enabled ? t.enabled : t.disabled}
|
||||
</span>
|
||||
<span
|
||||
className={[
|
||||
'inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform',
|
||||
plan.enabled ? 'translate-x-4.5' : 'translate-x-0.5',
|
||||
].join(' ')}
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
<td className={tdCls}>
|
||||
<span
|
||||
@@ -1083,19 +1103,24 @@ function SubscriptionsContent() {
|
||||
|
||||
{/* Enabled */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="form-enabled"
|
||||
type="checkbox"
|
||||
checked={formEnabled}
|
||||
onChange={(e) => setFormEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-slate-300"
|
||||
/>
|
||||
<label
|
||||
htmlFor="form-enabled"
|
||||
className={['text-sm', isDark ? 'text-slate-300' : 'text-slate-700'].join(' ')}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormEnabled(!formEnabled)}
|
||||
className={[
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
formEnabled ? 'bg-emerald-500' : isDark ? 'bg-slate-600' : 'bg-slate-300',
|
||||
].join(' ')}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
'inline-block h-4 w-4 rounded-full bg-white transition-transform',
|
||||
formEnabled ? 'translate-x-6' : 'translate-x-1',
|
||||
].join(' ')}
|
||||
/>
|
||||
</button>
|
||||
<span className={['text-sm', isDark ? 'text-slate-300' : 'text-slate-700'].join(' ')}>
|
||||
{t.fieldEnabled}
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user