This commit is contained in:
Bastian Wagner
2026-07-15 14:09:28 +02:00
commit 3e5348b7ec
104 changed files with 30367 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
import { HttpClient } from '@angular/common/http';
import { Component, Inject, OnInit, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { apiErrorMessage } from '../shared/api-error';
import { API_BASE_URL } from '../shared/api-base-url';
interface OidcClient {
id: string;
clientId: string;
clientName: string;
tokenEndpointAuthMethod: string;
redirectUris: string[];
postLogoutRedirectUris: string[];
grantTypes: string[];
responseTypes: string[];
scope: string;
firstParty: boolean;
enabled: boolean;
includeGroups: boolean;
}
interface CreatedOidcClient extends OidcClient {
clientSecret?: string;
}
@Component({
selector: 'app-admin-oidc-clients',
imports: [ReactiveFormsModule],
template: `
<section class="account-layout">
<header class="account-header">
<div>
<h1>OIDC Clients</h1>
<p>Clients fuer OpenID Connect Web-SSO verwalten.</p>
</div>
</header>
<section class="overview-grid">
<article class="info-panel">
<h2>Neuer Client</h2>
<form [formGroup]="form" (ngSubmit)="create()">
<label>
Name
<input formControlName="clientName">
</label>
<label>
Redirect URIs
<textarea formControlName="redirectUris" rows="4"></textarea>
</label>
<label>
Logout Redirect URIs
<textarea formControlName="postLogoutRedirectUris" rows="3"></textarea>
</label>
<label>
Scopes
<input formControlName="scope">
</label>
<label class="check-row">
<input type="checkbox" formControlName="publicClient">
Public Client ohne Secret
</label>
<label class="check-row">
<input type="checkbox" formControlName="firstParty">
First-Party Client
</label>
<label class="check-row">
<input type="checkbox" formControlName="includeGroups">
Gruppen-Claim ausgeben
</label>
@if (message()) {
<p class="message" [class.error]="failed()">{{ message() }}</p>
}
<button type="submit" [disabled]="form.invalid || loading()">Client erstellen</button>
</form>
@if (createdSecret()) {
<div class="secret-box">
<strong>Client Secret</strong>
<code>{{ createdSecret() }}</code>
</div>
}
</article>
<article class="info-panel">
<h2>Discovery</h2>
<dl>
<div><dt>Configuration</dt><dd>{{ apiBaseUrl }}/.well-known/openid-configuration</dd></div>
<div><dt>Authorize</dt><dd>{{ apiBaseUrl }}/oidc/auth</dd></div>
<div><dt>Token</dt><dd>{{ apiBaseUrl }}/oidc/token</dd></div>
<div><dt>UserInfo</dt><dd>{{ apiBaseUrl }}/oidc/me</dd></div>
<div><dt>JWKS</dt><dd>{{ apiBaseUrl }}/oidc/jwks</dd></div>
</dl>
</article>
</section>
<section class="info-panel full">
<h2>Registrierte Clients</h2>
@if (clients().length) {
<div class="client-list">
@for (client of clients(); track client.id) {
<article class="client-item">
<div>
<strong>{{ client.clientName }}</strong>
<code>{{ client.clientId }}</code>
</div>
<div class="flags">
<span>{{ client.enabled ? 'aktiv' : 'deaktiviert' }}</span>
<span>{{ client.tokenEndpointAuthMethod }}</span>
@if (client.firstParty) { <span>first-party</span> }
@if (client.includeGroups) { <span>groups</span> }
</div>
<dl>
<div><dt>Redirect URIs</dt><dd>{{ client.redirectUris.join(', ') }}</dd></div>
<div><dt>Scopes</dt><dd>{{ client.scope }}</dd></div>
</dl>
<div class="row-actions">
<button type="button" class="secondary-action" (click)="toggle(client)">
{{ client.enabled ? 'Deaktivieren' : 'Aktivieren' }}
</button>
<button type="button" class="danger-action" (click)="delete(client)">Loeschen</button>
</div>
</article>
}
</div>
} @else {
<p class="muted">Noch keine OIDC-Clients vorhanden.</p>
}
</section>
</section>
`,
})
export class AdminOidcClientsComponent implements OnInit {
readonly clients = signal<OidcClient[]>([]);
readonly loading = signal(false);
readonly failed = signal(false);
readonly message = signal('');
readonly createdSecret = signal('');
readonly form;
constructor(
private readonly fb: FormBuilder,
private readonly http: HttpClient,
@Inject(API_BASE_URL) readonly apiBaseUrl: string,
) {
this.form = this.fb.nonNullable.group({
clientName: ['', Validators.required],
redirectUris: ['http://localhost:8080/callback', Validators.required],
postLogoutRedirectUris: [''],
scope: ['openid profile email groups'],
publicClient: [false],
firstParty: [false],
includeGroups: [true],
});
}
ngOnInit(): void {
this.load();
}
create(): void {
if (this.form.invalid) {
return;
}
this.loading.set(true);
this.failed.set(false);
this.message.set('');
this.createdSecret.set('');
const value = this.form.getRawValue();
this.http
.post<CreatedOidcClient>(`${this.apiBaseUrl}/admin/oidc/clients`, {
...value,
redirectUris: this.lines(value.redirectUris),
postLogoutRedirectUris: this.lines(value.postLogoutRedirectUris),
})
.subscribe({
next: (client) => {
this.createdSecret.set(client.clientSecret ?? '');
this.message.set('Client wurde erstellt.');
this.load();
},
error: (error) => {
this.failed.set(true);
this.message.set(apiErrorMessage(error));
this.loading.set(false);
},
complete: () => this.loading.set(false),
});
}
toggle(client: OidcClient): void {
this.http
.patch<OidcClient>(`${this.apiBaseUrl}/admin/oidc/clients/${client.id}`, { enabled: !client.enabled })
.subscribe({ next: () => this.load(), error: (error) => this.message.set(apiErrorMessage(error)) });
}
delete(client: OidcClient): void {
this.http
.delete<void>(`${this.apiBaseUrl}/admin/oidc/clients/${client.id}`)
.subscribe({ next: () => this.load(), error: (error) => this.message.set(apiErrorMessage(error)) });
}
private load(): void {
this.http.get<OidcClient[]>(`${this.apiBaseUrl}/admin/oidc/clients`).subscribe({
next: (clients) => this.clients.set(clients),
error: (error) => {
this.failed.set(true);
this.message.set(apiErrorMessage(error));
},
});
}
private lines(value: string): string[] {
return value
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
}
}