diff --git a/.env.example b/.env.example index b0974f78..8f630280 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,12 @@ RERANKER_ENDPOINT_URL=https://www.dmxapi.cn/v1/rerank RERANKER_PROVIDER=openai_compatible RERANKER_MODEL=bge-reranker-v2-m3-free RERANKER_API_KEY= + +# --- 9. 系统管理员与认证准入配置 --- +# 超级管理员凭据 (系统启动时自动初始化为 role-admin) +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin-secret-password-123 + +# 全局准入策略 (是否开放自主注册与注册是否必须审批) +AUTH_REGISTRATION_ENABLED=true +AUTH_REQUIRE_APPROVAL=false diff --git a/web/src/App.tsx b/web/src/App.tsx index 4986ad6b..8d2c764e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10,8 +10,14 @@ import { CommentSidePanel } from './components/doc/CommentSidePanel'; import { DocGovActionBar } from './components/doc/DocGovActionBar'; import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard'; import { UserProjectRightsModal } from './components/user/UserProjectRightsModal'; -import { SettingsModal } from './components/user/SettingsModal'; +import { UserSettingsModal } from './components/user/UserSettingsModal'; +import { AdminSettingsModal } from './components/admin/AdminSettingsModal'; import { DocVisibilityModal } from './components/doc/DocVisibilityModal'; +import { LoginView } from './components/auth/LoginView'; +import { FirstLoginPasswordModal } from './components/auth/FirstLoginPasswordModal'; +import { DevRoleSwitcher } from './components/dev/DevRoleSwitcher'; +import { authClient } from './api/auth'; +import { LoginResult, SystemAuthConfig } from './types/auth'; import { markdownToBlockAST } from './engine/markdownToBlock'; import { blockASTToMarkdown } from './engine/blockToMarkdown'; import { recomputeNumbering } from './engine/autoNumber'; @@ -327,12 +333,41 @@ export default function App() { const [activeDocId, setActiveDocId] = useState('doc-001'); const [deckDrawerOpen, setDeckDrawerOpen] = useState(false); + // 认证与会话状态 + const [session, setSession] = useState(() => authClient.loadSavedSession()); + const [systemEnv, setSystemEnv] = useState('dev'); + const [systemAuthConfig, setSystemAuthConfig] = useState({ + allowRegistration: true, + requireApproval: false, + }); + // 模态浮层控制 const [rightsModalOpen, setRightsModalOpen] = useState(false); - const [settingsModalOpen, setSettingsModalOpen] = useState(false); + const [userSettingsOpen, setUserSettingsOpen] = useState(false); + const [adminSettingsOpen, setAdminSettingsOpen] = useState(false); const [docVisibilityModalOpen, setDocVisibilityModalOpen] = useState(false); const [targetDocForVisibility, setTargetDocForVisibility] = useState(null); + useEffect(() => { + authClient.getSystemAuthConfig().then(setSystemAuthConfig).catch(() => {}); + const saved = authClient.loadSavedSession(); + if (saved) { + setSession(saved); + if (saved.env) setSystemEnv(saved.env); + const match = allUsers.find((u) => u.id === saved.user.id); + if (match) { + setCurrentUser(match); + } else { + setCurrentUser(saved.user); + } + } + }, []); + + const handleLogout = () => { + authClient.logout(); + setSession(null); + }; + // 核心功能权限计算 const canCreateProject = useMemo(() => { return hasPermission(currentUser, roles, 'project:create'); @@ -1240,6 +1275,26 @@ export default function App() { })); }; + // 未登录拦截门禁:展示现代化医疗合规登录与注册页面 + if (!session) { + return ( + { + setSession(newSession); + if (newSession.env) setSystemEnv(newSession.env); + const match = allUsers.find((u) => u.id === newSession.user.id); + if (match) { + setCurrentUser(match); + } else { + setCurrentUser(newSession.user); + } + }} + systemConfig={systemAuthConfig} + env={systemEnv} + /> + ); + } + return ( <> {}} onOpenRightsModal={() => setRightsModalOpen(true)} - onOpenSettingsModal={() => setSettingsModalOpen(true)} + onOpenUserSettings={() => setUserSettingsOpen(true)} + onOpenAdminSettings={() => setAdminSettingsOpen(true)} chatContent={
{ const fresh = allUsers.find((x) => x.id === u.id) || u; setCurrentUser(fresh); @@ -1431,22 +1488,57 @@ export default function App() { onRoleChangeForDoc={handleRoleChangeForDoc} /> - {/* 系统权限与角色管理控制台 */} - setSettingsModalOpen(false)} + {/* 首次登录强制改密阻断弹窗 */} + { + const updated = { ...session, mustChangePassword: false }; + setSession(updated); + authClient.saveSession(updated); + }} + onLogout={handleLogout} + /> + + {/* 用户个人设置与偏好中心 (全员可用) */} + setUserSettingsOpen(false)} + currentUser={currentUser} + roles={roles} + onUpdateProfile={(updatedProfile) => { + setAllUsers((prev) => + prev.map((u) => (u.id === currentUser.id ? { ...u, ...updatedProfile } : u)) + ); + setCurrentUser((prev) => ({ ...prev, ...updatedProfile })); + }} + onLogout={handleLogout} + /> + + {/* 系统管理员独占治理控制台 (仅 role-admin 可入) */} + setAdminSettingsOpen(false)} + currentUser={currentUser} roles={roles} onSaveRoles={(newRoles) => setRoles(newRoles)} - users={allUsers} - currentUser={currentUser} - onUpdateUserRole={(userId, roleId) => { - setAllUsers((prev) => - prev.map((u) => (u.id === userId ? { ...u, roleId } : u)) - ); - if (currentUser.id === userId) { - setCurrentUser((prev) => ({ ...prev, roleId })); - } + allUsers={allUsers} + onRefreshUsers={() => { + authClient + .getAdminUsers() + .then((users) => { + setAllUsers(users); + }) + .catch(() => {}); }} + env={systemEnv} + /> + + {/* 研发调试快捷身份切换浮窗 (仅在 DEV 环境生效,生产与测试环境彻底剥离) */} + { const fresh = allUsers.find((u) => u.id === newUser.id) || newUser; setCurrentUser(fresh); diff --git a/web/src/components/dev/DevRoleSwitcher.tsx b/web/src/components/dev/DevRoleSwitcher.tsx new file mode 100644 index 00000000..60f9fac0 --- /dev/null +++ b/web/src/components/dev/DevRoleSwitcher.tsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import { Sparkles, ChevronUp, ChevronDown, Check, UserCheck } from 'lucide-react'; +import { UserProfile } from '../../types/docgov'; + +interface DevRoleSwitcherProps { + env?: string; + currentUser: UserProfile; + allUsers: UserProfile[]; + onSwitchUser: (user: UserProfile) => void; +} + +export const DevRoleSwitcher: React.FC = ({ + env = 'dev', + currentUser, + allUsers, + onSwitchUser, +}) => { + const [expanded, setExpanded] = useState(false); + + // 严格环境红线:非 dev 环境绝对不渲染任何 DOM 内容 + if (env !== 'dev') { + return null; + } + + return ( +
+
+ {/* 折叠收起顶栏 */} +
setExpanded(!expanded)} + className="px-3 py-2 bg-gradient-to-r from-amber-950/60 to-slate-900 flex items-center justify-between gap-3 cursor-pointer hover:bg-amber-950/80 transition" + > +
+ + 研发调试切人 + + DEV ONLY + +
+ +
+ {currentUser.name} + {expanded ? : } +
+
+ + {/* 展开的快速切换列表 */} + {expanded && ( +
+
+ 测试说明:点击一键切换身份,实测立项、负责人指派与单文档覆盖拦截效果: +
+ {allUsers.map((u) => { + const isCurrent = u.id === currentUser.id; + return ( + + ); + })} +
+ )} +
+
+ ); +}; diff --git a/web/src/components/layout/AppShell.tsx b/web/src/components/layout/AppShell.tsx index b77293cf..4282cd7c 100644 --- a/web/src/components/layout/AppShell.tsx +++ b/web/src/components/layout/AppShell.tsx @@ -18,7 +18,8 @@ interface AppShellProps { currentUserName: string; onChangeRole: (role: UserRole, name: string) => void; onOpenRightsModal: () => void; - onOpenSettingsModal: () => void; + onOpenUserSettings: () => void; + onOpenAdminSettings?: () => void; chatContent: React.ReactNode; docContent: React.ReactNode; } @@ -34,7 +35,8 @@ export const AppShell: React.FC = ({ currentUserName: _currentUserName, onChangeRole: _onChangeRole, onOpenRightsModal, - onOpenSettingsModal, + onOpenUserSettings, + onOpenAdminSettings, chatContent, docContent, }) => { @@ -117,7 +119,9 @@ export const AppShell: React.FC = ({ currentUser={currentUser} currentRole={currentRole} onOpenRightsModal={onOpenRightsModal} - onOpenSettingsModal={onOpenSettingsModal} + onOpenUserSettings={onOpenUserSettings} + onOpenAdminSettings={onOpenAdminSettings} + isAdmin={currentUser.roleId === 'role-admin' || currentUser.name.includes('管理员')} /> {!leftCollapsed && ( diff --git a/web/src/components/layout/LeftRail.tsx b/web/src/components/layout/LeftRail.tsx index 62b31ec2..f30e8a85 100644 --- a/web/src/components/layout/LeftRail.tsx +++ b/web/src/components/layout/LeftRail.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Plus, Settings, User, ShieldCheck, MessageSquare } from 'lucide-react'; +import { Plus, Settings, User, ShieldCheck, MessageSquare, Shield } from 'lucide-react'; import { ProjectSession } from '../../types/workbench'; import { UserRole, UserProfile } from '../../types/docgov'; @@ -14,7 +14,9 @@ interface LeftRailProps { currentUser: UserProfile; currentRole: UserRole; onOpenRightsModal: () => void; - onOpenSettingsModal: () => void; + onOpenUserSettings: () => void; + onOpenAdminSettings?: () => void; + isAdmin?: boolean; } export const LeftRail: React.FC = ({ @@ -28,7 +30,9 @@ export const LeftRail: React.FC = ({ currentUser, currentRole, onOpenRightsModal, - onOpenSettingsModal, + onOpenUserSettings, + onOpenAdminSettings, + isAdmin = false, }) => { if (collapsed) { return null; @@ -260,37 +264,62 @@ export const LeftRail: React.FC = ({
- {/* 独立设置按钮 (预留系统设置与管理员后台) */} - + {/* 设置操作区:普通设置所有人可用,系统管理仅管理员可见 */} +
+ {isAdmin && ( + + )} + + +
); diff --git a/web/src/components/user/UserProjectRightsModal.tsx b/web/src/components/user/UserProjectRightsModal.tsx index 47a3dc33..56545870 100644 --- a/web/src/components/user/UserProjectRightsModal.tsx +++ b/web/src/components/user/UserProjectRightsModal.tsx @@ -20,6 +20,7 @@ interface UserProjectRightsModalProps { allUsers?: UserProfile[]; canAssignLead?: boolean; onSwitchUser?: (user: UserProfile) => void; + allowDevSwitch?: boolean; onSelectDoc: (docId: string) => void; onRoleChangeForDoc?: (docId: string, role: DocPermissionRole) => void; } @@ -34,6 +35,7 @@ export const UserProjectRightsModal: FC = ({ allUsers, canAssignLead = true, onSwitchUser, + allowDevSwitch = true, onSelectDoc, onRoleChangeForDoc, }) => { @@ -565,9 +567,9 @@ export const UserProjectRightsModal: FC = ({ justifyContent: 'space-between', }} > - {allUsers && allUsers.length > 0 ? ( + {allowDevSwitch && allUsers && allUsers.length > 0 ? (
- 切换当前账号测试隔离: + [DEV 调试切人]: {allUsers.map((u) => { const isCurrent = u.id === currentUser.id; return (