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
+38 -2
View File
@@ -102,6 +102,12 @@ const loadedAvatars = new Set<string>();
const canInvite = computed(() => canManageUsers(auth.user?.role));
const canEditOrg = computed(() => canManageSettings(auth.user?.role));
const canEditRoles = computed(() => canManageUsers(auth.user?.role));
/** مالک واقعی می‌تواند از جدول کاربران وارد مشاهده شود / برگردد */
const canViewAsUsers = computed(
() =>
auth.user?.role === 'owner' || Boolean(auth.user?.viewAs?.canOpenTeamSettings),
);
const viewAsBusy = ref(false);
const selectedRole = computed(
() => roles.value.find((r) => r.id === selectedRoleId.value) ?? null,
@@ -232,8 +238,38 @@ function pickRole(id: string) {
menuUserId.value = null;
}
function onUserActivate(user: OrgUserDto) {
if (user.orgRoleId) pickRole(user.orgRoleId);
async function onUserActivate(user: OrgUserDto) {
if (!canViewAsUsers.value) {
if (user.orgRoleId) pickRole(user.orgRoleId);
return;
}
const impersonatorId = auth.user?.viewAs?.impersonatorId;
if (impersonatorId && user.id === impersonatorId) {
await switchViewAs(user.id);
return;
}
if (user.id === auth.user?.id) {
if (user.orgRoleId) pickRole(user.orgRoleId);
return;
}
await switchViewAs(user.id);
}
async function switchViewAs(targetUserId: string) {
if (viewAsBusy.value) return;
viewAsBusy.value = true;
error.value = '';
ok.value = '';
try {
await auth.viewAs(targetUserId);
ok.value = auth.user?.viewAs
? `در حال مشاهده به‌جای ${auth.user.fullName}`
: 'به حساب خودتان برگشتید.';
} catch (e) {
error.value = apiError(e, 'تغییر حالت مشاهده ناموفق بود.');
} finally {
viewAsBusy.value = false;
}
}
function formatLastLogin(iso: string | null | undefined) {