14176a3ee2
Introduce full admin user listing/detail endpoints and a forced password-change flow. Backend: make CurrentUserResponse.UserName nullable and add ToCurrentUserResponseAsync extension; AppUserController now exposes GET /auth/user (list) and GET /auth/user/{id} (detail) using UserManager and Admin-only policy; AuthController uses the new mapper and after successful password change clears MustChangePassword, updates UpdatedAt and persists changes (with error handling) before updating security stamp. Frontend: add admin users pages (list + detail), ChangePassword page and route, adminUsers and enhanced authSession services (typed responses, changePassword API, error mapping), router guard to redirect users with mustChangePassword=true to the change-password flow, and show success banner on login after password change. UI tweaks: separate admin section in sidebar, add password-change entries in account menu, footer sizing fixes, and various layout/UX improvements. These changes enable admin account management and enforce secure password updates across the app.
163 lines
4.3 KiB
TypeScript
163 lines
4.3 KiB
TypeScript
import { AuthRequestError } from '@/services/authSession'
|
|
|
|
export interface AdminUser {
|
|
id: string
|
|
userName: string
|
|
roles: string[]
|
|
isActive: boolean
|
|
mustChangePassword: boolean
|
|
}
|
|
|
|
interface ApiMessageResponse {
|
|
message?: unknown
|
|
}
|
|
|
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null
|
|
}
|
|
|
|
function normalizeAdminUser(value: unknown): AdminUser | null {
|
|
if (!isObject(value)) {
|
|
return null
|
|
}
|
|
|
|
const { id, userName, roles, isActive, mustChangePassword } = value
|
|
if (typeof id !== 'string') {
|
|
return null
|
|
}
|
|
|
|
const normalizedRoles = Array.isArray(roles)
|
|
? roles
|
|
.filter((entry): entry is string => typeof entry === 'string')
|
|
.map((entry) => entry.trim())
|
|
.filter((entry) => entry.length > 0)
|
|
: []
|
|
|
|
return {
|
|
id,
|
|
userName: typeof userName === 'string' ? userName : '',
|
|
roles: normalizedRoles,
|
|
isActive: Boolean(isActive),
|
|
mustChangePassword: Boolean(mustChangePassword),
|
|
}
|
|
}
|
|
|
|
async function readApiMessage(response: Response): Promise<string | null> {
|
|
const contentType = response.headers.get('content-type') ?? ''
|
|
if (!contentType.toLowerCase().includes('application/json')) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const payload = (await response.json()) as ApiMessageResponse
|
|
if (typeof payload.message === 'string' && payload.message.trim().length > 0) {
|
|
return payload.message.trim()
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function toAdminUserError(error: unknown, fallbackMessage: string): AuthRequestError {
|
|
if (error instanceof AuthRequestError) {
|
|
return error
|
|
}
|
|
|
|
return new AuthRequestError(fallbackMessage, 0)
|
|
}
|
|
|
|
export async function fetchAdminUsers(): Promise<AdminUser[]> {
|
|
let response: Response
|
|
|
|
try {
|
|
response = await fetch('/auth/user', {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
})
|
|
} 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) {
|
|
throw new AuthRequestError('Du bist angemeldet, aber nicht als Admin autorisiert.', response.status)
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const apiMessage = await readApiMessage(response)
|
|
throw new AuthRequestError(
|
|
apiMessage ?? 'Benutzerliste konnte nicht geladen werden.',
|
|
response.status,
|
|
)
|
|
}
|
|
|
|
const payload: unknown = await response.json()
|
|
if (!Array.isArray(payload)) {
|
|
throw new AuthRequestError('Antwortformat von /auth/user ist ungültig.', response.status)
|
|
}
|
|
|
|
const users = payload.map(normalizeAdminUser)
|
|
if (users.some((user) => user === null)) {
|
|
throw new AuthRequestError('Antwortformat von /auth/user ist ungültig.', response.status)
|
|
}
|
|
|
|
return users as AdminUser[]
|
|
}
|
|
|
|
export async function fetchAdminUserById(userId: string): 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: 'GET',
|
|
credentials: 'include',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
})
|
|
} 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) {
|
|
throw new AuthRequestError('Du bist angemeldet, aber nicht als Admin autorisiert.', response.status)
|
|
}
|
|
|
|
if (response.status === 404) {
|
|
throw new AuthRequestError('Benutzer wurde nicht gefunden.', response.status)
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const apiMessage = await readApiMessage(response)
|
|
throw new AuthRequestError(
|
|
apiMessage ?? 'Benutzerdetails konnten nicht geladen werden.',
|
|
response.status,
|
|
)
|
|
}
|
|
|
|
const payload: unknown = await response.json()
|
|
const user = normalizeAdminUser(payload)
|
|
if (!user) {
|
|
throw new AuthRequestError('Antwortformat von /auth/user/{id} ist ungültig.', response.status)
|
|
}
|
|
|
|
return user
|
|
}
|