Create button for ending impersonation

This commit is contained in:
Eli Ribble 2026-04-02 19:36:49 +00:00
parent 76c395d613
commit 522c5785a2
No known key found for this signature in database
6 changed files with 134 additions and 59 deletions

View file

@ -5,6 +5,7 @@
"type": "module",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@vueuse/core": "^14.2.1",
"axios": "^1.13.6",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",

34
pnpm-lock.yaml generated
View file

@ -11,6 +11,9 @@ importers:
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
'@vueuse/core':
specifier: ^14.2.1
version: 14.2.1(vue@3.5.30(typescript@5.9.3))
axios:
specifier: ^1.13.6
version: 1.13.6
@ -341,6 +344,9 @@ packages:
'@types/supercluster@7.1.3':
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@vitejs/plugin-vue@6.0.5':
resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -416,6 +422,19 @@ packages:
'@vue/shared@3.5.30':
resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==}
'@vueuse/core@14.2.1':
resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@14.2.1':
resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==}
'@vueuse/shared@14.2.1':
resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
peerDependencies:
vue: ^3.5.0
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
@ -1244,6 +1263,8 @@ snapshots:
dependencies:
'@types/geojson': 7946.0.16
'@types/web-bluetooth@0.0.21': {}
'@vitejs/plugin-vue@6.0.5(vite@8.0.1(sass@1.98.0)(yaml@2.8.3))(vue@3.5.30(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.2
@ -1367,6 +1388,19 @@ snapshots:
'@vue/shared@3.5.30': {}
'@vueuse/core@14.2.1(vue@3.5.30(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.2.1
'@vueuse/shared': 14.2.1(vue@3.5.30(typescript@5.9.3))
vue: 3.5.30(typescript@5.9.3)
'@vueuse/metadata@14.2.1': {}
'@vueuse/shared@14.2.1(vue@3.5.30(typescript@5.9.3))':
dependencies:
vue: 3.5.30(typescript@5.9.3)
acorn@8.16.0: {}
alien-signals@3.1.2: {}

View file

@ -12,7 +12,7 @@
/>
<div
v-if="showDropdown && filteredUsers.length > 0"
v-if="showDropdown && filteredUsers && filteredUsers.length > 0"
class="dropdown-menu show w-100"
style="max-height: 300px; overflow-y: auto"
>
@ -39,6 +39,7 @@
v-if="
showDropdown &&
searchQuery.length >= minChars &&
filteredUsers &&
filteredUsers.length === 0
"
class="dropdown-menu show w-100"
@ -49,7 +50,8 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from "vue";
import { ref, onMounted, watch } from "vue";
import { computedAsync } from "@vueuse/core";
import Avatar from "@/components/Avatar.vue";
import { useUserStore } from "@/store/user";
import type { User } from "@/types";
@ -76,15 +78,13 @@ const showDropdown = ref(false);
onMounted(async () => {
// Fetch all users if not already loaded
if (!usersStore.all) {
await usersStore.fetchAll();
}
usersStore.withAll().then((all: User[]) => {
// Initialize search query with selected user's name if provided
if (props.modelValue) {
searchQuery.value = props.modelValue.display_name;
}
});
});
// Watch for external changes to modelValue
watch(
@ -98,14 +98,15 @@ watch(
},
);
const filteredUsers = computed(() => {
if (searchQuery.value.length < props.minChars || !usersStore.all) {
const filteredUsers = computedAsync(async (): Promise<User[]> => {
if (searchQuery.value.length < props.minChars) {
return [];
}
const query = searchQuery.value.toLowerCase();
return usersStore.all
const users = await usersStore.withAll();
return users
.filter((user: User) => {
const displayName = user.display_name.toLowerCase();
const username = user.username.toLowerCase();

View file

@ -8,6 +8,14 @@
<i class="bi bi-people"></i> Impersonate User
</div>
<div class="card-body">
<template v-if="isImpersonating && impersonatedUser">
<h1>You're impersonating</h1>
<p>{{ impersonatedUser.username }}</p>
<button class="btn btn-primary" @click="doImpersonationEnd">
End Impersonation
</button>
</template>
<template v-else>
<div class="row mb-3">
<div class="col-md-6">
<label for="userSearch" class="form-label">Search Users</label>
@ -29,25 +37,41 @@
</div>
</div>
<div class="row mb-3">
<button class="btn btn-danger" @click="doImpersonation" type="submit">
<button class="btn btn-danger" @click="doImpersonationStart">
Impersonate
</button>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onMounted, ref } from "vue";
import { useSessionStore } from "@/store/session";
import { useUserStore } from "@/store/user";
import UserSelector from "@/components/UserSelector.vue";
import type { User } from "@/types";
import type { Session, User } from "@/types";
const session = useSessionStore();
const user = useUserStore();
const impersonatedUser = ref<User | null>(null);
const isImpersonating = ref<boolean>(false);
const selectedUser = ref<User | null>(null);
const doImpersonation = async () => {
const doImpersonationEnd = async () => {
const url = session.urls!.api.impersonation;
const response = await fetch(url, {
method: "DELETE",
});
if (!response.ok) {
throw new Error(`Failed to end impersonation: ${response.statusText}`);
}
const new_session = await session.fetchSession();
console.log("session is now", new_session);
};
const doImpersonationStart = async () => {
if (!selectedUser.value) {
console.log("Can't impersonate, null user");
return;
@ -72,4 +96,19 @@ const doImpersonation = async () => {
const new_session = await session.fetchSession();
console.log("session is now", new_session);
};
onMounted(() => {
session.get().then((session: Session) => {
if (session.impersonating) {
isImpersonating.value = true;
console.log("is impersonating, but who?");
user.byURI(session.impersonating).then((user: User | null) => {
impersonatedUser.value = user;
console.log("is impersonating", user);
});
} else {
isImpersonating.value = false;
impersonatedUser.value = null;
}
});
});
</script>

View file

@ -6,8 +6,8 @@ import { useSessionStore } from "@/store/session";
export const useUserStore = defineStore("users", () => {
// State
const _all = ref<User[] | null>(null);
const _byID = ref<Map<number, User>>(new Map());
const all = ref<User[] | null>(null);
const error = ref(null);
const loading = ref(false);
const ongoingFetch = ref<Promise<User[]> | null>(null);
@ -19,11 +19,19 @@ export const useUserStore = defineStore("users", () => {
}
});
// Actions
function byID(id: number) {
function byID(id: number): User | null {
const result = _byID.value.get(id);
if (!result) {
return null;
}
console.log("user", id, result);
return result;
}
async function byURI(uri: string): Promise<User | null> {
const all = await withAll();
const result = all.find((u: User) => u.uri == uri);
return result || null;
}
async function fetchAll(): Promise<User[]> {
const sessionStore = useSessionStore();
const session = await sessionStore.get();
@ -40,7 +48,7 @@ export const useUserStore = defineStore("users", () => {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
all.value = users;
_all.value = users;
for (const u of users) {
_byID.value.set(u.id, u);
}
@ -51,8 +59,8 @@ export const useUserStore = defineStore("users", () => {
}
}
async function withAll(): Promise<User[]> {
if (all.value != null) {
return all.value;
if (_all.value != null) {
return _all.value;
}
if (ongoingFetch.value !== null) {
@ -88,10 +96,9 @@ export const useUserStore = defineStore("users", () => {
}
return {
// State
all,
// Actions
byID,
byURI,
fetchAll,
fetchOne,
withAll,

View file

@ -100,17 +100,10 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { onMounted, ref } from "vue";
import { computedAsync } from "@vueuse/core";
import { useUserStore } from "@/store/user";
interface User {
id: number;
name: string;
avatar: string;
role: string;
status: "Active" | "Inactive";
tags: string[];
}
import { User } from "@/types";
interface URLConfiguration {
userAdd: string;
@ -123,8 +116,8 @@ interface URLConfiguration {
// Reactive state
const userStore = useUserStore();
const users = computed(() => {
return userStore.all;
const users = computedAsync(async (): Promise<User[]> => {
return await userStore.withAll();
});
const urlConfiguration = ref<URLConfiguration>({
userAdd: "/configuration/user/add", // Update with your actual route