From 004573a6e017ccc70fed903a9204aca2d1060cb6 Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Fri, 27 Mar 2026 11:38:36 +0100 Subject: [PATCH 1/9] Created modal for use when admin is deleting a user with vrEnvironments connected to it --- .../UserDeletionEnvironmentsQuestionModal.vue | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 app/src/components/UserDeletionEnvironmentsQuestionModal.vue diff --git a/app/src/components/UserDeletionEnvironmentsQuestionModal.vue b/app/src/components/UserDeletionEnvironmentsQuestionModal.vue new file mode 100644 index 00000000..9b47b852 --- /dev/null +++ b/app/src/components/UserDeletionEnvironmentsQuestionModal.vue @@ -0,0 +1,182 @@ + + + \ No newline at end of file From b5aa4012e5fe4b2335e7d131e59b3f66ba5d8db5 Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Fri, 27 Mar 2026 11:53:12 +0100 Subject: [PATCH 2/9] Added new call to transfer vrspace ownership between two users --- backend/mediaserver/src/routers/vrRouter.ts | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/mediaserver/src/routers/vrRouter.ts b/backend/mediaserver/src/routers/vrRouter.ts index 697839a8..46ca1a84 100644 --- a/backend/mediaserver/src/routers/vrRouter.ts +++ b/backend/mediaserver/src/routers/vrRouter.ts @@ -89,6 +89,38 @@ export const vrRouter = router({ ctx.client.leaveCurrentVrSpace(); // ctx.vrSpace.removeClient(ctx.client); }), + transferVrSpaceOwnership: userWithAdminRightsToVrSpace.input(z.object({ + fromUserId: z.string(), + toUserId: z.string(), + })).mutation(async ({ ctx, input }) => { + const { fromUserId, toUserId } = input; + + log.info(`Transferring VR spaces from ${fromUserId} to ${toUserId}`); + + const result = await db + .update(schema.vrSpaces) + .set({ ownerUserId: toUserId }) + .where(eq(schema.vrSpaces.ownerUserId, fromUserId)) + .returning({ + vrSpaceId: schema.vrSpaces.vrSpaceId, + name: schema.vrSpaces.name + }); + + log.info(`Transferred ${result.length} VR spaces`); + + // Notify affected VR spaces to reload (if active) + for (const space of result) { + const vrSpace = VrSpace.getVrSpace(space.vrSpaceId); + if (vrSpace) { + vrSpace.reloadDbData('ownership transferred'); + } + } + + return { + transferredCount: result.length, + spaces: result + }; + }), updateVrSpace: userWithEditRightsToVrSpace.input(VrSpaceUpdateSchema).mutation(async ({ ctx, input }) => { log.info('updating vrSpace', input); const { vrSpaceId, reason, ...data } = input; From 5240f9eefb19ef36bafa378c3f26c3abbdddf74f Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Mon, 30 Mar 2026 10:39:39 +0200 Subject: [PATCH 3/9] Added call on database to transfer ownership of all vr spaces of a user --- backend/mediaserver/src/routers/vrRouter.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/mediaserver/src/routers/vrRouter.ts b/backend/mediaserver/src/routers/vrRouter.ts index 46ca1a84..3efeea91 100644 --- a/backend/mediaserver/src/routers/vrRouter.ts +++ b/backend/mediaserver/src/routers/vrRouter.ts @@ -89,10 +89,13 @@ export const vrRouter = router({ ctx.client.leaveCurrentVrSpace(); // ctx.vrSpace.removeClient(ctx.client); }), - transferVrSpaceOwnership: userWithAdminRightsToVrSpace.input(z.object({ + transferVrSpaceOwnership: atLeastUserP.input(z.object({ fromUserId: z.string(), toUserId: z.string(), })).mutation(async ({ ctx, input }) => { + if (!hasAtLeastSecurityRole(ctx.role, 'admin')) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Admin rights required' }); + } const { fromUserId, toUserId } = input; log.info(`Transferring VR spaces from ${fromUserId} to ${toUserId}`); @@ -108,7 +111,6 @@ export const vrRouter = router({ log.info(`Transferred ${result.length} VR spaces`); - // Notify affected VR spaces to reload (if active) for (const space of result) { const vrSpace = VrSpace.getVrSpace(space.vrSpaceId); if (vrSpace) { From 530e6682bb8fee9b641fcbb704d9ffc81acbe00c Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Mon, 30 Mar 2026 10:43:11 +0200 Subject: [PATCH 4/9] Changed delete user button to allow transfer of ownership of vr spaces before deleting user --- app/src/stores/vrSpaceStore.ts | 5 ++ app/src/views/admin/AdminUserManagerView.vue | 51 +++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/app/src/stores/vrSpaceStore.ts b/app/src/stores/vrSpaceStore.ts index 50569ebb..c988ce8e 100644 --- a/app/src/stores/vrSpaceStore.ts +++ b/app/src/stores/vrSpaceStore.ts @@ -129,6 +129,10 @@ export const useVrSpaceStore = defineStore('vrSpace', () => { return vrSpaceId; } + async function transferVrSpaceOwnership(fromUserId: string, toUserId: string) { + await connection.client.vr.transferVrSpaceOwnership.mutate({ fromUserId, toUserId }); + } + async function deleteVrSpace(vrSpaceId?: VrSpaceId) { if (!vrSpaceId) vrSpaceId = currentVrSpace.value?.dbData.vrSpaceId; if (!vrSpaceId) { @@ -237,6 +241,7 @@ export const useVrSpaceStore = defineStore('vrSpace', () => { navMeshUrl, worldModelScaleString, panoramicPreviewUrl, + transferVrSpaceOwnership, createVrSpace, deleteVrSpace, enterVrSpace, diff --git a/app/src/views/admin/AdminUserManagerView.vue b/app/src/views/admin/AdminUserManagerView.vue index 10fd4dbc..d80fde6a 100644 --- a/app/src/views/admin/AdminUserManagerView.vue +++ b/app/src/views/admin/AdminUserManagerView.vue @@ -92,7 +92,10 @@ class="btn"> edit - @@ -104,18 +107,27 @@ Du saknar behörighet till den här sidan. + From 2930a8870cfc383ad742cda56c6f9467ded3ff55 Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Mon, 30 Mar 2026 12:30:01 +0200 Subject: [PATCH 5/9] Made it so page called vrspacestore to transferownership instead of calling backend directly --- app/src/views/admin/AdminUserManagerView.vue | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/views/admin/AdminUserManagerView.vue b/app/src/views/admin/AdminUserManagerView.vue index d80fde6a..8f036374 100644 --- a/app/src/views/admin/AdminUserManagerView.vue +++ b/app/src/views/admin/AdminUserManagerView.vue @@ -121,12 +121,14 @@ import { useAuthStore } from '@/stores/authStore'; import { useConnectionStore } from '@/stores/connectionStore'; import { computed, onBeforeMount, reactive, ref } from 'vue'; import { createUser, getAdmins, updateUser, deleteUser, getUsers } from '@/modules/authClient'; +import { useVrSpaceStore } from '@/stores/vrSpaceStore'; import { allRolesBelow, hasAtLeastSecurityRole, roleHierarchy, translateUserRole, type UserRole } from 'schemas'; import MaxWidth7xl from '@/components/layout/MaxWidth7xl.vue'; -import UserDeleteOwnershipModal from '@/components/UserDeletionEnvironmentsQuestionModal.vue'; +import UserDeleteOwnershipModal from '@/components/UserDeletionEnvironmentsQuestionModal.vue'; // Use imports const authStore = useAuthStore(); +const vrSpaceStore = useVrSpaceStore(); const connection = useConnectionStore(); const creatableRoles = computed(() => { if (!authStore.role) return []; @@ -249,14 +251,11 @@ async function handleUserDelete(payload: { action: 'take-over' | 'transfer'; tar if (!userToDelete.value) return; const newOwnerId = payload.action === 'transfer' - ? payload.targetUserId! + ? payload.targetUserId : authStore.userId; try { - await connection.client.vr.transferVrSpaceOwnership.mutate({ - fromUserId: userToDelete.value.userId, - toUserId: newOwnerId, - }); + await vrSpaceStore.transferVrSpaceOwnership(userToDelete.value.userId, newOwnerId); await deleteUser(userToDelete.value.userId); From 5caf544654ff1073dbbc1ec1c7da728ef6a58d0c Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Mon, 30 Mar 2026 15:03:44 +0200 Subject: [PATCH 6/9] Created a database call that checks if a user owns any vrspaces --- backend/mediaserver/src/routers/vrRouter.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/mediaserver/src/routers/vrRouter.ts b/backend/mediaserver/src/routers/vrRouter.ts index 3efeea91..a28358b5 100644 --- a/backend/mediaserver/src/routers/vrRouter.ts +++ b/backend/mediaserver/src/routers/vrRouter.ts @@ -123,6 +123,26 @@ export const vrRouter = router({ spaces: result }; }), + doesUserHaveVrSpaces: atLeastUserP.input(z.object({ userId: z.string() })) + .mutation(async ({ ctx, input }) => { + if (!hasAtLeastSecurityRole(ctx.role, 'admin')) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Admin rights required' }); + } + + const { userId } = input; + log.info(`Checking if user ${userId} owns VR spaces`); + + + const result = await db + .select({ count: sql`count(*)`.mapWith(Number) }) + .from(schema.vrSpaces) + .where(eq(schema.vrSpaces.ownerUserId, userId)); + + const hasSpaces = result[0]?.count > 0; + log.info(`User ${userId} ${hasSpaces ? 'HAS' : 'has NO'} VR spaces`); + + return hasSpaces; + }), updateVrSpace: userWithEditRightsToVrSpace.input(VrSpaceUpdateSchema).mutation(async ({ ctx, input }) => { log.info('updating vrSpace', input); const { vrSpaceId, reason, ...data } = input; From 72bbbf88054faf4b1485fbb10299a1f9843b824c Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Mon, 30 Mar 2026 15:04:25 +0200 Subject: [PATCH 7/9] Made usemanagement page utilize new method to check if user owns vrspaces when deleting users --- app/src/stores/vrSpaceStore.ts | 5 +++++ app/src/views/admin/AdminUserManagerView.vue | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/stores/vrSpaceStore.ts b/app/src/stores/vrSpaceStore.ts index c988ce8e..7306798c 100644 --- a/app/src/stores/vrSpaceStore.ts +++ b/app/src/stores/vrSpaceStore.ts @@ -133,6 +133,10 @@ export const useVrSpaceStore = defineStore('vrSpace', () => { await connection.client.vr.transferVrSpaceOwnership.mutate({ fromUserId, toUserId }); } + async function doesUserHaveVrSpaces(userId: string) { + return await connection.client.vr.doesUserHaveVrSpaces.mutate({ userId }); + } + async function deleteVrSpace(vrSpaceId?: VrSpaceId) { if (!vrSpaceId) vrSpaceId = currentVrSpace.value?.dbData.vrSpaceId; if (!vrSpaceId) { @@ -241,6 +245,7 @@ export const useVrSpaceStore = defineStore('vrSpace', () => { navMeshUrl, worldModelScaleString, panoramicPreviewUrl, + doesUserHaveVrSpaces, transferVrSpaceOwnership, createVrSpace, deleteVrSpace, diff --git a/app/src/views/admin/AdminUserManagerView.vue b/app/src/views/admin/AdminUserManagerView.vue index 8f036374..9ad1436f 100644 --- a/app/src/views/admin/AdminUserManagerView.vue +++ b/app/src/views/admin/AdminUserManagerView.vue @@ -242,9 +242,20 @@ const adminUsers = computed(() => { .filter(u => u.userId !== authStore.userId); }); -function openDeleteModal(user: Awaited>[number]) { - userToDelete.value = { userId: user.userId, username: user.username }; - showDeleteModal.value = true; +async function openDeleteModal(user: Awaited>[number]) { + try { + const hasVrSpaces = await vrSpaceStore.doesUserHaveVrSpaces(user.userId); + + if (hasVrSpaces) { + userToDelete.value = { userId: user.userId, username: user.username }; + showDeleteModal.value = true; + } else { + await deleteUser(user.userId); + await makeCallThenResetList(async () => {}); + } + } catch (error) { + console.error('Check failed:', error); + } } async function handleUserDelete(payload: { action: 'take-over' | 'transfer'; targetUserId?: string }) { From 5dc4d1bb6c3c9936ef7c1e1912e7c1190d371b4c Mon Sep 17 00:00:00 2001 From: Darkdusk Date: Tue, 31 Mar 2026 10:59:45 +0200 Subject: [PATCH 8/9] Made it so the create new user section goes vertical at lower screen sizes to avoid having to scroll to the right to access all functions --- app/src/views/admin/AdminUserManagerView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/views/admin/AdminUserManagerView.vue b/app/src/views/admin/AdminUserManagerView.vue index 9ad1436f..69e90652 100644 --- a/app/src/views/admin/AdminUserManagerView.vue +++ b/app/src/views/admin/AdminUserManagerView.vue @@ -8,7 +8,7 @@

Skapa ny användare

-
+
-