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

299 lines
9.1 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Header from '@/components/Header'
interface Material {
id: string
userId: string
productModel: string
categoryName: string
sceneName: string
materialType: string
renderer: string
authorName: string
}
export default function EditMaterialPage({ params }: { params: { id: string } }) {
const { data: session, status } = useSession()
const router = useRouter()
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [material, setMaterial] = useState<Material | null>(null)
const [productModel, setProductModel] = useState('')
const [categoryName, setCategoryName] = useState('')
const [sceneName, setSceneName] = useState('')
const [materialType, setMaterialType] = useState('')
const [renderer, setRenderer] = useState('')
const [authorName, setAuthorName] = useState('')
const [categories, setCategories] = useState<Array<{ id: string; name: string }>>([])
const [scenes, setScenes] = useState<Array<{ id: string; name: string }>>([])
const materialTypes = [
'3DMAX', 'PSD', 'AI', 'CAD', 'C4D', '材质', '模特', '标准饰品', '其他'
]
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login')
return
}
if (status === 'authenticated') {
fetchMaterial()
fetchCategories()
fetchScenes()
}
}, [status, params.id])
const fetchMaterial = async () => {
try {
const response = await fetch(`/api/materials/${params.id}`)
if (!response.ok) {
throw new Error('获取素材信息失败')
}
const data = await response.json()
// 权限检查
if (data.userId !== (session?.user as any)?.id && (session?.user as any)?.role !== 'ADMIN') {
setError('您没有权限编辑此素材')
setTimeout(() => router.push('/'), 2000)
return
}
setMaterial(data)
setProductModel(data.productModel || '')
setCategoryName(data.categoryName || '')
setSceneName(data.sceneName || '')
setMaterialType(data.materialType || '')
setRenderer(data.renderer || '')
setAuthorName(data.authorName || '')
setLoading(false)
} catch (error: any) {
console.error('获取素材失败:', error)
setError(error.message || '获取素材信息失败')
setLoading(false)
}
}
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)
}
}
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)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setSuccess('')
if (!productModel || !categoryName || !sceneName || !materialType || !renderer || !authorName) {
setError('请填写所有必填字段')
return
}
setSubmitting(true)
try {
const response = await fetch(`/api/materials/${params.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productModel,
categoryName,
sceneName,
materialType,
renderer,
authorName
})
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || '更新失败')
}
setSuccess('素材信息更新成功!')
setTimeout(() => {
router.push('/')
}, 1500)
} catch (error: any) {
console.error('更新失败:', error)
setError(error.message || '更新失败,请稍后重试')
} finally {
setSubmitting(false)
}
}
if (status === 'loading' || loading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="text-center text-gray-600 dark:text-gray-400">加载中...</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">编辑素材信息</h1>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-600 rounded-lg">
{error}
</div>
)}
{success && (
<div className="mb-4 p-4 bg-green-50 border border-green-200 text-green-600 rounded-lg">
{success}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
产品型号 *
</label>
<input
type="text"
value={productModel}
onChange={(e) => setProductModel(e.target.value)}
className="input-apple"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
分类 *
</label>
<select
value={categoryName}
onChange={(e) => setCategoryName(e.target.value)}
className="input-apple"
required
>
<option value="">请选择分类</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.name}>{cat.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
场景 *
</label>
<select
value={sceneName}
onChange={(e) => setSceneName(e.target.value)}
className="input-apple"
required
>
<option value="">请选择场景</option>
{scenes.map((scene) => (
<option key={scene.id} value={scene.name}>{scene.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
素材类型 *
</label>
<select
value={materialType}
onChange={(e) => setMaterialType(e.target.value)}
className="input-apple"
required
>
<option value="">请选择素材类型</option>
{materialTypes.map((type) => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
渲染器 *
</label>
<input
type="text"
value={renderer}
onChange={(e) => setRenderer(e.target.value)}
className="input-apple"
placeholder="例如: V-Ray, Corona, Arnold"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
作者 *
</label>
<input
type="text"
value={authorName}
onChange={(e) => setAuthorName(e.target.value)}
className="input-apple"
required
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => router.back()}
className="flex-1 px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-apple font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
disabled={submitting}
>
取消
</button>
<button
type="submit"
className="flex-1 btn-primary"
disabled={submitting}
>
{submitting ? '保存中...' : '保存修改'}
</button>
</div>
</form>
</div>
</div>
</div>
)
}