Files
meijiaka-zy/tauri-app/src/components/Layout/Sidebar.tsx
T
小鱼开发 95e55293c6 security: 全面生产安全加固与部署修复
后端安全:
- DEBUG 默认 True → False
- 彻底移除 AUTH_BYPASS 认证绕过
- 验证码不再明文打印到日志
- 上传接口增加大小限制(500MB/20MB/100MB)与魔数校验
- python-jose → PyJWT, 更新 requirements.lock/uv.lock
- Bandit 恢复关键规则(B104/B301/B305/B314/B324/B603/B607)
- 修复 5 处 try_except_pass, 15 处加 nosec 注释
- 启用 Bandit pre-commit 钩子

前端安全:
- 配置完整 CSP 策略
- 收紧 Capabilities(fs:allow-read-file → $RESOURCE/**)
- 移除硬编码 devToken
- 清理前端 TODO(美家卡智影命名统一)

部署修复:
- docker-compose.prod 增加 alembic 迁移步骤
- api + scheduler 增加 Redis 心跳健康检查
- Nginx 添加安全响应头
- Nginx client_max_body_size 100M → 500M
- .env.example 补充 UPLOAD_MAX_* 配置与安全注释

其他:
- /voice/upload 合并到 /upload/audio
- Rust 上传增加文件大小检查
- 清理 Rust 19 处 println! + 前端 21 处 console.info
- 修复 VideoCompose.tsx toast 未导入(已有bug)
2026-05-10 23:31:34 +08:00

216 lines
7.1 KiB
TypeScript

import { useState } from 'react';
import { createNewProject, useAuthStore } from '../../store';
import { useNavigation } from '../../contexts/NavigationContext';
import './Sidebar.css';
interface NavItem {
id: string;
label: string;
icon: string;
children?: { id: string; label: string }[];
}
const navItems: NavItem[] = [
{
id: 'video-creation',
label: '视频创作',
icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z',
},
{
id: 'content-management',
label: '内容管理',
icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
children: [
{ id: 'voice-material', label: '声音复刻' },
{ id: 'my-works', label: '我的作品' },
],
},
{
id: 'settings',
label: '系统设置',
icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
children: [
{ id: 'theme-settings', label: '主题设置' },
{ id: 'about-us', label: '关于我们' },
{ id: 'system-update', label: '系统更新' },
],
},
];
interface SidebarProps {
currentPath: string;
onNavigate: (path: string) => void;
}
export default function Sidebar({ currentPath, onNavigate }: SidebarProps) {
const { appEnvironment } = useNavigation();
const authUser = useAuthStore((s) => s.user);
const [expandedItems, setExpandedItems] = useState<Set<string>>(
new Set(['content-management', 'settings'])
);
const toggleExpand = (id: string) => {
setExpandedItems(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const handleClick = (item: NavItem) => {
if (item.children) {
toggleExpand(item.id);
// Navigate to first child
if (!expandedItems.has(item.id)) {
onNavigate(item.children[0].id);
}
} else {
onNavigate(item.id);
}
};
const handleNewProject = async (e: React.MouseEvent) => {
e.stopPropagation();
await createNewProject();
onNavigate('video-creation');
};
const isActive = (id: string) => currentPath === id;
return (
<aside className="sidebar">
<div className="sidebar-header">
<div className="sidebar-logo">
<img
src="/assets/logo.png"
alt="美家卡智影"
style={{ width: 28, height: 24, objectFit: 'contain' }}
/>
<span className="sidebar-title"></span>
</div>
</div>
<nav className="sidebar-nav">
{navItems.map(item => (
<div key={item.id} className="nav-group">
<button
className={`nav-item ${isActive(item.id) ? 'active' : ''} ${
item.children && item.children.some(c => isActive(c.id)) ? 'child-active' : ''
}`}
onClick={() => handleClick(item)}
>
<svg
className="nav-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d={item.icon} />
</svg>
<span className="nav-label">{item.label}</span>
{item.id === 'video-creation' && (
<span
className="nav-new-project"
onClick={handleNewProject}
title="新建项目"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleNewProject(e as unknown as React.MouseEvent);
}
}}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="8" y1="12" x2="16" y2="12" />
</svg>
</span>
)}
{item.children && (
<svg
className={`nav-chevron ${expandedItems.has(item.id) ? 'expanded' : ''}`}
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 18 15 12 9 6" />
</svg>
)}
</button>
{item.children && expandedItems.has(item.id) && (
<div className="nav-children">
{item.children.map(child => (
<button
key={child.id}
className={`nav-child-item ${isActive(child.id) ? 'active' : ''}`}
onClick={() => onNavigate(child.id)}
>
<span>{child.label}</span>
</button>
))}
</div>
)}
</div>
))}
</nav>
{appEnvironment !== 'production' && (
<div
style={{
margin: '0 var(--spacing-md) var(--spacing-sm)',
padding: 'var(--spacing-xs) var(--spacing-sm)',
background: 'var(--color-warning)',
color: '#fff',
borderRadius: '6px',
fontSize: '12px',
fontWeight: 600,
textAlign: 'center',
letterSpacing: '0.5px',
}}
>
{appEnvironment === 'staging' ? '测试环境' : appEnvironment === 'development' ? '开发环境' : appEnvironment}
</div>
)}
<div className="sidebar-footer">
<div className="sidebar-user" onClick={() => onNavigate('profile')} title="个人中心">
<img
src={authUser?.avatar || '/default-avatar.svg'}
alt="avatar"
className="user-avatar"
style={{ objectFit: 'cover' }}
/>
<div className="user-info">
<span className="user-name">{authUser?.nickname || '用户'}</span>
</div>
</div>
</div>
</aside>
);
}