Add admin user management and password-change flow
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.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user