gongxiangp2/components/EditMaterialModal.tsx
chenpei 383772d39f 🛡️ 服务器安全加固和FRP问题修复
主要更新:
1. FRP服务器诊断与修复
   - 发现并修复文件描述符耗尽问题(65,536 → 1,048,576)
   - 分析并解决用户上传失败问题
   - 确认SSH服务器安全未被攻破

2. 安全防护系统部署
   - 部署IP黑名单防火墙(封禁20+攻击者)
   - 设置SSH端口速率限制
   - 配置自动化定时任务(每小时更新)
   - 安装Fail2ban实时防护系统
   - 添加白名单保护机制

3. 内网访问优化
   - 改进内网检测逻辑
   - 添加网络状态API
   - 优化钉钉登录流程
   - 完善信息补充功能

4. 文档完善
   - FRP服务器诊断修复报告
   - SSH安全评估报告
   - Fail2ban使用指南
   - 文件描述符详解
   - 用户上传失败原因分析
   - 防火墙配置说明
   - 内网检测改进报告

修复的问题:
- ✅ 文件描述符耗尽导致的服务异常
- ✅ 用户上传失败(影响9.5小时)
- ✅ SSH暴力破解攻击(91,299+次)
- ✅ 第三方项目攻击影响
- ✅ 127.0.0.1误封禁问题

部署的安全措施:
- 三重防护体系(Fail2ban + 防火墙 + 强密码)
- 自动化监控和封禁
- 实时攻击拦截(秒级响应)
- 完整的管理和监控工具

测试环境:开发环境
影响范围:服务器安全、用户上传、系统稳定性
当前状态:✅ 所有问题已修复,系统运行正常
2025-11-06 13:42:36 +08:00

473 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useRef, useEffect } from 'react'
import Image from 'next/image'
interface EditMaterialModalProps {
material: {
id: string
previewUrl: string
productModel: string
categoryName: string
sceneName: string
materialType: string
renderer: string
authorName: string
}
onClose: () => void
onSuccess: () => void
}
export default function EditMaterialModal({ material, onClose, onSuccess }: EditMaterialModalProps) {
const [previewImage, setPreviewImage] = useState<File | null>(null)
const [previewUrl, setPreviewUrl] = useState<string>(material.previewUrl)
const [productModel, setProductModel] = useState(material.productModel)
const [categoryName, setCategoryName] = useState(material.categoryName)
const [sceneName, setSceneName] = useState(material.sceneName)
const [materialType, setMaterialType] = useState(material.materialType || '3DMAX')
const [renderer, setRenderer] = useState(material.renderer || 'CR')
const [authorName, setAuthorName] = useState(material.authorName)
const [categories, setCategories] = useState<Array<{ id: string; name: string }>>([])
const [scenes, setScenes] = useState<Array<{ id: string; name: string }>>([])
const [showNewCategoryInput, setShowNewCategoryInput] = useState(false)
const [newCategoryName, setNewCategoryName] = useState('')
const [showNewSceneInput, setShowNewSceneInput] = useState(false)
const [newSceneName, setNewSceneName] = useState('')
const [updating, setUpdating] = useState(false)
const [error, setError] = useState('')
const previewInputRef = useRef<HTMLInputElement>(null)
// 加载分类和场景列表
useEffect(() => {
fetchCategories()
fetchScenes()
}, [])
const fetchCategories = async () => {
try {
const response = await fetch('/api/categories')
if (response.ok) {
const data = await response.json()
setCategories(data)
}
} catch (error) {
console.error('Error fetching categories:', error)
}
}
const fetchScenes = async () => {
try {
const response = await fetch('/api/scenes')
if (response.ok) {
const data = await response.json()
setScenes(data)
}
} catch (error) {
console.error('Error fetching scenes:', error)
}
}
const handleAddNewCategory = async () => {
if (!newCategoryName.trim()) {
setError('请输入一级分类名称')
return
}
try {
const response = await fetch('/api/categories', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newCategoryName.trim() }),
})
if (response.ok) {
const newCategory = await response.json()
await fetchCategories()
setCategoryName(newCategory.name)
setNewCategoryName('')
setShowNewCategoryInput(false)
setError('')
} else {
const data = await response.json()
setError(data.error || '添加一级分类失败')
}
} catch (error) {
console.error('Error adding category:', error)
setError('添加一级分类失败')
}
}
const handleAddNewScene = async () => {
if (!newSceneName.trim()) {
setError('请输入场景名称')
return
}
try {
const response = await fetch('/api/scenes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newSceneName.trim() }),
})
if (response.ok) {
const newScene = await response.json()
await fetchScenes()
setSceneName(newScene.name)
setNewSceneName('')
setShowNewSceneInput(false)
setError('')
} else {
const data = await response.json()
setError(data.error || '添加场景失败')
}
} catch (error) {
console.error('Error adding scene:', error)
setError('添加场景失败')
}
}
const handlePreviewChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
if (!file.type.startsWith('image/')) {
setError('请选择图片文件')
return
}
setPreviewImage(file)
setPreviewUrl(URL.createObjectURL(file))
setError('')
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (!productModel || !categoryName || !sceneName || !renderer || !authorName) {
setError('请填写所有字段')
return
}
setUpdating(true)
try {
const formData = new FormData()
formData.append('id', material.id)
formData.append('productModel', productModel)
formData.append('categoryName', categoryName)
formData.append('sceneName', sceneName)
formData.append('materialType', materialType)
formData.append('renderer', renderer)
formData.append('authorName', authorName)
if (previewImage) {
formData.append('previewImage', previewImage)
}
const response = await fetch('/api/materials/update', {
method: 'PUT',
body: formData,
})
if (response.ok) {
onSuccess()
} else {
const data = await response.json()
setError(data.error || '更新失败')
}
} catch (error) {
console.error('Update error:', error)
setError('更新失败,请稍后再试')
} finally {
setUpdating(false)
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-apple max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-apple-lg">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">编辑素材</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl leading-none"
>
×
</button>
</div>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-apple">
{error}
</div>
)}
{/* Preview Image */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
预览图
</label>
<div
onClick={() => previewInputRef.current?.click()}
className="border-2 border-dashed border-gray-300 rounded-apple p-4 text-center cursor-pointer hover:border-primary-500 transition-colors"
>
<div className="relative w-full h-48">
<Image
src={previewUrl}
alt="预览"
fill
unoptimized
className="object-contain"
/>
</div>
<p className="text-xs text-gray-400 mt-2">
点击更换预览图(可选)
</p>
</div>
<input
ref={previewInputRef}
type="file"
accept="image/*"
onChange={handlePreviewChange}
className="hidden"
/>
</div>
{/* Product Model */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
商品型号 *
</label>
<input
type="text"
value={productModel}
onChange={(e) => {
// 只允许英文、数字和常见符号(- _ .),过滤中文,自动转大写
const value = e.target.value.replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase()
setProductModel(value)
}}
className="input-apple"
placeholder="例如:XY-2024-001"
required
/>
</div>
{/* Category Name (一级分类) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
一级分类(使用店铺) *
</label>
{!showNewCategoryInput ? (
<div className="space-y-2">
<select
value={categoryName}
onChange={(e) => {
if (e.target.value === '__add_new__') {
setShowNewCategoryInput(true)
setCategoryName('')
} else {
setCategoryName(e.target.value)
}
}}
className="input-apple"
required
>
<option value="">请选择一级分类</option>
{categories.map((category) => (
<option key={category.id} value={category.name}>
{category.name}
</option>
))}
<option value="__add_new__">➕ 添加新分类</option>
</select>
</div>
) : (
<div className="space-y-2">
<div className="flex gap-2">
<input
type="text"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
className="input-apple flex-1"
placeholder="输入新的一级分类名称"
autoFocus
/>
<button
type="button"
onClick={handleAddNewCategory}
className="px-4 py-2 bg-blue-600 text-white rounded-apple hover:bg-blue-700 transition-colors"
>
确认
</button>
<button
type="button"
onClick={() => {
setShowNewCategoryInput(false)
setNewCategoryName('')
setError('')
}}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-apple hover:bg-gray-200 transition-colors"
>
取消
</button>
</div>
</div>
)}
</div>
{/* Scene Name (二级分类) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
二级分类(场景名称) *
</label>
{!showNewSceneInput ? (
<div className="space-y-2">
<select
value={sceneName}
onChange={(e) => {
if (e.target.value === '__add_new__') {
setShowNewSceneInput(true)
setSceneName('')
} else {
setSceneName(e.target.value)
}
}}
className="input-apple"
required
>
<option value="">请选择场景</option>
{scenes.map((scene) => (
<option key={scene.id} value={scene.name}>
{scene.name}
</option>
))}
<option value="__add_new__">+ 添加新场景</option>
</select>
</div>
) : (
<div className="space-y-2">
<div className="flex space-x-2">
<input
type="text"
value={newSceneName}
onChange={(e) => setNewSceneName(e.target.value)}
className="input-apple"
placeholder="输入新场景名称"
autoFocus
/>
<button
type="button"
onClick={handleAddNewScene}
className="btn-primary whitespace-nowrap"
>
确定
</button>
<button
type="button"
onClick={() => {
setShowNewSceneInput(false)
setNewSceneName('')
setError('')
}}
className="btn-secondary whitespace-nowrap"
>
取消
</button>
</div>
<p className="text-xs text-gray-500">
添加后将自动加入到场景列表中
</p>
</div>
)}
</div>
{/* Material Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
素材类型 *
</label>
<select
value={materialType}
onChange={(e) => setMaterialType(e.target.value)}
className="input-apple"
required
>
<option value="3DMAX">3DMAX</option>
<option value="PSD">PSD</option>
<option value="AI">AI</option>
<option value="CAD">CAD</option>
<option value="C4D">C4D</option>
<option value="其他">其他</option>
</select>
</div>
{/* Renderer (仅3DMAX显示) */}
{materialType === '3DMAX' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
渲染器 *
</label>
<select
value={renderer}
onChange={(e) => setRenderer(e.target.value)}
className="input-apple"
required
>
<option value="CR">CR</option>
<option value="VR">VR</option>
<option value="其它">其它</option>
</select>
</div>
)}
{/* Author Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
作者花名 *
</label>
<input
type="text"
value={authorName}
onChange={(e) => setAuthorName(e.target.value)}
className="input-apple"
placeholder="作者花名"
required
/>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onClose}
className="btn-secondary"
disabled={updating}
>
取消
</button>
<button
type="submit"
className="btn-primary"
disabled={updating}
>
{updating ? '更新中...' : '保存修改'}
</button>
</div>
</form>
</div>
</div>
)
}