Replace IsAdmin with role-based admin

Switch user admin handling from an AppUser boolean to ASP.NET Identity roles. Removed AppUser.IsAdmin and related configuration/model entries; added migration ReplaceIsAdminWithRoles to copy Users.IsAdmin=true into a persistent admin role and drop the IsAdmin column. CurrentUserResponse now exposes roles (string[]), AuthController returns ordered roles from UserManager, and IdentitySeedService now ensures the admin role exists and assigns/creates an initial admin user in that role. Program.cs registers an Admin-only policy (PolicyNames/RoleNames), adjusts cookie auth events to return 401/403 for API requests, and wires up authorization. Frontend updated to use roles: authSession normalizes roles, adds hasRole and ROLE_ADMIN, router and layout support meta.requiredRoles, and new Forbidden and AdminUsers pages/route are added. codexInfo.md updated to reflect the migration to role-based auth.
This commit is contained in:
Jonas
2026-04-20 19:57:49 +02:00
parent bd261b6868
commit b2984fcf1a
19 changed files with 813 additions and 39 deletions
+155
View File
@@ -0,0 +1,155 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { AuthRequestError, ROLE_ADMIN, fetchCurrentUser, hasRole } from '@/services/authSession'
const isLoading = ref(true)
const responseMessage = ref('')
const errorMessage = ref('')
async function loadAdminStatus() {
isLoading.value = true
errorMessage.value = ''
try {
const currentUser = await fetchCurrentUser({ force: true })
if (!hasRole(currentUser, ROLE_ADMIN)) {
errorMessage.value = 'Dein Konto hat aktuell keine Admin-Rolle.'
responseMessage.value = ''
return
}
const response = await fetch('/auth/user', {
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
},
})
if (response.status === 401) {
throw new AuthRequestError('Session abgelaufen. Bitte melde dich erneut an.', 401)
}
if (response.status === 403) {
throw new AuthRequestError('Du bist angemeldet, aber nicht als Admin autorisiert.', 403)
}
if (!response.ok) {
throw new AuthRequestError('Admin-Endpunkt konnte nicht geladen werden.', response.status)
}
const payload = (await response.json()) as { message?: unknown }
responseMessage.value =
typeof payload.message === 'string' && payload.message.trim().length > 0
? payload.message
: 'Admin-Endpunkt erfolgreich erreicht.'
} catch (error) {
if (error instanceof AuthRequestError) {
errorMessage.value = error.message
} else {
errorMessage.value = 'Adminbereich konnte nicht geladen werden.'
}
} finally {
isLoading.value = false
}
}
onMounted(() => {
void loadAdminStatus()
})
</script>
<template>
<v-container fluid class="admin-users-page hoard-page">
<section class="admin-users-shell hoard-panel">
<header class="admin-users-head">
<p class="hoard-kicker">Adminbereich</p>
<h1>Benutzerverwaltung</h1>
<p>Diese Seite ist nur für Konten mit Rolle <code>admin</code> sichtbar.</p>
</header>
<v-alert
v-if="errorMessage"
type="error"
variant="tonal"
border="start"
>
{{ errorMessage }}
</v-alert>
<v-alert
v-else-if="responseMessage"
type="success"
variant="tonal"
border="start"
>
{{ responseMessage }}
</v-alert>
<p v-else-if="isLoading" class="admin-users-loading">Adminstatus wird geladen...</p>
<div class="admin-users-actions">
<v-btn
variant="outlined"
prepend-icon="mdi-refresh"
:loading="isLoading"
:disabled="isLoading"
@click="loadAdminStatus"
>
Neu laden
</v-btn>
</div>
</section>
</v-container>
</template>
<style scoped>
.admin-users-page {
--hoard-page-width: 980px;
}
.admin-users-shell {
display: grid;
gap: var(--space-4);
padding: var(--space-6);
}
.admin-users-head h1,
.admin-users-head p,
.admin-users-loading {
margin: 0;
}
.admin-users-head h1 {
margin-top: var(--space-2);
margin-bottom: var(--space-2);
}
.admin-users-head p {
color: var(--color-text-secondary);
}
.admin-users-loading {
color: var(--color-text-secondary);
}
.admin-users-actions {
display: flex;
justify-content: flex-end;
}
@media (width <= 600px) {
.admin-users-shell {
padding: var(--space-4);
}
.admin-users-actions {
justify-content: stretch;
}
:deep(.admin-users-actions .v-btn) {
width: 100%;
min-height: 44px;
}
}
</style>