主要更新: 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 + 防火墙 + 强密码) - 自动化监控和封禁 - 实时攻击拦截(秒级响应) - 完整的管理和监控工具 测试环境:开发环境 影响范围:服务器安全、用户上传、系统稳定性 当前状态:✅ 所有问题已修复,系统运行正常
299 lines
9.1 KiB
TypeScript
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>
|
|
)
|
|
}
|
|
|