Create button for ending impersonation
This commit is contained in:
parent
76c395d613
commit
522c5785a2
6 changed files with 134 additions and 59 deletions
|
|
@ -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
34
pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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,14 +78,12 @@ const showDropdown = ref(false);
|
|||
|
||||
onMounted(async () => {
|
||||
// Fetch all users if not already loaded
|
||||
if (!usersStore.all) {
|
||||
await usersStore.fetchAll();
|
||||
}
|
||||
|
||||
// Initialize search query with selected user's name if provided
|
||||
if (props.modelValue) {
|
||||
searchQuery.value = props.modelValue.display_name;
|
||||
}
|
||||
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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -8,46 +8,70 @@
|
|||
<i class="bi bi-people"></i> Impersonate User
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="userSearch" class="form-label">Search Users</label>
|
||||
<UserSelector
|
||||
v-model="selectedUser"
|
||||
label="Choose a user"
|
||||
placeholder="Select a user..."
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="userRole" class="form-label">Filter by Role</label>
|
||||
<select class="form-select" id="userRole">
|
||||
<option value="">All Roles</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">Standard User</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="premium">Premium User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<button class="btn btn-danger" @click="doImpersonation" type="submit">
|
||||
Impersonate
|
||||
<template v-if="isImpersonating && impersonatedUser">
|
||||
<h1>You're impersonating</h1>
|
||||
<p>{{ impersonatedUser.username }}</p>
|
||||
<button class="btn btn-primary" @click="doImpersonationEnd">
|
||||
End Impersonation
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="userSearch" class="form-label">Search Users</label>
|
||||
<UserSelector
|
||||
v-model="selectedUser"
|
||||
label="Choose a user"
|
||||
placeholder="Select a user..."
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="userRole" class="form-label">Filter by Role</label>
|
||||
<select class="form-select" id="userRole">
|
||||
<option value="">All Roles</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">Standard User</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="premium">Premium User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue