Add owner-only update controls, a read-only SSH checker and independent systemd deployment services. Build before maintenance; back up code and database; restore on failed deployment. Preserve runtime configuration and gate business writes during the switch.
98 lines
3.9 KiB
TypeScript
98 lines
3.9 KiB
TypeScript
/*
|
|
Copyright 2024-2026 ReplyPier
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Groups from the leaf module, not from permissions.ts: this map is built at
|
|
// module scope, and permissions.ts sits inside the router import cycle.
|
|
import {
|
|
AGENT_PERMISSIONS,
|
|
AI_CONFIG_PERMISSIONS,
|
|
CHAT_VIEW_PERMISSIONS,
|
|
KNOWLEDGE_PERMISSIONS,
|
|
ORGANIZATION_PERMISSIONS,
|
|
PEOPLE_PERMISSIONS,
|
|
TICKET_PERMISSIONS,
|
|
} from '@/utils/permissionGroups'
|
|
// Only ever called at runtime, so a partially-initialised module is harmless.
|
|
import { hasAnyPermission } from '@/utils/permissions'
|
|
|
|
/**
|
|
* The one place a route's required permissions are written down.
|
|
*
|
|
* Both the router guard and the sidebar (navItems `show`) read this. They used to name permissions independently and had drifted: People
|
|
* was listed in the nav for anyone with a chat grant but the route required
|
|
* view_people, so an assigned-chats-only agent saw a link that bounced them.
|
|
*
|
|
* An empty array means "any authenticated user".
|
|
*/
|
|
export const ROUTE_PERMISSIONS: Record<string, readonly string[]> = {
|
|
'/ai-agents': AGENT_PERMISSIONS,
|
|
'/analytics': ['view_analytics'],
|
|
'/knowledge': KNOWLEDGE_PERMISSIONS,
|
|
// manage_knowledge, not the read pair: every /help-center endpoint the page
|
|
// calls requires manage_knowledge, so a view_knowledge role would get the
|
|
// link and a page that 403s on its first request.
|
|
'/faq': ['manage_knowledge'],
|
|
'/tickets': TICKET_PERMISSIONS,
|
|
'/tickets/:id': TICKET_PERMISSIONS,
|
|
'/settings/system-updates': ['super_admin'],
|
|
'/settings/ticketing': ['manage_organization'],
|
|
'/conversations': CHAT_VIEW_PERMISSIONS,
|
|
'/people': PEOPLE_PERMISSIONS,
|
|
'/human-agents': ['manage_users'],
|
|
'/settings/organization': ORGANIZATION_PERMISSIONS,
|
|
'/settings/ai-config': AI_CONFIG_PERMISSIONS,
|
|
'/settings/integrations': ['manage_organization'],
|
|
'/settings/app-integrations': ['manage_organization'],
|
|
'/settings/widget-apps': ['manage_organization'],
|
|
// Everyone can reach their own profile, and 403 must never deny itself or
|
|
// the guard has nowhere to send anyone.
|
|
'/settings/user': [],
|
|
'/403': [],
|
|
}
|
|
|
|
// Deliberately absent: /settings/subscription and its billing-setup child.
|
|
// The enterprise subscription guard redirects an expired org THERE, so gating
|
|
// it here would ping-pong (guard sends you to billing, this sends you to /403,
|
|
// guard sends you back) until Vue Router aborts. The nav hides it via
|
|
// canViewSubscription instead; gating the route needs the guard fixed first,
|
|
// which is an enterprise-submodule change.
|
|
|
|
/** Shopify embedded routes authenticate by session token, not by user role. */
|
|
const isShopifyPath = (path: string) => path.startsWith('/shopify')
|
|
|
|
/**
|
|
* Whether the current user may open `path`.
|
|
*
|
|
* Unknown paths are allowed: the map lists what is restricted, so a public or
|
|
* not-yet-mapped route must not become unreachable by omission.
|
|
*/
|
|
export function canAccessPath(path: string): boolean {
|
|
if (isShopifyPath(path)) return true
|
|
const required = ROUTE_PERMISSIONS[path]
|
|
if (!required || required.length === 0) return true
|
|
return hasAnyPermission([...required])
|
|
}
|
|
|
|
/**
|
|
* Whether the user may open a resolved route.
|
|
*
|
|
* Takes the matched record paths rather than the URL so a parameterised route
|
|
* is looked up by its pattern ('/tickets/:id'), not by '/tickets/42'.
|
|
*/
|
|
export function canAccessMatchedPaths(matchedPaths: string[]): boolean {
|
|
return matchedPaths.every(canAccessPath)
|
|
}
|