diff --git a/test-permission-flow.mjs b/test-permission-flow.mjs new file mode 100644 index 0000000..7a94895 --- /dev/null +++ b/test-permission-flow.mjs @@ -0,0 +1,132 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:13001'; +const API = 'http://localhost:3001'; + +const USERS = [ + { label: 'SUPER_ADMIN (admin)', username: 'admin', password: 'admin123', expectedPerms: 26 }, + { label: 'TENANT_ADMIN (ta_admin)', username: 'ta_admin', password: 'pass123', expectedPerms: 21 }, + { label: 'USER (user1)', username: 'user1', password: 'pass123', expectedPerms: 5 }, +]; + +async function login(page, username, password) { + await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + await page.locator('input[type="text"]').first().fill(username); + await page.locator('input[type="password"]').first().fill(password); + await page.locator('button[type="submit"]').click(); + await page.waitForURL('**/'); + await page.waitForTimeout(1000); +} + +async function getApiKey(page) { + return await page.evaluate(() => localStorage.getItem('kb_api_key') || ''); +} + +async function run() { + const browser = await chromium.launch({ headless: true }); + const results = []; + + for (const u of USERS) { + console.log(`\n${'='.repeat(60)}`); + console.log(`🧑‍💻 ${u.label}`); + console.log(`${'='.repeat(60)}`); + + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const r = { user: u.label, login: false, perms: 0, nav: [], settingsTabs: [], api: {} }; + + try { + // Login + await login(page, u.username, u.password); + r.login = true; + console.log(` ✅ 登录成功`); + + // Get API key from page + const apiKey = await getApiKey(page); + console.log(` 🔑 API Key: ${apiKey.substring(0, 16)}...`); + + // Check navigation sidebar + r.nav = await page.evaluate(() => { + return Array.from(document.querySelectorAll('aside, nav, [class*="sidebar"], [class*="navigation"]')) + .flatMap(el => Array.from(el.querySelectorAll('a, button'))) + .map(el => (el.textContent || '').trim()) + .filter(Boolean) + .filter(t => !t.startsWith('AuraK') && !t.startsWith('Admin')) + .filter((v, i, a) => a.indexOf(v) === i); + }); + console.log(` 📋 导航: ${r.nav.join(', ')}`); + + // Check settings tabs + await page.goto(`${BASE}/settings`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + + r.settingsTabs = await page.evaluate(() => { + return Array.from(document.querySelectorAll('aside button, [class*="sidebar"] button')) + .map(b => (b.textContent || '').trim()) + .filter(Boolean) + .filter((v, i, a) => a.indexOf(v) === i); + }); + console.log(` ⚙️ 设置Tab: ${r.settingsTabs.join(', ')}`); + + // API permission checks + const headers = { 'x-api-key': apiKey }; + + // /api/permissions/mine + const permRes = await fetch(`${API}/api/permissions/mine`, { headers }); + const permData = await permRes.json(); + r.perms = (permData.permissions || []).length; + console.log(` 🔒 权限数: ${r.perms} (期望: ${u.expectedPerms})`); + + // /api/users (user:view) + const usersRes = await fetch(`${API}/api/users`, { headers }); + r.api['user:view'] = usersRes.status; + console.log(` GET /api/users: ${usersRes.ok ? '✅' : '❌'} (${usersRes.status})`); + + // /api/roles (TENANT_ADMIN+) + const rolesRes = await fetch(`${API}/api/roles`, { headers }); + r.api['roles'] = rolesRes.status; + console.log(` GET /api/roles: ${rolesRes.ok ? '✅' : '❌'} (${rolesRes.status})`); + + // Check assessment access + await page.goto(`${BASE}/assessment`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + + const assessOK = await page.evaluate(() => { + const body = document.body.textContent || ''; + return body.includes('AI协作技巧') || body.includes('开始专业评估'); + }); + r.api['assessment'] = assessOK; + console.log(` 📝 考核页可访问: ${assessOK ? '✅' : '❌'}`); + + } catch (err) { + console.error(` ❌ 错误: ${err.message}`); + } finally { + await page.close(); + } + + results.push(r); + } + + // Summary table + console.log(`\n${'='.repeat(60)}`); + console.log('📊 权限测试汇总'); + console.log(`${'='.repeat(60)}`); + console.log(`用户 登录 权限 users roles 设置Tab`); + console.log(`${'─'.repeat(60)}`); + for (const r of results) { + const tabStr = r.settingsTabs.filter(t => !['基本设置'].includes(t)).join('/') || '-'; + console.log( + r.user.padEnd(22), + r.login ? '✅' : '❌', + String(r.perms).padStart(3), + r.api['user:view'] === 200 ? ' ✅' : ' ❌', + String(r.api['roles']).padStart(3), + tabStr, + ); + } + console.log(`${'='.repeat(60)}`); + console.log('✅ 测试完成'); + await browser.close(); +} + +run().catch(e => { console.error('❌', e.message); process.exit(1); }); diff --git a/web/components/views/SettingsView.tsx b/web/components/views/SettingsView.tsx index 7355ec4..5c1663a 100644 --- a/web/components/views/SettingsView.tsx +++ b/web/components/views/SettingsView.tsx @@ -419,7 +419,15 @@ export const SettingsView: React.FC = ({ const [passwordChangeUserData, setPasswordChangeUserData] = useState<{ userId: string, newPassword: string } | null>(null); // --- Edit User State --- - const [editUserData, setEditUserData] = useState<{ userId: string, username: string, displayName: string } | null>(null); + const [editUserData, setEditUserData] = useState<{ + userId: string; + username: string; + displayName: string; + memberId?: string; + tenantId?: string; + role?: string; + isAdmin?: boolean; + } | null>(null); const handleToggleUserAdmin = async (userId: string, newAdminStatus: boolean) => { try { @@ -582,10 +590,26 @@ export const SettingsView: React.FC = ({ const handleUpdateUser = async () => { if (!editUserData) return; try { + // 更新基本信息 await userService.updateUserInfo(editUserData.userId, { username: editUserData.username, - displayName: editUserData.displayName + displayName: editUserData.displayName, + isAdmin: editUserData.role === 'SUPER_ADMIN', }); + + // 更新角色(通过 tenant member API) + if (editUserData.memberId && editUserData.tenantId) { + const res = await fetch(`/api/v1/tenants/${editUserData.tenantId}/members/${editUserData.userId}`, { + method: 'PATCH', + headers: { + 'x-api-key': authToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ role: editUserData.role }), + }); + if (!res.ok) console.warn('Role update returned', res.status); + } + showSuccess(t('featureUpdated')); setEditUserData(null); fetchUsers(); @@ -1028,11 +1052,63 @@ export const SettingsView: React.FC = ({ required /> -
-

{t('globalUserNote') || "Note"}

-

- {t('roleManagedInOrg') || "Roles are managed within organizations."} + {/* 角色选择 */} +

+ +
+ {['USER', 'TENANT_ADMIN', 'SUPER_ADMIN'].map(r => ( + + ))} +
+ {editUserData.role === 'SUPER_ADMIN' && currentUser?.role !== 'SUPER_ADMIN' && ( +

仅超级管理员可提升用户为超级管理员

+ )} +
+ + {/* 权限预览 */} +
+

+ 该角色的权限 ({editUserData.role === 'SUPER_ADMIN' ? '26项' : editUserData.role === 'TENANT_ADMIN' ? '21项' : '5项'})

+
+ {editUserData.role === 'SUPER_ADMIN' && ( + ['全部权限:用户管理、租户管理、知识库、考核评估、模型配置、插件管理、系统设置'].map(p => ( + {p} + )) + )} + {editUserData.role === 'TENANT_ADMIN' && ( + ['查看用户','创建用户','编辑用户','重置密码','管理知识库','管理考核','管理模型','管理插件'].map(p => ( + {p} + )) + )} + {editUserData.role === 'USER' && ( + ['使用知识库','参与考核'].map(p => ( + {p} + )) + )} +
@@ -1054,6 +1130,7 @@ export const SettingsView: React.FC = ({ {t('username')} {t('displayName') || t('name')} {t('organizations')} + 角色 {t('createdAt')} {t('actions')} @@ -1108,6 +1185,22 @@ export const SettingsView: React.FC = ({ {t('noOrganization')} )} + + {(() => { + const tm = user.tenantMembers?.filter((m: any) => m.tenant?.name !== 'Default')?.[0]; + const role = user.isAdmin ? 'SUPER_ADMIN' : tm?.role || 'USER'; + const colors: Record = { + SUPER_ADMIN: 'bg-red-50 text-red-700 border-red-100', + TENANT_ADMIN: 'bg-indigo-50 text-indigo-700 border-indigo-100', + USER: 'bg-slate-50 text-slate-500 border-slate-100', + }; + return ( + + {role === 'SUPER_ADMIN' ? '超级管理员' : role === 'TENANT_ADMIN' ? '管理员' : '用户'} + + ); + })()} +

{new Date(user.createdAt).toLocaleDateString()} @@ -1121,10 +1214,15 @@ export const SettingsView: React.FC = ({ onClick={(e) => { e.preventDefault(); e.stopPropagation(); + const tm = user.tenantMembers?.filter((m: any) => m.tenant?.name !== 'Default')?.[0]; setEditUserData({ userId: user.id, username: user.username, - displayName: user.displayName || '' + displayName: user.displayName || '', + memberId: tm?.id, + tenantId: tm?.tenantId, + role: user.isAdmin ? 'SUPER_ADMIN' : tm?.role || 'USER', + isAdmin: !!user.isAdmin, }); }} className="p-2 rounded-lg text-slate-400 hover:text-indigo-600 hover:bg-white shadow-sm transition-all"