diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 2c314ce..d0e83d6 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -193,6 +193,13 @@ router.post('/login', async (req, res) => { const token = generateToken(user); + // 查询用户角色(通过 UserRole 中间表) + const userRoles = await UserRole.findAll({ + where: { UserId: user.userId }, + include: [{ model: Role }], + }); + const roles = userRoles.map(ur => ur.Role || ur); + user.lastLoginTime = new Date(); user.lastLoginIp = req.ip || req.connection.remoteAddress; user.loginCount = 0; @@ -208,6 +215,11 @@ router.post('/login', async (req, res) => { email: user.email, realName: user.realName, avatar: user.avatar, + roles: roles.map(r => ({ + roleId: r.roleId, + roleName: r.roleName, + roleCode: r.roleCode, + })), }, token, }, diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index fa6de8b..b109c13 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -116,7 +116,13 @@ export const AuthProvider = ({ children }) => { const hasPermission = permission => { if (!user) return false; - return true; + const roles = user.roles || []; + // 管理员拥有所有权限 + if (roles.some(r => r.roleCode === 'admin')) return true; + // 检查具体权限 + if (permission === 'admin') return roles.some(r => r.roleCode === 'admin'); + // roleCode 格式的权限直接匹配 + return roles.some(r => r.roleCode === permission); }; const value = { diff --git a/frontend/src/pages/InventoryManagement.jsx b/frontend/src/pages/InventoryManagement.jsx index af6edf9..0cee316 100644 --- a/frontend/src/pages/InventoryManagement.jsx +++ b/frontend/src/pages/InventoryManagement.jsx @@ -79,10 +79,10 @@ const InventoryManagement = () => { ...searchParamsObj, }; const res = await api.get('/inventory/plans', { params }); - setPlans(res.data.plans || []); + setPlans(res.plans || []); setPagination(prev => ({ ...prev, - total: res.data.total || 0, + total: res.total || 0, })); } catch (error) { message.error('获取盘点计划失败'); @@ -95,9 +95,9 @@ const InventoryManagement = () => { try { const res = await api.get('/inventory/stats/dashboard'); setStats({ - totalPlans: res.data.totalPlans || 0, - completedPlans: res.data.completedPlans || 0, - inProgressPlans: res.data.inProgressPlans || 0, + totalPlans: res.totalPlans || 0, + completedPlans: res.completedPlans || 0, + inProgressPlans: res.inProgressPlans || 0, }); } catch (error) { console.error('获取统计失败', error); @@ -107,7 +107,7 @@ const InventoryManagement = () => { const fetchRooms = async () => { try { const res = await api.get('/rooms', { params: { pageSize: 1000 } }); - setRooms(Array.isArray(res.data) ? res.data : res.data.rooms || []); + setRooms(Array.isArray(res) ? res : res.rooms || []); } catch (error) { console.error('获取机房失败', error); } @@ -116,7 +116,7 @@ const InventoryManagement = () => { const fetchRacks = async () => { try { const res = await api.get('/racks', { params: { pageSize: 1000 } }); - const allRacks = Array.isArray(res.data) ? res.data : res.data.racks || []; + const allRacks = Array.isArray(res) ? res : res.racks || []; setRacks(allRacks); setFilteredRacks(allRacks); } catch (error) { diff --git a/frontend/src/utils/secureStorage.js b/frontend/src/utils/secureStorage.js index 9f2e0cc..6584ecb 100644 --- a/frontend/src/utils/secureStorage.js +++ b/frontend/src/utils/secureStorage.js @@ -1,10 +1,50 @@ const STORAGE_PREFIX = 'idc_'; const ENCRYPTION_ENABLED = import.meta.env.PROD; +/** + * 生成运行时密钥(基于浏览器指纹) + * 密钥在每次会话中动态生成,不会出现在源码或构建产物中 + * 同一浏览器同一域名下密钥保持一致,确保刷新页面后仍可解密 + */ +const getOrCreateRuntimeKey = () => { + const KEY_STORAGE_ID = '__idc_sk'; + let key = sessionStorage.getItem(KEY_STORAGE_ID); + if (key) return key; + + // 基于浏览器特征生成指纹作为密钥种子 + const fingerprint = [ + navigator.userAgent, + screen.width, + screen.height, + screen.colorDepth, + new Date().getTimezoneOffset(), + navigator.language, + ].join('|'); + + // 使用简单的哈希混合生成 32 字符密钥 + let hash = 0; + for (let i = 0; i < fingerprint.length; i++) { + const char = fingerprint.charCodeAt(i); + hash = ((hash << 5) - hash + char) | 0; + } + + // 生成 32 字符的随机密钥 + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + key = ''; + let seed = Math.abs(hash) + Date.now(); + for (let i = 0; i < 32; i++) { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + key += chars[seed % chars.length]; + } + + sessionStorage.setItem(KEY_STORAGE_ID, key); + return key; +}; + const simpleEncrypt = (text) => { if (!ENCRYPTION_ENABLED) return text; - - const key = import.meta.env.VITE_STORAGE_KEY || 'idc_default_secret_key_2024'; + + const key = getOrCreateRuntimeKey(); let result = ''; for (let i = 0; i < text.length; i++) { result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length)); @@ -14,9 +54,9 @@ const simpleEncrypt = (text) => { const simpleDecrypt = (encrypted) => { if (!ENCRYPTION_ENABLED) return encrypted; - + try { - const key = import.meta.env.VITE_STORAGE_KEY || 'idc_default_secret_key_2024'; + const key = getOrCreateRuntimeKey(); const decoded = decodeURIComponent(atob(encrypted)); let result = ''; for (let i = 0; i < decoded.length; i++) { @@ -51,22 +91,22 @@ export const secureStorage = { try { const storageKey = `${STORAGE_PREFIX}${key}`; const encrypted = localStorage.getItem(storageKey); - + if (!encrypted) return null; - + const decrypted = simpleDecrypt(encrypted); if (!decrypted) { localStorage.removeItem(storageKey); return null; } - + const data = JSON.parse(decrypted); - + if (data.expiry && Date.now() > data.expiry) { localStorage.removeItem(storageKey); return null; } - + return data.value; } catch (error) { console.error('[SecureStorage] Get error:', error);