idp angepasst
This commit is contained in:
@@ -18,11 +18,11 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section class="groups-section" aria-label="Keycloak Gruppen">
|
||||
<section class="groups-section" aria-label="OIDC Gruppen">
|
||||
<div class="settings-heading">
|
||||
<mat-icon aria-hidden="true">groups</mat-icon>
|
||||
<div>
|
||||
<h2>Keycloak-Gruppen</h2>
|
||||
<h2>OIDC-Gruppen</h2>
|
||||
<p>{{ auth.user()?.groups?.length || 0 }} synchronisiert</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,6 +38,44 @@
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="access-section" aria-label="App Rechte">
|
||||
<div class="settings-heading">
|
||||
<mat-icon aria-hidden="true">admin_panel_settings</mat-icon>
|
||||
<div>
|
||||
<h2>App-Rollen</h2>
|
||||
<p>{{ auth.user()?.roles?.length || 0 }} aktiv</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (auth.user()?.roles?.length) {
|
||||
<ul class="group-list">
|
||||
@for (role of auth.user()?.roles ?? []; track role) {
|
||||
<li>{{ role }}</li>
|
||||
}
|
||||
</ul>
|
||||
} @else {
|
||||
<p class="settings-description">Keine App-Rollen hinterlegt.</p>
|
||||
}
|
||||
|
||||
<div class="settings-heading compact-heading">
|
||||
<mat-icon aria-hidden="true">key</mat-icon>
|
||||
<div>
|
||||
<h2>App-Rechte</h2>
|
||||
<p>{{ auth.user()?.permissions?.length || 0 }} aktiv</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (auth.user()?.permissions?.length) {
|
||||
<ul class="group-list">
|
||||
@for (permission of auth.user()?.permissions ?? []; track permission) {
|
||||
<li>{{ permission }}</li>
|
||||
}
|
||||
</ul>
|
||||
} @else {
|
||||
<p class="settings-description">Keine App-Rechte hinterlegt.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="settings-section" aria-label="Task-Mail Einstellungen">
|
||||
<div class="settings-heading">
|
||||
<mat-icon aria-hidden="true">mark_email_unread</mat-icon>
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
background: color-mix(in srgb, var(--mat-sys-surface-container-low) 36%, var(--mat-sys-surface));
|
||||
}
|
||||
|
||||
.groups-section {
|
||||
.groups-section,
|
||||
.access-section {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
margin-top: 1rem;
|
||||
@@ -50,6 +51,10 @@
|
||||
background: color-mix(in srgb, var(--mat-sys-surface-container-low) 36%, var(--mat-sys-surface));
|
||||
}
|
||||
|
||||
.compact-heading {
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
|
||||
.settings-heading {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -30,7 +29,6 @@ export class AccountComponent {
|
||||
protected readonly auth = inject(AuthService);
|
||||
protected readonly onboarding = inject(OnboardingService);
|
||||
protected readonly taskPush = inject(TaskPushService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
protected savingTaskDigestPreference = false;
|
||||
protected readonly taskDigestPreferenceOptions: ReadonlyArray<{
|
||||
@@ -116,7 +114,6 @@ export class AccountComponent {
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.auth.logout();
|
||||
void this.router.navigateByUrl('/login');
|
||||
this.auth.logoutThroughProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,16 +105,18 @@
|
||||
<mat-icon matListItemIcon aria-hidden="true">account_circle</mat-icon>
|
||||
<span matListItemTitle>Account</span>
|
||||
</a>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="/assistant/logs"
|
||||
routerLinkActive="active-nav-link"
|
||||
ariaCurrentWhenActive="page"
|
||||
(click)="closeSidebarOnCompact()"
|
||||
>
|
||||
<mat-icon matListItemIcon aria-hidden="true">manage_search</mat-icon>
|
||||
<span matListItemTitle>Assistant Logs</span>
|
||||
</a>
|
||||
@if (auth.hasPermission('assistant.logs.view')) {
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="/assistant/logs"
|
||||
routerLinkActive="active-nav-link"
|
||||
ariaCurrentWhenActive="page"
|
||||
(click)="closeSidebarOnCompact()"
|
||||
>
|
||||
<mat-icon matListItemIcon aria-hidden="true">manage_search</mat-icon>
|
||||
<span matListItemTitle>Assistant Logs</span>
|
||||
</a>
|
||||
}
|
||||
</mat-nav-list>
|
||||
</nav>
|
||||
</mat-sidenav>
|
||||
@@ -128,7 +130,9 @@
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
||||
<app-assistant-chat />
|
||||
@if (auth.hasPermission('assistant.chat')) {
|
||||
<app-assistant-chat />
|
||||
}
|
||||
|
||||
<nav class="bottom-nav" aria-label="Mobile Hauptnavigation">
|
||||
<a
|
||||
|
||||
@@ -70,6 +70,7 @@ function isAuthRequest(request: HttpRequest<unknown>): boolean {
|
||||
'/api/auth/register',
|
||||
'/api/auth/sso',
|
||||
'/api/auth/refresh',
|
||||
'/api/auth/logout',
|
||||
'/api/auth/resend-verification',
|
||||
'/api/auth/verify-email',
|
||||
].some((publicAuthUrl) => request.url.startsWith(publicAuthUrl));
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface PublicUser {
|
||||
onboardingCompleted: boolean;
|
||||
taskDigestPreference: TaskDigestPreference;
|
||||
groups: string[];
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface PublicUserSearchResult {
|
||||
@@ -18,9 +20,14 @@ export interface PublicUserSearchResult {
|
||||
export interface AuthTokenResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
idToken?: string;
|
||||
user: PublicUser;
|
||||
}
|
||||
|
||||
export interface AuthLogoutResponse {
|
||||
logoutUrl: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
message: string;
|
||||
user: PublicUser;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { Observable, finalize, shareReplay, tap, throwError } from 'rxjs';
|
||||
import {
|
||||
AuthLogoutResponse,
|
||||
AuthTokenResponse,
|
||||
LoginRequest,
|
||||
PublicUser,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'listify.accessToken';
|
||||
const REFRESH_TOKEN_KEY = 'listify.refreshToken';
|
||||
const ID_TOKEN_KEY = 'listify.idToken';
|
||||
const USER_KEY = 'listify.user';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -82,6 +84,18 @@ export class AuthService {
|
||||
return this.storage?.getItem(REFRESH_TOKEN_KEY) ?? null;
|
||||
}
|
||||
|
||||
idToken(): string | null {
|
||||
return this.storage?.getItem(ID_TOKEN_KEY) ?? null;
|
||||
}
|
||||
|
||||
hasPermission(permission: string): boolean {
|
||||
return this.userSignal()?.permissions.includes(permission) === true;
|
||||
}
|
||||
|
||||
hasAnyPermission(permissions: string[]): boolean {
|
||||
return permissions.some((permission) => this.hasPermission(permission));
|
||||
}
|
||||
|
||||
refreshSession(): Observable<AuthTokenResponse> {
|
||||
const refreshToken = this.refreshToken();
|
||||
|
||||
@@ -103,8 +117,31 @@ export class AuthService {
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.clearSession();
|
||||
}
|
||||
|
||||
logoutThroughProvider(): void {
|
||||
const payload = {
|
||||
refreshToken: this.refreshToken(),
|
||||
idTokenHint: this.idToken(),
|
||||
};
|
||||
|
||||
this.http.post<AuthLogoutResponse>(`${this.apiUrl}/logout`, payload).subscribe({
|
||||
next: (response) => {
|
||||
this.clearSession();
|
||||
window.location.href = response.logoutUrl;
|
||||
},
|
||||
error: () => {
|
||||
this.clearSession();
|
||||
window.location.href = '/login';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private clearSession(): void {
|
||||
this.storage?.removeItem(ACCESS_TOKEN_KEY);
|
||||
this.storage?.removeItem(REFRESH_TOKEN_KEY);
|
||||
this.storage?.removeItem(ID_TOKEN_KEY);
|
||||
this.storage?.removeItem(USER_KEY);
|
||||
this.userSignal.set(null);
|
||||
}
|
||||
@@ -112,6 +149,11 @@ export class AuthService {
|
||||
private storeSession(response: AuthTokenResponse): void {
|
||||
this.storage?.setItem(ACCESS_TOKEN_KEY, response.accessToken);
|
||||
this.storage?.setItem(REFRESH_TOKEN_KEY, response.refreshToken);
|
||||
if (response.idToken) {
|
||||
this.storage?.setItem(ID_TOKEN_KEY, response.idToken);
|
||||
} else {
|
||||
this.storage?.removeItem(ID_TOKEN_KEY);
|
||||
}
|
||||
this.storeUser(response.user);
|
||||
}
|
||||
|
||||
@@ -140,6 +182,8 @@ export class AuthService {
|
||||
return {
|
||||
...user,
|
||||
groups: Array.isArray(user.groups) ? user.groups : [],
|
||||
roles: Array.isArray(user.roles) ? user.roles : [],
|
||||
permissions: Array.isArray(user.permissions) ? user.permissions : [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ export class SsoCallbackComponent implements OnInit {
|
||||
const params = new URLSearchParams(window.location.hash.replace(/^#/, ''));
|
||||
const accessToken = params.get('accessToken');
|
||||
const refreshToken = params.get('refreshToken');
|
||||
const idToken = params.get('idToken') ?? undefined;
|
||||
const userJson = params.get('user');
|
||||
|
||||
if (!accessToken || !refreshToken || !userJson) {
|
||||
@@ -92,6 +93,7 @@ export class SsoCallbackComponent implements OnInit {
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken,
|
||||
user: JSON.parse(userJson) as AuthTokenResponse['user'],
|
||||
};
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user