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",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@vueuse/core": "^14.2.1",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
|
|
||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
|
|
@ -11,6 +11,9 @@ importers:
|
||||||
'@popperjs/core':
|
'@popperjs/core':
|
||||||
specifier: ^2.11.8
|
specifier: ^2.11.8
|
||||||
version: 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:
|
axios:
|
||||||
specifier: ^1.13.6
|
specifier: ^1.13.6
|
||||||
version: 1.13.6
|
version: 1.13.6
|
||||||
|
|
@ -341,6 +344,9 @@ packages:
|
||||||
'@types/supercluster@7.1.3':
|
'@types/supercluster@7.1.3':
|
||||||
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
|
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21':
|
||||||
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.5':
|
'@vitejs/plugin-vue@6.0.5':
|
||||||
resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==}
|
resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
@ -416,6 +422,19 @@ packages:
|
||||||
'@vue/shared@3.5.30':
|
'@vue/shared@3.5.30':
|
||||||
resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==}
|
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:
|
acorn@8.16.0:
|
||||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
@ -1244,6 +1263,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/geojson': 7946.0.16
|
'@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))':
|
'@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:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
'@rolldown/pluginutils': 1.0.0-rc.2
|
||||||
|
|
@ -1367,6 +1388,19 @@ snapshots:
|
||||||
|
|
||||||
'@vue/shared@3.5.30': {}
|
'@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: {}
|
acorn@8.16.0: {}
|
||||||
|
|
||||||
alien-signals@3.1.2: {}
|
alien-signals@3.1.2: {}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showDropdown && filteredUsers.length > 0"
|
v-if="showDropdown && filteredUsers && filteredUsers.length > 0"
|
||||||
class="dropdown-menu show w-100"
|
class="dropdown-menu show w-100"
|
||||||
style="max-height: 300px; overflow-y: auto"
|
style="max-height: 300px; overflow-y: auto"
|
||||||
>
|
>
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
v-if="
|
v-if="
|
||||||
showDropdown &&
|
showDropdown &&
|
||||||
searchQuery.length >= minChars &&
|
searchQuery.length >= minChars &&
|
||||||
|
filteredUsers &&
|
||||||
filteredUsers.length === 0
|
filteredUsers.length === 0
|
||||||
"
|
"
|
||||||
class="dropdown-menu show w-100"
|
class="dropdown-menu show w-100"
|
||||||
|
|
@ -49,7 +50,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 Avatar from "@/components/Avatar.vue";
|
||||||
import { useUserStore } from "@/store/user";
|
import { useUserStore } from "@/store/user";
|
||||||
import type { User } from "@/types";
|
import type { User } from "@/types";
|
||||||
|
|
@ -76,14 +78,12 @@ const showDropdown = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Fetch all users if not already loaded
|
// Fetch all users if not already loaded
|
||||||
if (!usersStore.all) {
|
usersStore.withAll().then((all: User[]) => {
|
||||||
await usersStore.fetchAll();
|
// Initialize search query with selected user's name if provided
|
||||||
}
|
if (props.modelValue) {
|
||||||
|
searchQuery.value = props.modelValue.display_name;
|
||||||
// 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 for external changes to modelValue
|
||||||
|
|
@ -98,14 +98,15 @@ watch(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredUsers = computed(() => {
|
const filteredUsers = computedAsync(async (): Promise<User[]> => {
|
||||||
if (searchQuery.value.length < props.minChars || !usersStore.all) {
|
if (searchQuery.value.length < props.minChars) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = searchQuery.value.toLowerCase();
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
|
||||||
return usersStore.all
|
const users = await usersStore.withAll();
|
||||||
|
return users
|
||||||
.filter((user: User) => {
|
.filter((user: User) => {
|
||||||
const displayName = user.display_name.toLowerCase();
|
const displayName = user.display_name.toLowerCase();
|
||||||
const username = user.username.toLowerCase();
|
const username = user.username.toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -8,46 +8,70 @@
|
||||||
<i class="bi bi-people"></i> Impersonate User
|
<i class="bi bi-people"></i> Impersonate User
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row mb-3">
|
<template v-if="isImpersonating && impersonatedUser">
|
||||||
<div class="col-md-6">
|
<h1>You're impersonating</h1>
|
||||||
<label for="userSearch" class="form-label">Search Users</label>
|
<p>{{ impersonatedUser.username }}</p>
|
||||||
<UserSelector
|
<button class="btn btn-primary" @click="doImpersonationEnd">
|
||||||
v-model="selectedUser"
|
End Impersonation
|
||||||
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
|
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useSessionStore } from "@/store/session";
|
import { useSessionStore } from "@/store/session";
|
||||||
|
import { useUserStore } from "@/store/user";
|
||||||
import UserSelector from "@/components/UserSelector.vue";
|
import UserSelector from "@/components/UserSelector.vue";
|
||||||
import type { User } from "@/types";
|
import type { Session, User } from "@/types";
|
||||||
|
|
||||||
const session = useSessionStore();
|
const session = useSessionStore();
|
||||||
|
const user = useUserStore();
|
||||||
|
|
||||||
|
const impersonatedUser = ref<User | null>(null);
|
||||||
|
const isImpersonating = ref<boolean>(false);
|
||||||
const selectedUser = ref<User | null>(null);
|
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) {
|
if (!selectedUser.value) {
|
||||||
console.log("Can't impersonate, null user");
|
console.log("Can't impersonate, null user");
|
||||||
return;
|
return;
|
||||||
|
|
@ -72,4 +96,19 @@ const doImpersonation = async () => {
|
||||||
const new_session = await session.fetchSession();
|
const new_session = await session.fetchSession();
|
||||||
console.log("session is now", new_session);
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import { useSessionStore } from "@/store/session";
|
||||||
|
|
||||||
export const useUserStore = defineStore("users", () => {
|
export const useUserStore = defineStore("users", () => {
|
||||||
// State
|
// State
|
||||||
|
const _all = ref<User[] | null>(null);
|
||||||
const _byID = ref<Map<number, User>>(new Map());
|
const _byID = ref<Map<number, User>>(new Map());
|
||||||
const all = ref<User[] | null>(null);
|
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const ongoingFetch = ref<Promise<User[]> | null>(null);
|
const ongoingFetch = ref<Promise<User[]> | null>(null);
|
||||||
|
|
@ -19,11 +19,19 @@ export const useUserStore = defineStore("users", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Actions
|
// Actions
|
||||||
function byID(id: number) {
|
function byID(id: number): User | null {
|
||||||
const result = _byID.value.get(id);
|
const result = _byID.value.get(id);
|
||||||
|
if (!result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
console.log("user", id, result);
|
console.log("user", id, result);
|
||||||
return 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[]> {
|
async function fetchAll(): Promise<User[]> {
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const session = await sessionStore.get();
|
const session = await sessionStore.get();
|
||||||
|
|
@ -40,7 +48,7 @@ export const useUserStore = defineStore("users", () => {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const users = await response.json();
|
const users = await response.json();
|
||||||
all.value = users;
|
_all.value = users;
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
_byID.value.set(u.id, u);
|
_byID.value.set(u.id, u);
|
||||||
}
|
}
|
||||||
|
|
@ -51,8 +59,8 @@ export const useUserStore = defineStore("users", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function withAll(): Promise<User[]> {
|
async function withAll(): Promise<User[]> {
|
||||||
if (all.value != null) {
|
if (_all.value != null) {
|
||||||
return all.value;
|
return _all.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ongoingFetch.value !== null) {
|
if (ongoingFetch.value !== null) {
|
||||||
|
|
@ -88,10 +96,9 @@ export const useUserStore = defineStore("users", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
|
||||||
all,
|
|
||||||
// Actions
|
// Actions
|
||||||
byID,
|
byID,
|
||||||
|
byURI,
|
||||||
fetchAll,
|
fetchAll,
|
||||||
fetchOne,
|
fetchOne,
|
||||||
withAll,
|
withAll,
|
||||||
|
|
|
||||||
|
|
@ -100,17 +100,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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";
|
import { useUserStore } from "@/store/user";
|
||||||
|
import { User } from "@/types";
|
||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
avatar: string;
|
|
||||||
role: string;
|
|
||||||
status: "Active" | "Inactive";
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface URLConfiguration {
|
interface URLConfiguration {
|
||||||
userAdd: string;
|
userAdd: string;
|
||||||
|
|
@ -123,8 +116,8 @@ interface URLConfiguration {
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const users = computed(() => {
|
const users = computedAsync(async (): Promise<User[]> => {
|
||||||
return userStore.all;
|
return await userStore.withAll();
|
||||||
});
|
});
|
||||||
const urlConfiguration = ref<URLConfiguration>({
|
const urlConfiguration = ref<URLConfiguration>({
|
||||||
userAdd: "/configuration/user/add", // Update with your actual route
|
userAdd: "/configuration/user/add", // Update with your actual route
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue