import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { useAuthStore } from "../store/auth"; import { useStudyStore } from "../store/study"; import { isSystemAdmin } from "../utils/roles"; import { isApiPermissionAllowed } from "../utils/apiPermissionValue"; import { findFirstAccessibleProjectPath, getProjectRoutePermission, hasProjectPermission } from "../utils/projectRoutePermissions"; import { fetchStudies, fetchStudyDetail } from "../api/studies"; import { fetchApiEndpointPermissions } from "../api/projectPermissions"; import Layout from "../components/Layout.vue"; import Login from "../views/Login.vue"; import Register from "../views/Register.vue"; import ForgotPassword from "../views/ForgotPassword.vue"; import DesktopServerSettings from "../views/DesktopServerSettings.vue"; import DesktopProjectEntry from "../views/DesktopProjectEntry.vue"; import DesktopSessionRestore from "../views/DesktopSessionRestore.vue"; import WebWorkbenchEntry from "../views/WebWorkbenchEntry.vue"; import StudyHome from "../views/StudyHome.vue"; import FaqDetail from "../views/FaqDetail.vue"; import AuditLogs from "../views/admin/AuditLogs.vue"; import AdminUsers from "../views/admin/Users.vue"; import AdminProjects from "../views/admin/Projects.vue"; import AdminSites from "../views/admin/Sites.vue"; import PermissionManagement from "../views/admin/PermissionManagement.vue"; import SystemMonitoringPage from "../views/admin/SystemMonitoringPage.vue"; import EmailSettings from "../views/admin/EmailSettings.vue"; import ProjectDetail from "../views/admin/ProjectDetail.vue"; import ProfileSettings from "../views/ProfileSettings.vue"; import ProjectOverview from "../views/ia/ProjectOverview.vue"; import ProjectMilestones from "../views/ia/ProjectMilestones.vue"; import FeeContracts from "../views/fees/ContractFees.vue"; import FeeContractDetail from "../views/fees/ContractFeeDetail.vue"; import DrugShipments from "../views/ia/DrugShipments.vue"; import ShipmentDetail from "../views/drug/ShipmentDetail.vue"; import MaterialEquipment from "../views/ia/MaterialEquipment.vue"; import MaterialEquipmentDetail from "../views/materials/MaterialEquipmentDetail.vue"; import DocumentList from "../views/documents/DocumentList.vue"; import DocumentDetail from "../views/documents/DocumentDetail.vue"; import StartupFeasibilityEthics from "../views/ia/StartupFeasibilityEthics.vue"; import StartupMeetingAuth from "../views/ia/StartupMeetingAuth.vue"; import SubjectManagement from "../views/ia/SubjectManagement.vue"; import RiskIssueSae from "../views/ia/RiskIssueSae.vue"; import RiskIssuePd from "../views/ia/RiskIssuePd.vue"; import RiskIssueMonitoringVisits from "../views/ia/RiskIssueMonitoringVisits.vue"; import EtmfPlaceholder from "../views/ia/EtmfPlaceholder.vue"; import KnowledgeMedicalConsult from "../views/ia/KnowledgeMedicalConsult.vue"; import Precautions from "../views/ia/Precautions.vue"; import KnowledgeSupportFiles from "../views/ia/KnowledgeSupportFiles.vue"; import KnowledgeInstructionFiles from "../views/ia/KnowledgeInstructionFiles.vue"; import FeasibilityForm from "../views/startup/FeasibilityForm.vue"; import FeasibilityDetail from "../views/startup/FeasibilityDetail.vue"; import EthicsForm from "../views/startup/EthicsForm.vue"; import EthicsDetail from "../views/startup/EthicsDetail.vue"; import KickoffForm from "../views/startup/KickoffForm.vue"; import KickoffDetail from "../views/startup/KickoffDetail.vue"; import TrainingDetail from "../views/startup/TrainingDetail.vue"; import PrecautionForm from "../views/knowledge/PrecautionForm.vue"; import PrecautionDetail from "../views/knowledge/PrecautionDetail.vue"; import SubjectForm from "../views/subjects/SubjectForm.vue"; import SubjectDetail from "../views/subjects/SubjectDetail.vue"; import { TEXT } from "../locales"; import { resolveDesktopRouteRedirect } from "./desktopGuard"; import { isTauriRuntime } from "../runtime"; import { DESKTOP_SESSION_RESTORE_PATH, shouldPreserveDesktopSessionOnAuthCheckFailure } from "../session/authRecovery"; const SYSTEM_PERMISSION_READ = "system:permissions:read"; const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config"; const WEB_WORKBENCH_ENTRY_PATH = "/workbench"; const DESKTOP_PROJECT_ENTRY_PATH = "/desktop/project-entry"; const resolveWorkbenchEntryPath = (isDesktopRuntime: boolean) => isDesktopRuntime ? DESKTOP_PROJECT_ENTRY_PATH : WEB_WORKBENCH_ENTRY_PATH; const isWorkbenchEntryPath = (path: string) => path === WEB_WORKBENCH_ENTRY_PATH || path === DESKTOP_PROJECT_ENTRY_PATH; const routes: RouteRecordRaw[] = [ { path: "/login", name: "Login", component: Login, meta: { public: true, title: TEXT.common.actions.login }, }, { path: "/register", name: "Register", component: Register, meta: { public: true, title: TEXT.common.actions.register }, }, { path: "/forgot-password", name: "ForgotPassword", component: ForgotPassword, meta: { public: true, title: TEXT.modules.auth.forgotTitle }, }, { path: "/desktop/server-settings", name: "DesktopServerSettings", component: DesktopServerSettings, meta: { public: true, title: "服务器设置" }, }, { path: DESKTOP_SESSION_RESTORE_PATH, name: "DesktopSessionRestore", component: DesktopSessionRestore, meta: { public: true, title: "恢复登录状态" }, }, { path: WEB_WORKBENCH_ENTRY_PATH, name: "WorkbenchEntry", component: WebWorkbenchEntry, meta: { title: "工作台入口" }, }, { path: DESKTOP_PROJECT_ENTRY_PATH, name: "DesktopProjectEntry", component: DesktopProjectEntry, meta: { title: "选择工作入口" }, }, { path: "/", component: Layout, redirect: "/project/overview", children: [ { path: "profile", name: "ProfileSettings", component: ProfileSettings, meta: { title: TEXT.menu.profile }, }, { path: "project/overview", name: "ProjectOverview", component: ProjectOverview, meta: { title: TEXT.menu.projectOverview, requiresStudy: true }, }, { path: "project/milestones", name: "ProjectMilestones", component: ProjectMilestones, meta: { title: TEXT.menu.projectMilestones, requiresStudy: true }, }, { path: "fees/contracts", name: "FeeContracts", component: FeeContracts, meta: { title: TEXT.menu.feeContracts, requiresStudy: true }, }, { path: "fees/contracts/:contractId", name: "FeeContractDetail", component: FeeContractDetail, meta: { title: TEXT.modules.feeContracts.detailTitle, requiresStudy: true }, }, { path: "drug/shipments", name: "DrugShipments", component: DrugShipments, meta: { title: TEXT.menu.drugShipments, requiresStudy: true }, }, { path: "drug/shipments/:shipmentId", name: "DrugShipmentDetail", component: ShipmentDetail, meta: { title: TEXT.modules.drugShipments.detailTitle, requiresStudy: true }, }, { path: "materials/equipment", name: "MaterialEquipment", component: MaterialEquipment, meta: { title: TEXT.menu.materialEquipment, requiresStudy: true }, }, { path: "materials/equipment/:equipmentId", name: "MaterialEquipmentDetail", component: MaterialEquipmentDetail, meta: { title: TEXT.modules.materialEquipment.detailTitle, requiresStudy: true }, }, { path: "file-versions", name: "FileVersionManagement", component: DocumentList, meta: { title: TEXT.menu.fileVersionManagement, requiresStudy: true }, }, { path: "trial/:trialId/documents", name: "DocumentList", component: DocumentList, meta: { title: TEXT.menu.fileVersionManagement, requiresStudy: true }, }, { path: "documents/:id", name: "DocumentDetail", component: DocumentDetail, meta: { title: TEXT.menu.fileVersionManagement, requiresStudy: true }, }, { path: "startup/feasibility-ethics", name: "StartupFeasibilityEthics", component: StartupFeasibilityEthics, meta: { title: TEXT.menu.startupFeasibilityEthics, requiresStudy: true }, }, { path: "startup/feasibility/new", name: "StartupFeasibilityNew", component: FeasibilityForm, meta: { title: TEXT.common.actions.add + TEXT.modules.startupFeasibilityEthics.feasibilityTab, requiresStudy: true }, }, { path: "startup/feasibility/:recordId", name: "StartupFeasibilityDetail", component: FeasibilityDetail, meta: { title: TEXT.modules.startupFeasibilityEthics.feasibilityDetailTitle, requiresStudy: true }, }, { path: "startup/feasibility/:recordId/edit", name: "StartupFeasibilityEdit", component: FeasibilityForm, meta: { title: TEXT.common.actions.edit + TEXT.modules.startupFeasibilityEthics.feasibilityTab, requiresStudy: true }, }, { path: "startup/ethics/new", name: "StartupEthicsNew", component: EthicsForm, meta: { title: TEXT.common.actions.add + TEXT.modules.startupFeasibilityEthics.ethicsTab, requiresStudy: true }, }, { path: "startup/ethics/:recordId", name: "StartupEthicsDetail", component: EthicsDetail, meta: { title: TEXT.modules.startupFeasibilityEthics.ethicsDetailTitle, requiresStudy: true }, }, { path: "startup/ethics/:recordId/edit", name: "StartupEthicsEdit", component: EthicsForm, meta: { title: TEXT.common.actions.edit + TEXT.modules.startupFeasibilityEthics.ethicsTab, requiresStudy: true }, }, { path: "startup/meeting-auth", name: "StartupMeetingAuth", component: StartupMeetingAuth, meta: { title: TEXT.menu.startupMeetingAuth, requiresStudy: true }, }, { path: "startup/kickoff/new", name: "StartupKickoffNew", component: KickoffForm, meta: { title: TEXT.common.actions.add + TEXT.modules.startupMeetingAuth.kickoffTab, requiresStudy: true }, }, { path: "startup/kickoff/:meetingId", name: "StartupKickoffDetail", component: KickoffDetail, meta: { title: TEXT.modules.startupMeetingAuth.kickoffDetailTitle, requiresStudy: true }, }, { path: "startup/training/:recordId", name: "StartupTrainingDetail", component: TrainingDetail, meta: { title: TEXT.modules.startupMeetingAuth.trainingDetailTitle, requiresStudy: true }, }, { path: "subjects", name: "SubjectManagement", component: SubjectManagement, meta: { title: TEXT.menu.subjects, requiresStudy: true }, }, { path: "subjects/new", name: "SubjectNew", component: SubjectForm, meta: { title: TEXT.common.actions.add + TEXT.menu.subjects, requiresStudy: true }, }, { path: "subjects/:subjectId", name: "SubjectDetail", component: SubjectDetail, meta: { title: TEXT.modules.subjectDetail.title, requiresStudy: true }, }, { path: "subjects/:subjectId/edit", name: "SubjectEdit", component: SubjectForm, meta: { title: TEXT.common.actions.edit + TEXT.menu.subjects, requiresStudy: true }, }, { path: "risk-issues", redirect: "/risk-issues/sae", }, { path: "risk-issues/sae", name: "RiskIssueSae", component: RiskIssueSae, meta: { title: TEXT.menu.riskIssueSae, requiresStudy: true }, }, { path: "risk-issues/pd", name: "RiskIssuePd", component: RiskIssuePd, meta: { title: TEXT.menu.riskIssuePd, requiresStudy: true }, }, { path: "risk-issues/monitoring-visits", name: "RiskIssueMonitoringVisits", component: RiskIssueMonitoringVisits, meta: { title: TEXT.menu.riskIssueMonitoringVisits, requiresStudy: true }, }, { path: "etmf", name: "EtmfPlaceholder", component: EtmfPlaceholder, meta: { title: TEXT.menu.etmf, requiresStudy: true }, }, { path: "knowledge/medical-consult", name: "KnowledgeMedicalConsult", component: KnowledgeMedicalConsult, meta: { title: TEXT.menu.knowledgeMedicalConsult, requiresStudy: true }, }, { path: "knowledge/medical-consult/:itemId", name: "KnowledgeMedicalConsultDetail", component: FaqDetail, meta: { title: TEXT.modules.knowledgeMedicalConsult.detailTitle, requiresStudy: true }, }, { path: "knowledge/precautions", name: "Precautions", component: Precautions, meta: { title: TEXT.menu.knowledgeNotes, requiresStudy: true }, }, { path: "knowledge/precautions/new", name: "PrecautionNew", component: PrecautionForm, meta: { title: TEXT.common.actions.add + TEXT.menu.knowledgeNotes, requiresStudy: true }, }, { path: "knowledge/precautions/:id", name: "PrecautionDetail", component: PrecautionDetail, meta: { title: TEXT.modules.knowledgeNotes.detailTitle, requiresStudy: true }, }, { path: "knowledge/precautions/:id/edit", name: "PrecautionEdit", component: PrecautionForm, meta: { title: TEXT.common.actions.edit + TEXT.menu.knowledgeNotes, requiresStudy: true }, }, { path: "knowledge/support-files", name: "KnowledgeSupportFiles", component: KnowledgeSupportFiles, meta: { title: TEXT.menu.knowledgeSupportFiles, requiresStudy: true }, }, { path: "knowledge/instruction-files", name: "KnowledgeInstructionFiles", component: KnowledgeInstructionFiles, meta: { title: TEXT.menu.knowledgeInstructionFiles, requiresStudy: true }, }, ], }, { path: "/admin", component: Layout, redirect: "/admin/users", children: [ { path: "users", name: "AdminUsers", component: AdminUsers, meta: { title: TEXT.menu.accountManagement, requiresAdmin: true }, }, { path: "projects", name: "AdminProjects", component: AdminProjects, meta: { title: TEXT.menu.projectManagement }, }, { path: "projects/:projectId", name: "AdminProjectDetail", component: ProjectDetail, meta: { title: TEXT.modules.adminProjects.detailTitle, requiresProjectMember: true, projectPermission: "setup_config:read" }, }, { path: "projects/:projectId/members", redirect: (to) => ({ path: "/admin/permissions/project", query: { projectId: to.params.projectId as string, sub: "members" } }), }, { path: "projects/:id/api-permissions", redirect: (to) => ({ path: "/admin/permissions/project", query: { projectId: to.params.id as string } }), }, { path: "projects/:projectId/sites", name: "AdminSites", component: AdminSites, meta: { title: TEXT.modules.adminSites.title, adminProjectPermission: { module: "sites", action: "read" } }, }, { path: "audit-logs", name: "AdminAuditLogs", component: AuditLogs, meta: { title: TEXT.menu.auditLogs, adminProjectPermission: { module: "audit_export", action: "read" } }, }, { path: "system-monitoring", name: "AdminSystemMonitoring", component: SystemMonitoringPage, meta: { title: TEXT.menu.systemMonitoring, requiresAdmin: true }, }, { path: "email-settings", name: "AdminEmailSettings", component: EmailSettings, meta: { title: "邮件服务", requiresAdmin: true }, }, { path: "permission-monitoring", redirect: "/admin/system-monitoring", }, { path: "permissions/monitoring", redirect: "/admin/system-monitoring", }, { path: "permissions", redirect: "/admin/permissions/system", }, { path: "permissions/system", name: "AdminPermissionsSystem", component: PermissionManagement, meta: { title: "系统级权限", systemPermission: SYSTEM_PERMISSION_READ }, }, { path: "permissions/project", name: "AdminPermissionsProject", component: PermissionManagement, meta: { title: "项目权限配置", systemPermission: SYSTEM_PERMISSION_PROJECT_CONFIG }, }, ], }, { path: "/projects/:projectId", redirect: (to) => `/admin/projects/${to.params.projectId}`, }, ]; const router = createRouter({ history: createWebHistory(), routes, }); const ADMIN_PROJECT_OPERATION_KEYS: Record> = { audit_export: { read: "audit_logs:read", write: "audit_logs:export" }, project_members: { read: "project_members:read", write: "project_members:update" }, sites: { read: "sites:read", write: "sites:update" }, }; const SYSTEM_PERMISSION_ROLES: Record = { [SYSTEM_PERMISSION_READ]: ["ADMIN", "PM"], [SYSTEM_PERMISSION_PROJECT_CONFIG]: ["ADMIN", "PM"], }; const getTargetProjectId = (to: any) => { const queryProjectId = typeof to.query?.projectId === "string" ? to.query.projectId : ""; const paramProjectId = typeof to.params?.projectId === "string" ? to.params.projectId : ""; const paramId = typeof to.params?.id === "string" ? to.params.id : ""; return queryProjectId || paramProjectId || paramId; }; const hasSystemPermissionAccess = async (permissionKey: string, isAdmin: boolean, to: any) => { const roles = SYSTEM_PERMISSION_ROLES[permissionKey] || []; if (isAdmin) return roles.includes("ADMIN"); if (!roles.includes("PM")) return false; const studyStore = useStudyStore(); const targetProjectId = getTargetProjectId(to); if (targetProjectId) { if (studyStore.currentStudy?.id === targetProjectId) { return studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM"; } const { data } = await fetchStudies(); const items = (data as any).items || []; const matchedProject = items.find((study: any) => study.id === targetProjectId); return matchedProject?.role_in_study === "PM"; } const hasPmProject = studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM"; if (!hasPmProject) await studyStore.ensureDefaultPmStudy().catch(() => {}); return studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM"; }; const ensureAdminProjectAccess = async (to: any, isAdmin: boolean) => { if (isAdmin) return true; const studyStore = useStudyStore(); const permission = to.meta.adminProjectPermission as { module: string; action: "read" | "write" } | undefined; if (!permission) return true; const targetProjectId = getTargetProjectId(to); if (targetProjectId && studyStore.currentStudy?.id !== targetProjectId) { const [{ data: project }, { data: permissions }] = await Promise.all([ fetchStudyDetail(targetProjectId), fetchApiEndpointPermissions(targetProjectId), ]); studyStore.setCurrentStudy(project); studyStore.currentPermissions = permissions; } else if (!studyStore.currentPermissions) { await studyStore.loadCurrentStudyPermissions(); } const role = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || ""; if (role !== "PM") return false; const operationKey = ADMIN_PROJECT_OPERATION_KEYS[permission.module]?.[permission.action]; if (!operationKey) return false; return isApiPermissionAllowed(studyStore.currentPermissions?.[role]?.[operationKey]); }; const ensureRouteProjectMember = async (to: any, studyStore: ReturnType) => { const routeProjectId = typeof to.params.projectId === "string" ? to.params.projectId : ""; if (!routeProjectId) return false; const { data: project } = await fetchStudyDetail(routeProjectId); studyStore.setCurrentStudy(project); return true; }; const ensureProjectPermissionAccess = async ( operationKey: string, isAdmin: boolean, studyStore: ReturnType, ) => { if (isAdmin) return true; if (!operationKey || !studyStore.currentStudy) return false; if (!studyStore.currentPermissions) { await studyStore.loadCurrentStudyPermissions(); } const role = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || ""; return isApiPermissionAllowed(studyStore.currentPermissions?.[role]?.[operationKey]); }; router.beforeEach(async (to, _from, next) => { const isDesktopRuntime = isTauriRuntime(); const desktopRedirect = resolveDesktopRouteRedirect(to); if (desktopRedirect) { next({ path: desktopRedirect }); return; } if (isDesktopRuntime && to.path === WEB_WORKBENCH_ENTRY_PATH) { next({ path: DESKTOP_PROJECT_ENTRY_PATH }); return; } if (!isDesktopRuntime && to.path === DESKTOP_PROJECT_ENTRY_PATH) { next({ path: WEB_WORKBENCH_ENTRY_PATH }); return; } const auth = useAuthStore(); const studyStore = useStudyStore(); const getToken = () => auth.token; let token = getToken(); const isDesktopSessionRestoreRoute = to.path === DESKTOP_SESSION_RESTORE_PATH; if (!auth.user && getToken() && !isDesktopSessionRestoreRoute) { try { await auth.fetchMe({ disableNetworkRetry: true, suppressErrorMessage: true }); } catch (error) { if (isDesktopRuntime && shouldPreserveDesktopSessionOnAuthCheckFailure(error)) { next({ path: DESKTOP_SESSION_RESTORE_PATH, query: { redirect: to.fullPath } }); return; } // 有 token 但无法获取用户,强制回登录,避免进入“无用户上下文”页面 await auth.logout(); next({ path: "/login" }); return; } } token = getToken(); const isAdmin = isSystemAdmin(auth.user); if (!to.meta.public && !token) { next({ path: "/login" }); return; } const workbenchEntryPath = resolveWorkbenchEntryPath(isDesktopRuntime); if (token && studyStore.currentStudy && auth.user?.email) { studyStore.rememberCurrentStudyForUser(auth.user.email); } if (token && isWorkbenchEntryPath(to.path) && studyStore.currentStudy) { studyStore.clearCurrentStudy(); } if (isDesktopRuntime && token && !isAdmin && to.path.startsWith("/admin")) { next({ path: DESKTOP_PROJECT_ENTRY_PATH }); return; } if (isDesktopRuntime && token && isAdmin && to.path.startsWith("/admin") && studyStore.currentStudy) { studyStore.clearCurrentStudy(); } if (isDesktopRuntime && token && to.path === "/") { next({ path: studyStore.currentStudy ? "/project/overview" : DESKTOP_PROJECT_ENTRY_PATH }); return; } if (token && to.path === "/") { next({ path: studyStore.currentStudy ? "/project/overview" : workbenchEntryPath }); return; } if ((to.path === "/login" || to.path === "/register") && token) { if (!auth.forceLogin) { next({ path: workbenchEntryPath }); return; } } if (to.name === "FileVersionManagement" && studyStore.currentStudy?.id) { next({ name: "DocumentList", params: { trialId: studyStore.currentStudy.id }, replace: true }); return; } if (to.meta.adminProjectPermission) { const allowed = await ensureAdminProjectAccess(to, isAdmin).catch(() => false); if (!allowed) { next({ path: isAdmin ? "/admin/users" : "/admin/projects" }); return; } } if (to.meta.requiresAdmin && !isAdmin) { next({ path: "/admin/projects" }); return; } if (to.meta.requiresProjectMember && !isAdmin) { const allowed = await ensureRouteProjectMember(to, studyStore).catch(() => false); if (!allowed) { next({ path: "/admin/projects" }); return; } } if (to.meta.projectPermission) { const allowed = await ensureProjectPermissionAccess(to.meta.projectPermission as string, isAdmin, studyStore).catch(() => false); if (!allowed) { next({ path: isAdmin ? "/admin/users" : "/admin/projects" }); return; } } if (to.meta.systemPermission) { const allowed = await hasSystemPermissionAccess(to.meta.systemPermission as string, isAdmin, to); if (!allowed) { next({ path: isAdmin ? "/admin/users" : "/admin/projects" }); return; } } if (to.meta.requiresProjectPm && !isAdmin) { const hasPmProject = studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM"; if (!hasPmProject) { await studyStore.ensureDefaultPmStudy().catch(() => {}); } const hasProjectPmAccess = studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM"; if (!hasProjectPmAccess) { next({ path: "/admin/projects" }); return; } } if (to.name === "AdminAuditLogs") { if (studyStore.currentStudy && !studyStore.currentPermissions) { await studyStore.loadCurrentStudyPermissions().catch(() => {}); } const role = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || ""; const canReadAudit = isApiPermissionAllowed(studyStore.currentPermissions?.[role]?.["audit_logs:read"]); if (!(isAdmin || (role === "PM" && canReadAudit))) { next({ path: "/project/overview" }); return; } } if (to.meta.requiresStudy && !studyStore.currentStudy) { next({ path: workbenchEntryPath }); return; } if (to.meta.requiresStudy && studyStore.currentStudy) { if (!studyStore.currentPermissions) { await studyStore.loadCurrentStudyPermissions().catch(() => {}); } const projectRole = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || ""; const permission = getProjectRoutePermission(to.path); const canAccessProjectRoute = hasProjectPermission(studyStore.currentPermissions, projectRole, permission, isAdmin); if (!canAccessProjectRoute) { const fallback = findFirstAccessibleProjectPath(studyStore.currentPermissions, projectRole, isAdmin); next({ path: fallback || "/admin/projects" }); return; } } next(); }); export default router;