From 1d00fb3a4b8b62fc6388d9e9c1962dc6ec7e1ab6 Mon Sep 17 00:00:00 2001 From: Jonas <77726472+kobolol@users.noreply.github.com> Date: Fri, 1 May 2026 15:40:54 +0200 Subject: [PATCH] Admin user edit: UI, API and server guard Add full admin user editing flow: introduce EditUserDialog component and integrate it into AdminUserDetail (with minor copy and button variant tweaks), plus layout tweaks to animate the account chevron. Implement updateAdminUser(...) in GUI services to PATCH /auth/user/{id} with comprehensive error handling and export FORBIDDEN_NOT_ADMIN_MESSAGE. Server-side AppUserController now prevents deactivating users in the Admin role and returns a 403, ensuring admin accounts cannot be disabled. These changes enable editing usernames and activation status from the admin UI while protecting admin accounts. --- API/Controllers/Auth/AppUserController.cs | 7 + GUI/src/Layout.vue | 28 ++- GUI/src/components/admin/EditUserDialog.vue | 264 ++++++++++++++++++++ GUI/src/routes/admin/AdminUserDetail.vue | 26 +- GUI/src/services/adminUsers.ts | 76 ++++++ 5 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 GUI/src/components/admin/EditUserDialog.vue diff --git a/API/Controllers/Auth/AppUserController.cs b/API/Controllers/Auth/AppUserController.cs index 1719f61..d2e868a 100644 --- a/API/Controllers/Auth/AppUserController.cs +++ b/API/Controllers/Auth/AppUserController.cs @@ -2,6 +2,7 @@ using API.Contracts.Auth; using API.Models; using API.Security; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -48,6 +49,12 @@ namespace API.Controllers.Auth if (changeDto.IsActive != null) { + if (!changeDto.IsActive.Value && await userManager.IsInRoleAsync(user, RoleNames.Admin)) + { + return StatusCode(StatusCodes.Status403Forbidden, + new { message = "Adminkonten können nicht deaktiviert werden." }); + } + user.IsActive = changeDto.IsActive.Value; if (!changeDto.IsActive.Value) diff --git a/GUI/src/Layout.vue b/GUI/src/Layout.vue index 4199c9f..c7c0d20 100644 --- a/GUI/src/Layout.vue +++ b/GUI/src/Layout.vue @@ -20,6 +20,8 @@ const theme = useTheme() const route = useRoute() const router = useRouter() const showDrawer = ref(true) +const isAccountMenuOpen = ref(false) +const accountChevRotation = ref(0) const currentYear = new Date().getFullYear() const themeStorageKey = 'theme' const currentUser = ref(null) @@ -162,6 +164,14 @@ onMounted(() => { void refreshAuthState({ force: true }) }) +watch(isAccountMenuOpen, (open) => { + if (open) { + accountChevRotation.value = Math.random() < 0.5 ? 180 : -180 + } else { + accountChevRotation.value = 0 + } +}) + watch( () => route.fullPath, () => { @@ -224,8 +234,7 @@ watch( @@ -636,6 +649,13 @@ watch( color: var(--color-text-muted); width: 18px; height: 18px; + transition: transform var(--transition-medium); +} + +@media (prefers-reduced-motion: reduce) { + .account-pill__chev { + transition: none; + } } @media (width <= 1280px) { diff --git a/GUI/src/components/admin/EditUserDialog.vue b/GUI/src/components/admin/EditUserDialog.vue new file mode 100644 index 0000000..59858cb --- /dev/null +++ b/GUI/src/components/admin/EditUserDialog.vue @@ -0,0 +1,264 @@ + + + + + diff --git a/GUI/src/routes/admin/AdminUserDetail.vue b/GUI/src/routes/admin/AdminUserDetail.vue index 3121c8b..e18d571 100644 --- a/GUI/src/routes/admin/AdminUserDetail.vue +++ b/GUI/src/routes/admin/AdminUserDetail.vue @@ -6,6 +6,7 @@ import { AuthRequestError } from '@/services/authSession' import { useAppBannersStore } from '@/stores/appBanners' import StatusPill from '@/components/ui/StatusPill.vue' import UserAvatar from '@/components/ui/UserAvatar.vue' +import EditUserDialog from '@/components/admin/EditUserDialog.vue' const route = useRoute() const router = useRouter() @@ -14,6 +15,7 @@ const appBannersStore = useAppBannersStore() const isLoading = ref(true) const errorMessage = ref('') const user = ref(null) +const isEditDialogOpen = ref(false) const routeUserId = computed(() => { const value = route.params.userId @@ -91,6 +93,10 @@ async function navigateToList() { await router.push({ name: 'AdminUsers' }) } +function handleUserUpdated(updated: AdminUser) { + user.value = updated +} + onMounted(() => { void loadUser() }) @@ -102,7 +108,7 @@ onMounted(() => {

Adminbereich

Benutzerdetails

-

Read-only Ansicht des ausgewählten Kontos. Änderungen erfolgen aktuell außerhalb der App.

+

Detailansicht des ausgewählten Kontos. Über Bearbeiten lassen sich Benutzername und Status anpassen.

@@ -114,7 +120,7 @@ onMounted(() => { Zurück zur Liste { > Neu laden + + Bearbeiten +
@@ -174,6 +189,13 @@ onMounted(() => { + + diff --git a/GUI/src/services/adminUsers.ts b/GUI/src/services/adminUsers.ts index 92a6c3d..1800d8c 100644 --- a/GUI/src/services/adminUsers.ts +++ b/GUI/src/services/adminUsers.ts @@ -160,3 +160,79 @@ export async function fetchAdminUserById(userId: string): Promise { return user } + +export interface UpdateAdminUserPayload { + userName?: string + isActive?: boolean +} + +export async function updateAdminUser( + userId: string, + payload: UpdateAdminUserPayload, +): Promise { + const normalizedId = userId.trim() + if (normalizedId.length === 0) { + throw new AuthRequestError('Ungültige Benutzer-ID.', 400) + } + + let response: Response + + try { + response = await fetch(`/auth/user/${encodeURIComponent(normalizedId)}`, { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(payload), + }) + } catch (error) { + throw toAdminUserError(error, 'Server ist nicht erreichbar. Bitte später erneut versuchen.') + } + + if (response.status === 401) { + throw new AuthRequestError('Session abgelaufen. Bitte melde dich erneut an.', response.status) + } + + if (response.status === 403) { + const apiMessage = await readApiMessage(response) + throw new AuthRequestError( + apiMessage ?? 'Du bist angemeldet, aber nicht als Admin autorisiert.', + response.status, + ) + } + + if (response.status === 404) { + const apiMessage = await readApiMessage(response) + throw new AuthRequestError(apiMessage ?? 'Benutzer wurde nicht gefunden.', response.status) + } + + if (response.status === 409) { + const apiMessage = await readApiMessage(response) + throw new AuthRequestError(apiMessage ?? 'Benutzername ist bereits vergeben.', response.status) + } + + if (response.status === 400) { + const apiMessage = await readApiMessage(response) + throw new AuthRequestError(apiMessage ?? 'Eingaben sind ungültig.', response.status) + } + + if (!response.ok) { + const apiMessage = await readApiMessage(response) + throw new AuthRequestError( + apiMessage ?? 'Benutzer konnte nicht aktualisiert werden.', + response.status, + ) + } + + const body: unknown = await response.json() + const user = normalizeAdminUser(body) + if (!user) { + throw new AuthRequestError('Antwortformat von /auth/user/{id} ist ungültig.', response.status) + } + + return user +} + +export const FORBIDDEN_NOT_ADMIN_MESSAGE = 'Du bist angemeldet, aber nicht als Admin autorisiert.'