This commit is contained in:
Bastian Wagner
2026-08-01 13:17:26 +02:00
parent e3bff40181
commit 4c1bd49405
23 changed files with 185 additions and 2027 deletions

View File

@@ -0,0 +1,19 @@
<div class="auth-page">
<mat-card class="auth-card">
<mat-card-header><mat-card-title>E-Mail-Bestätigung</mat-card-title></mat-card-header>
<mat-card-content>
@if (confirming()) {
<div class="state">
<mat-spinner diameter="36" />
<span>Deine E-Mail-Adresse wird bestätigt …</span>
</div>
} @else if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p>
<a mat-flat-button routerLink="/auth/login">Zum Login</a>
} @else {
<p>Deine E-Mail-Adresse wurde erfolgreich bestätigt.</p>
<a mat-flat-button routerLink="/auth/login">Zum Login</a>
}
</mat-card-content>
</mat-card>
</div>

View File

@@ -0,0 +1,18 @@
.auth-page {
min-height: 100dvh;
display: grid;
place-items: center;
padding: 1rem;
box-sizing: border-box;
}
.auth-card {
width: min(100%, 420px);
}
.state {
display: flex;
align-items: center;
gap: 0.75rem;
}
.error {
color: var(--mat-sys-error);
}

View File

@@ -0,0 +1,69 @@
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
import { ConfirmEmail } from './confirm-email';
import { environment } from '../../../../environments/environment';
describe('ConfirmEmail', () => {
let httpMock: HttpTestingController;
function setup(hash: string | null) {
return TestBed.configureTestingModule({
imports: [ConfirmEmail],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
provideRouter([]),
{
provide: ActivatedRoute,
useValue: { snapshot: { paramMap: convertToParamMap(hash ? { hash } : {}) } },
},
],
}).compileComponents();
}
afterEach(() => httpMock.verify());
it('confirms the route hash and shows the success state', async () => {
await setup('confirm-hash');
httpMock = TestBed.inject(HttpTestingController);
const fixture = TestBed.createComponent(ConfirmEmail);
fixture.detectChanges();
expect(fixture.componentInstance['confirming']()).toBe(true);
const request = httpMock.expectOne(`${environment.apiUrl}auth/email/confirm`);
expect(request.request.body).toEqual({ hash: 'confirm-hash' });
request.flush(null);
expect(fixture.componentInstance['confirming']()).toBe(false);
expect(fixture.componentInstance['errorMessage']()).toBeNull();
});
it('shows an error state when the hash is invalid or expired', async () => {
await setup('bad-hash');
httpMock = TestBed.inject(HttpTestingController);
const fixture = TestBed.createComponent(ConfirmEmail);
fixture.detectChanges();
const request = httpMock.expectOne(`${environment.apiUrl}auth/email/confirm`);
request.flush(null, { status: 404, statusText: 'Not Found' });
expect(fixture.componentInstance['confirming']()).toBe(false);
expect(fixture.componentInstance['errorMessage']()).toBe(
'Der Bestätigungslink ist ungültig oder abgelaufen.',
);
});
it('shows an error state without making a request when the hash is missing', async () => {
await setup(null);
httpMock = TestBed.inject(HttpTestingController);
const fixture = TestBed.createComponent(ConfirmEmail);
fixture.detectChanges();
httpMock.expectNone(`${environment.apiUrl}auth/email/confirm`);
expect(fixture.componentInstance['confirming']()).toBe(false);
expect(fixture.componentInstance['errorMessage']()).toBe('Der Bestätigungslink ist unvollständig.');
});
});

View File

@@ -0,0 +1,37 @@
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { AuthApi } from '../../../core/auth/auth-api';
@Component({
selector: 'app-confirm-email',
imports: [RouterLink, MatButtonModule, MatCardModule, MatProgressSpinnerModule],
templateUrl: './confirm-email.html',
styleUrl: './confirm-email.scss',
})
export class ConfirmEmail {
private readonly authApi = inject(AuthApi);
private readonly route = inject(ActivatedRoute);
private readonly hash = this.route.snapshot.paramMap.get('hash');
protected readonly confirming = signal(true);
protected readonly errorMessage = signal<string | null>(null);
constructor() {
if (!this.hash) {
this.confirming.set(false);
this.errorMessage.set('Der Bestätigungslink ist unvollständig.');
return;
}
this.authApi.confirmEmail(this.hash).subscribe({
next: () => this.confirming.set(false),
error: () => {
this.confirming.set(false);
this.errorMessage.set('Der Bestätigungslink ist ungültig oder abgelaufen.');
},
});
}
}