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.
This commit is contained in:
@@ -2,6 +2,7 @@ using API.Contracts.Auth;
|
|||||||
using API.Models;
|
using API.Models;
|
||||||
using API.Security;
|
using API.Security;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -48,6 +49,12 @@ namespace API.Controllers.Auth
|
|||||||
|
|
||||||
if (changeDto.IsActive != null)
|
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;
|
user.IsActive = changeDto.IsActive.Value;
|
||||||
|
|
||||||
if (!changeDto.IsActive.Value)
|
if (!changeDto.IsActive.Value)
|
||||||
|
|||||||
+24
-4
@@ -20,6 +20,8 @@ const theme = useTheme()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const showDrawer = ref(true)
|
const showDrawer = ref(true)
|
||||||
|
const isAccountMenuOpen = ref(false)
|
||||||
|
const accountChevRotation = ref(0)
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
const themeStorageKey = 'theme'
|
const themeStorageKey = 'theme'
|
||||||
const currentUser = ref<CurrentUser | null>(null)
|
const currentUser = ref<CurrentUser | null>(null)
|
||||||
@@ -162,6 +164,14 @@ onMounted(() => {
|
|||||||
void refreshAuthState({ force: true })
|
void refreshAuthState({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(isAccountMenuOpen, (open) => {
|
||||||
|
if (open) {
|
||||||
|
accountChevRotation.value = Math.random() < 0.5 ? 180 : -180
|
||||||
|
} else {
|
||||||
|
accountChevRotation.value = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
() => {
|
() => {
|
||||||
@@ -224,8 +234,7 @@ watch(
|
|||||||
<template v-if="isAuthenticated">
|
<template v-if="isAuthenticated">
|
||||||
<v-menu
|
<v-menu
|
||||||
v-if="!display.smAndDown.value"
|
v-if="!display.smAndDown.value"
|
||||||
open-on-hover
|
v-model="isAccountMenuOpen"
|
||||||
:open-on-click="false"
|
|
||||||
location="bottom end"
|
location="bottom end"
|
||||||
offset="10"
|
offset="10"
|
||||||
>
|
>
|
||||||
@@ -234,14 +243,18 @@ watch(
|
|||||||
type="button"
|
type="button"
|
||||||
class="account-pill"
|
class="account-pill"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
@click="router.push({ name: 'Dashboard' })"
|
|
||||||
>
|
>
|
||||||
<span class="account-pill__avatar">{{ userInitials }}</span>
|
<span class="account-pill__avatar">{{ userInitials }}</span>
|
||||||
<span class="account-pill__label">
|
<span class="account-pill__label">
|
||||||
<span class="account-pill__hint">Angemeldet</span>
|
<span class="account-pill__hint">Angemeldet</span>
|
||||||
<span class="account-pill__name">{{ userLabel }}</span>
|
<span class="account-pill__name">{{ userLabel }}</span>
|
||||||
</span>
|
</span>
|
||||||
<v-icon class="account-pill__chev">mdi-chevron-down</v-icon>
|
<v-icon
|
||||||
|
class="account-pill__chev"
|
||||||
|
:style="{ transform: `rotate(${accountChevRotation}deg)` }"
|
||||||
|
>
|
||||||
|
mdi-chevron-down
|
||||||
|
</v-icon>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -636,6 +649,13 @@ watch(
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
transition: transform var(--transition-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.account-pill__chev {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 1280px) {
|
@media (width <= 1280px) {
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
FORBIDDEN_NOT_ADMIN_MESSAGE,
|
||||||
|
updateAdminUser,
|
||||||
|
type AdminUser,
|
||||||
|
type UpdateAdminUserPayload,
|
||||||
|
} from '@/services/adminUsers'
|
||||||
|
import { AuthRequestError } from '@/services/authSession'
|
||||||
|
import { useAppBannersStore } from '@/stores/appBanners'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
user: AdminUser
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: boolean): void
|
||||||
|
(event: 'updated', user: AdminUser): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const appBannersStore = useAppBannersStore()
|
||||||
|
|
||||||
|
const formUserName = ref(props.user.userName)
|
||||||
|
const formIsActive = ref(props.user.isActive)
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const userNameError = ref('')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open, prevOpen) => {
|
||||||
|
if (open && !prevOpen) {
|
||||||
|
formUserName.value = props.user.userName
|
||||||
|
formIsActive.value = props.user.isActive
|
||||||
|
errorMessage.value = ''
|
||||||
|
userNameError.value = ''
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const isAdmin = computed(() => props.user.roles.some((role) => role.toLowerCase() === 'admin'))
|
||||||
|
|
||||||
|
const trimmedUserName = computed(() => formUserName.value.trim())
|
||||||
|
|
||||||
|
const hasChanges = computed(() => {
|
||||||
|
return (
|
||||||
|
trimmedUserName.value !== props.user.userName ||
|
||||||
|
formIsActive.value !== props.user.isActive
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showDeactivationHint = computed(() => {
|
||||||
|
return props.user.isActive && !formIsActive.value
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUserNameErrorOnEdit() {
|
||||||
|
if (userNameError.value) {
|
||||||
|
userNameError.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (isSubmitting.value || !hasChanges.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.value = ''
|
||||||
|
userNameError.value = ''
|
||||||
|
|
||||||
|
const payload: UpdateAdminUserPayload = {}
|
||||||
|
|
||||||
|
if (trimmedUserName.value !== props.user.userName) {
|
||||||
|
if (trimmedUserName.value.length === 0) {
|
||||||
|
userNameError.value = 'Benutzername darf nicht leer sein.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload.userName = trimmedUserName.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formIsActive.value !== props.user.isActive) {
|
||||||
|
payload.isActive = formIsActive.value
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await updateAdminUser(props.user.id, payload)
|
||||||
|
emit('updated', updated)
|
||||||
|
appBannersStore.push({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Benutzer wurde aktualisiert.',
|
||||||
|
})
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthRequestError) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
await router.replace({
|
||||||
|
name: 'Login',
|
||||||
|
query: { redirect: route.fullPath },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status === 403 && error.message === FORBIDDEN_NOT_ADMIN_MESSAGE) {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
await router.replace({ name: 'Forbidden' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.value = error.message
|
||||||
|
|
||||||
|
if (error.status === 400 && /leer/i.test(error.message)) {
|
||||||
|
userNameError.value = error.message
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage.value = 'Benutzer konnte nicht aktualisiert werden.'
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
max-width="520"
|
||||||
|
persistent
|
||||||
|
@update:model-value="(value: boolean) => emit('update:modelValue', value)"
|
||||||
|
>
|
||||||
|
<v-card class="edit-user-dialog ui-panel">
|
||||||
|
<v-card-title class="edit-user-dialog__title">
|
||||||
|
<p class="ui-kicker ui-kicker--xs">Adminbereich</p>
|
||||||
|
<h2>Benutzer bearbeiten</h2>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="edit-user-dialog__body">
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formUserName"
|
||||||
|
label="Benutzername"
|
||||||
|
prepend-inner-icon="mdi-account-outline"
|
||||||
|
autocomplete="off"
|
||||||
|
:error="!!userNameError"
|
||||||
|
:error-messages="userNameError"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@update:model-value="clearUserNameErrorOnEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-switch
|
||||||
|
v-model="formIsActive"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
:label="formIsActive ? 'Konto ist aktiv' : 'Konto ist inaktiv'"
|
||||||
|
:disabled="isSubmitting || isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p v-if="isAdmin" class="edit-user-dialog__hint">
|
||||||
|
<v-icon size="16" icon="mdi-shield-lock-outline" />
|
||||||
|
Adminkonten können nicht deaktiviert werden.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-else-if="showDeactivationHint" class="edit-user-dialog__hint">
|
||||||
|
<v-icon size="16" icon="mdi-information-outline" />
|
||||||
|
Beim Deaktivieren werden bestehende Sessions des Nutzers beendet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="errorMessage"
|
||||||
|
type="error"
|
||||||
|
density="comfortable"
|
||||||
|
class="edit-user-dialog__alert"
|
||||||
|
>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</v-alert>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="edit-user-dialog__actions">
|
||||||
|
<v-btn variant="text" :disabled="isSubmitting" @click="close">
|
||||||
|
Abbrechen
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="elevated"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-content-save-outline"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:disabled="!hasChanges || isSubmitting"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.edit-user-dialog {
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-user-dialog__title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-5) var(--space-6) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-user-dialog__title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-user-dialog__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-5) var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-user-dialog__body :deep(.v-form) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-user-dialog__hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-user-dialog__alert {
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-user-dialog__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: 0 var(--space-6) var(--space-5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,7 @@ import { AuthRequestError } from '@/services/authSession'
|
|||||||
import { useAppBannersStore } from '@/stores/appBanners'
|
import { useAppBannersStore } from '@/stores/appBanners'
|
||||||
import StatusPill from '@/components/ui/StatusPill.vue'
|
import StatusPill from '@/components/ui/StatusPill.vue'
|
||||||
import UserAvatar from '@/components/ui/UserAvatar.vue'
|
import UserAvatar from '@/components/ui/UserAvatar.vue'
|
||||||
|
import EditUserDialog from '@/components/admin/EditUserDialog.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,6 +15,7 @@ const appBannersStore = useAppBannersStore()
|
|||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const user = ref<AdminUser | null>(null)
|
const user = ref<AdminUser | null>(null)
|
||||||
|
const isEditDialogOpen = ref(false)
|
||||||
|
|
||||||
const routeUserId = computed(() => {
|
const routeUserId = computed(() => {
|
||||||
const value = route.params.userId
|
const value = route.params.userId
|
||||||
@@ -91,6 +93,10 @@ async function navigateToList() {
|
|||||||
await router.push({ name: 'AdminUsers' })
|
await router.push({ name: 'AdminUsers' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUserUpdated(updated: AdminUser) {
|
||||||
|
user.value = updated
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadUser()
|
void loadUser()
|
||||||
})
|
})
|
||||||
@@ -102,7 +108,7 @@ onMounted(() => {
|
|||||||
<div class="admin-user-detail-head__copy">
|
<div class="admin-user-detail-head__copy">
|
||||||
<p class="ui-kicker ui-kicker--wide">Adminbereich</p>
|
<p class="ui-kicker ui-kicker--wide">Adminbereich</p>
|
||||||
<h1>Benutzerdetails</h1>
|
<h1>Benutzerdetails</h1>
|
||||||
<p>Read-only Ansicht des ausgewählten Kontos. Änderungen erfolgen aktuell außerhalb der App.</p>
|
<p>Detailansicht des ausgewählten Kontos. Über <strong>Bearbeiten</strong> lassen sich Benutzername und Status anpassen.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-user-detail-head__actions ui-action-row">
|
<div class="admin-user-detail-head__actions ui-action-row">
|
||||||
@@ -114,7 +120,7 @@ onMounted(() => {
|
|||||||
Zurück zur Liste
|
Zurück zur Liste
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
variant="elevated"
|
variant="outlined"
|
||||||
prepend-icon="mdi-refresh"
|
prepend-icon="mdi-refresh"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
@@ -122,6 +128,15 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
Neu laden
|
Neu laden
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="elevated"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-pencil"
|
||||||
|
:disabled="isLoading || !user"
|
||||||
|
@click="isEditDialogOpen = true"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -174,6 +189,13 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<EditUserDialog
|
||||||
|
v-if="user"
|
||||||
|
v-model="isEditDialogOpen"
|
||||||
|
:user="user"
|
||||||
|
@updated="handleUserUpdated"
|
||||||
|
/>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -160,3 +160,79 @@ export async function fetchAdminUserById(userId: string): Promise<AdminUser> {
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateAdminUserPayload {
|
||||||
|
userName?: string
|
||||||
|
isActive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAdminUser(
|
||||||
|
userId: string,
|
||||||
|
payload: UpdateAdminUserPayload,
|
||||||
|
): Promise<AdminUser> {
|
||||||
|
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.'
|
||||||
|
|||||||
Reference in New Issue
Block a user