feat: owner read-only view-as from team users table

Allow org owners to inspect any member's data scope via dual JWT; server blocks mutations and team page stays reachable for return.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-17 19:57:26 +03:30
co-authored by Cursor
parent 52c7306e78
commit f7b07f6b7a
13 changed files with 435 additions and 49 deletions
+33 -18
View File
@@ -26,7 +26,15 @@ const route = useRoute();
const { unreadTotal: chatUnreadTotal, ensureConnected: ensureChatConnected } = useOrgChat();
const chatBadge = computed(() => chatUnreadTotal.value);
const canOpenTeamWhileViewing = computed(() =>
Boolean(auth.user?.viewAs?.canOpenTeamSettings),
);
/** تنظیمات مدیریتی واقعی (نقش مشاهده‌شده) */
const canSettings = computed(() => canManageSettings(auth.user?.role));
/** نمایش هاب تنظیمات؛ در مشاهده فقط‌خواندنی فقط صفحهٔ تیم باز می‌ماند */
const showSettingsNav = computed(
() => canSettings.value || canOpenTeamWhileViewing.value,
);
const showOwnRecordsBanner = computed(
() => Boolean(auth.user?.role) && !canViewAllRecords(auth.user?.role),
);
@@ -142,15 +150,30 @@ function moduleAllowed(module?: string) {
return enabledModules.value.includes(module as never);
}
function cardAllowedForNav(card: {
ownerOnly?: boolean;
admin?: boolean;
module?: string;
to?: string;
}) {
if (card.ownerOnly && auth.user?.role !== 'owner') return false;
if (card.admin && !canSettings.value && !canOpenTeamWhileViewing.value) return false;
if (!moduleAllowed(card.module)) return false;
if (
canOpenTeamWhileViewing.value &&
!canSettings.value &&
card.to &&
!card.to.startsWith('/settings/team')
) {
return false;
}
return true;
}
const hubVisible = computed(() => {
const map: Record<string, boolean> = {};
for (const hub of NAV_HUBS) {
map[hub.key] = hub.cards.some(
(card) =>
(!card.ownerOnly || auth.user?.role === 'owner') &&
(!card.admin || canSettings.value) &&
moduleAllowed(card.module),
);
map[hub.key] = hub.cards.some((card) => cardAllowedForNav(card));
}
return map;
});
@@ -159,12 +182,7 @@ const openHub = computed<NavHubDef | null>(() => {
if (!openHubKey.value) return null;
const hub = NAV_HUBS.find((item) => item.key === openHubKey.value);
if (!hub) return null;
const cards = hub.cards.filter(
(card) =>
(!card.ownerOnly || auth.user?.role === 'owner') &&
(!card.admin || canSettings.value) &&
moduleAllowed(card.module),
);
const cards = hub.cards.filter((card) => cardAllowedForNav(card));
if (
hub.key === 'customers' &&
isMobileNav.value &&
@@ -183,10 +201,7 @@ const openHub = computed<NavHubDef | null>(() => {
return {
...hub,
cards,
shortcuts: hub.shortcuts.filter(
(item) =>
(!item.admin || canSettings.value) && moduleAllowed(item.module),
),
shortcuts: hub.shortcuts.filter((item) => cardAllowedForNav(item)),
};
});
@@ -657,7 +672,7 @@ onUnmounted(() => {
</div>
<div
v-if="canSettings"
v-if="showSettingsNav"
class="nav-fold"
:class="{ active: settingsActive || openHubKey === 'settings' }"
>
@@ -1027,7 +1042,7 @@ onUnmounted(() => {
<i class="chev" aria-hidden="true" />
</button>
<button
v-if="canSettings"
v-if="showSettingsNav"
class="nav-item nav-fold-btn"
type="button"
:class="{