feat(auth): add view as user and role functionality for owners

Allow owners to impersonate users or roles for debugging permissions.
Add view-as targets to user sessions and resolve effective permissions.
Display a persistent banner in the app shell to exit view-as mode.
This commit is contained in:
2026-05-07 20:31:52 +08:00
parent ee054dcd15
commit 02db73aa4e
12 changed files with 411 additions and 3 deletions

View File

@@ -20,6 +20,7 @@ import {
iconDelete,
iconDish,
iconEdit,
iconEye,
iconHabitat,
iconItem,
iconKey,
@@ -35,6 +36,7 @@ import {
import { defaultLocale, getCurrentLocale, loadSystemWordings, setCurrentLocale } from '../i18n';
import {
api,
notifyAuthChange,
type AncientArtifact,
type AiModerationApiFormat,
type AiModerationAuthMode,
@@ -370,6 +372,7 @@ const activeConfigTab = computed({
}
});
const canEdit = computed(() => can('admin.access'));
const canUseViewAs = computed(() => currentUser.value?.roles.some((role) => role.key === 'owner') === true && !currentUser.value?.viewAs);
const showAdminSkeleton = computed(() => busy.value && !message.value && (!currentUser.value || contentLoading.value));
const canSetLanguageDefault = computed(() => languageForm.value.code === 'en');
const configModalTitle = computed(() =>
@@ -822,6 +825,14 @@ function openUserRoles(user: AdminUser) {
userRoleModalOpen.value = true;
}
async function viewAsUser(user: AdminUser) {
await run(async () => {
const response = await api.viewAsUser(user.id);
currentUser.value = response.user;
notifyAuthChange();
});
}
function closeUserRoleModal() {
userRoleModalOpen.value = false;
resetUserRoleForm();
@@ -854,6 +865,14 @@ function editRolePermissions(role: RoleDetail) {
rolePermissionsModalOpen.value = true;
}
async function viewAsRole(role: RoleDetail) {
await run(async () => {
const response = await api.viewAsRole(role.id);
currentUser.value = response.user;
notifyAuthChange();
});
}
function closeRolePermissionsModal() {
rolePermissionsModalOpen.value = false;
resetRolePermissionForm();
@@ -1851,6 +1870,10 @@ onMounted(() => {
</span>
</span>
<span class="row-actions">
<button v-if="canUseViewAs" type="button" :disabled="busy" @click="viewAsUser(user)">
<Icon :icon="iconEye" class="ui-icon" aria-hidden="true" />
{{ t('viewAs.userAction') }}
</button>
<button v-if="can('admin.users.update') && can('admin.roles.read')" type="button" :disabled="busy" @click="openUserRoles(user)">
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
{{ t('pages.admin.userRoles') }}
@@ -1883,6 +1906,10 @@ onMounted(() => {
</span>
</span>
<span class="row-actions">
<button v-if="canUseViewAs && role.enabled" type="button" :disabled="busy" @click="viewAsRole(role)">
<Icon :icon="iconEye" class="ui-icon" aria-hidden="true" />
{{ t('viewAs.roleAction') }}
</button>
<button v-if="can('admin.roles.update')" type="button" :disabled="busy" @click="editRole(role)">
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
{{ t('common.edit') }}