import { Component, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ApiClientService, type AdminSessionDto, type AdminUserDetailDto, type ApiErrorBody, type RoleDto, } from '@boilerplate/api-client'; @Component({ standalone: true, imports: [FormsModule, RouterLink], template: ` Zurueck zur Benutzerliste @if (error(); as currentError) {
{{ messageFor(currentError) }} @if (currentError.requestId) { Request-ID: {{ currentError.requestId }} }
} @if (user(); as currentUser) {

{{ currentUser.name }}

{{ currentUser.email || 'Keine E-Mail' }}

{{ currentUser.active ? 'Aktiv' : 'Deaktiviert' }}

@if (currentUser.active) { } @else { }

Rollen

@for (role of currentUser.roles; track role.id) { {{ role.name }} }

Effektive Permissions

@for (permission of currentUser.effectivePermissions; track permission) { {{ permission }} }

Aktive Sessions

@if (currentUser.sessions.length === 0) {
Keine aktiven Sessions.
} @else {
@for (session of currentUser.sessions; track session.id) {
{{ session.current ? 'Aktuelle Session' : 'Session' }} {{ session.userAgent || 'Unbekannter Browser' }} {{ session.approximateIp || 'Keine IP' }}
Aktivitaet
{{ session.lastActivityAt }}
Ablauf
{{ session.expiresAt }}
}
}
} @else if (loading()) {
Benutzer wird geladen.
} `, styles: [ ` a { display: inline-flex; margin-bottom: 16px; color: var(--color-primary-hover); } .notice, .hero, article { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 8px; padding: 16px; } .notice.error { border-color: var(--color-danger); color: var(--color-danger); margin-bottom: 16px; } .hero { display: grid; gap: 16px; margin-bottom: 16px; } .hero.inactive { border-left: 4px solid var(--color-border-strong); } h2, h3, p { margin: 0; } h3 { margin-bottom: 12px; } .actions, .inline, .grid, .list, article, dl, .permission-list { display: grid; gap: 12px; } button, select { min-height: 44px; } button { border: 0; border-radius: 6px; background: var(--color-primary); color: var(--color-surface); padding: 0 14px; } button.secondary { background: var(--color-text-secondary); } button.danger { background: var(--color-danger); } button:disabled { opacity: 0.55; } select { border: 1px solid var(--color-border); border-radius: 6px; padding: 0 10px; } .chips, .permission-list { display: flex; flex-wrap: wrap; gap: 8px; } .chips span { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--color-border); border-radius: 999px; padding: 4px 6px 4px 10px; } .chips button { min-height: 34px; background: var(--color-danger); } code { border: 1px solid var(--color-border); border-radius: 6px; background: var(--color-background); padding: 6px 8px; } dl { margin: 0; } dt { color: var(--color-text-muted); font-size: 0.88rem; } dd { margin: 0; } @media (min-width: 900px) { .hero, .list article { grid-template-columns: 1fr auto; align-items: center; } .actions, .inline { grid-template-columns: auto auto; } .grid { grid-template-columns: 1fr 1fr; margin-bottom: 16px; } .list article { grid-template-columns: 1.2fr 1fr auto; } } `, ], }) export class AdminUserDetailPageComponent { private readonly api = inject(ApiClientService); private readonly route = inject(ActivatedRoute); readonly user = signal(null); readonly roles = signal([]); readonly loading = signal(false); readonly error = signal(null); selectedRoleId = ''; constructor() { this.api.adminRoles().subscribe((roles) => this.roles.set(roles)); this.load(); } load(): void { const id = this.route.snapshot.paramMap.get('id'); if (!id) return; this.loading.set(true); this.error.set(null); this.api.adminUser(id).subscribe({ next: (user) => { this.user.set(user); this.selectedRoleId = ''; this.loading.set(false); }, error: (error: { error?: ApiErrorBody }) => { this.error.set(error.error ?? this.genericError()); this.loading.set(false); }, }); } activate(id: string): void { this.api.activateAdminUser(id).subscribe({ next: () => this.load(), error: (error: unknown) => this.handleError(error), }); } deactivate(id: string): void { if (!confirm('Benutzer wirklich deaktivieren und alle Sessions beenden?')) return; this.api.deactivateAdminUser(id).subscribe({ next: () => this.load(), error: (error: unknown) => this.handleError(error), }); } assignRole(userId: string): void { if (!this.selectedRoleId) return; this.api.assignAdminUserRole(userId, this.selectedRoleId).subscribe({ next: () => this.load(), error: (error: unknown) => this.handleError(error), }); } removeRole(userId: string, roleId: string, roleName: string): void { if (roleName === 'admin' && !confirm('Adminrolle wirklich entfernen?')) return; this.api.removeAdminUserRole(userId, roleId).subscribe({ next: () => this.load(), error: (error: unknown) => this.handleError(error), }); } revokeSession(userId: string, session: AdminSessionDto): void { this.api.revokeAdminUserSession(userId, session.id).subscribe({ next: () => this.load(), error: (error: unknown) => this.handleError(error), }); } revokeAllSessions(userId: string): void { if (!confirm('Alle Sessions dieses Benutzers beenden?')) return; this.api.revokeAdminUserSessions(userId).subscribe({ next: () => this.load(), error: (error: unknown) => this.handleError(error), }); } availableRoles(user: AdminUserDetailDto): RoleDto[] { const assigned = new Set(user.roles.map((role) => role.id)); return this.roles().filter((role) => !assigned.has(role.id)); } messageFor(error: ApiErrorBody): string { if (error.code === 'LAST_ACTIVE_ADMIN_REQUIRED') { return 'Dieser Benutzer ist der letzte aktive Administrator und kann nicht entmachtet werden.'; } return error.message; } private handleError(error: unknown): void { this.error.set(this.extractError(error)); } private extractError(error: unknown): ApiErrorBody { if (typeof error === 'object' && error !== null && 'error' in error) { const body = (error as { error?: unknown }).error; if (this.isApiErrorBody(body)) return body; } return this.genericError(); } private isApiErrorBody(value: unknown): value is ApiErrorBody { return ( typeof value === 'object' && value !== null && 'message' in value && 'code' in value && 'status' in value && 'requestId' in value ); } private genericError(): ApiErrorBody { return { status: 0, code: 'UNKNOWN', message: 'Die Aktion konnte nicht ausgefuehrt werden.', requestId: '', }; } }