generalized dashboard for member and admin

This commit is contained in:
Bernhard Müller 2026-07-05 10:49:30 +02:00
parent caf05d3103
commit 59a0b6b9b7
38 changed files with 528 additions and 93 deletions

View file

@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jdk21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View file

@ -37,6 +37,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>

View file

@ -0,0 +1,33 @@
package at.mueller.eeg.backend.dashboard.api;
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
import at.mueller.eeg.backend.dashboard.service.DashboardService;
import at.mueller.eeg.backend.iam.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/dashboard")
@RequiredArgsConstructor
public class DashboardController {
private final DashboardService dashboardService;
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<DashboardStatsDto.AdminStats> getAdminDashboard() {
return ResponseEntity.ok(dashboardService.getAdminStats());
}
@GetMapping("/user")
@PreAuthorize("hasRole('MEMBER')")
public ResponseEntity<DashboardStatsDto.UserStats> getUserDashboard(@AuthenticationPrincipal User user) {
// user.getId() funktioniert, da UserDetails implementiert ist
return ResponseEntity.ok(dashboardService.getUserStats(user.getId()));
}
}

View file

@ -0,0 +1,15 @@
package at.mueller.eeg.backend.dashboard.api.dto;
public interface DashboardStatsDto {
record AdminStats(
long totalCommunities,
long pendingUsers,
long totalActiveMemberships
) {}
record UserStats(
long totalMeteringPoints,
long activeMemberships,
long pendingMemberships
) {}
}

View file

@ -0,0 +1,47 @@
package at.mueller.eeg.backend.dashboard.service;
import at.mueller.eeg.backend.community.domain.MembershipStatus;
import at.mueller.eeg.backend.community.repository.EnergyCommunityRepository;
import at.mueller.eeg.backend.community.repository.MembershipRepository;
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
import at.mueller.eeg.backend.iam.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DashboardService {
private final EnergyCommunityRepository energyCommunityRepository;
private final UserRepository userRepository;
private final MembershipRepository membershipRepository;
private final MeteringPointRepository meteringPointRepository;
public DashboardStatsDto.AdminStats getAdminStats() {
long communities = energyCommunityRepository.count();
long pendingUsers = userRepository.findAllByStatusAndEmailVerified(RegistrationStatus.PENDING, true).size();
// Zählt alle aktiven Mitgliedschaften
long activeMemberships = membershipRepository.findAll().stream()
.filter(m -> m.getStatus() == MembershipStatus.ACTIVE)
.count();
return new DashboardStatsDto.AdminStats(communities, pendingUsers, activeMemberships);
}
public DashboardStatsDto.UserStats getUserStats(UUID userId) {
long meteringPoints = meteringPointRepository.findAllByUserId(userId).size();
// Zählt die Mitgliedschaften der Zählpunkte des Users
long activeMembers = 0; // Hier idealerweise eine angepasste Query im Repository nutzen
long pendingMembers = 0;
return new DashboardStatsDto.UserStats(meteringPoints, activeMembers, pendingMembers);
}
}

View file

@ -10,10 +10,7 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@ -41,4 +38,10 @@ public class AuthController {
return ResponseEntity.ok(authService.login(request));
}
@GetMapping("/verify-email")
public ResponseEntity<Void> verifyEmail(@RequestParam String token) {
authService.verifyEmail(token);
return ResponseEntity.ok().build();
}
}

View file

@ -35,6 +35,12 @@ public class User implements UserDetails {
private String iban;
@Column(nullable = false)
private boolean emailVerified = false;
@Column(name = "verification_token")
private String verificationToken;
@Enumerated(EnumType.STRING)
private UserRole role = UserRole.MEMBER;

View file

@ -25,6 +25,8 @@ public interface UserMapper {
@Mapping(target = "iban", ignore = true)
@Mapping(target = "enabled", constant = "false")
@Mapping(target = "authorities", ignore = true)
@Mapping(target = "emailVerified", ignore = true)
@Mapping(target = "verificationToken", ignore = true)
User mapToEntity(IamDtos.RegistrationRequest request, @MappingTarget User user);

View file

@ -12,5 +12,7 @@ public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByEmail(String email);
List<User> findAllByStatus(RegistrationStatus status);
List<User> findAllByStatusAndEmailVerified(RegistrationStatus status, Boolean verified);
Optional<User> findByVerificationToken(String token);
}

View file

@ -23,7 +23,7 @@ public class AdminIamService {
private final ApplicationEventPublisher eventPublisher;
public List<User> getPendingUsers() {
return userRepository.findAllByStatus(RegistrationStatus.PENDING);
return userRepository.findAllByStatusAndEmailVerified(RegistrationStatus.PENDING, true);
}
@Transactional

View file

@ -1,6 +1,5 @@
package at.mueller.eeg.backend.iam.service;
import at.mueller.eeg.backend.common.event.UserRegisteredEvent;
import at.mueller.eeg.backend.iam.domain.User;
import at.mueller.eeg.backend.iam.mapper.UserMapper;
import at.mueller.eeg.backend.iam.repository.UserRepository;
@ -20,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import static at.mueller.eeg.backend.iam.api.dto.IamDtos.*;
@ -32,6 +32,7 @@ public class AuthService {
private final UserRepository userRepository;
private final UserMapper userMapper;
private final ApplicationEventPublisher eventPublisher;
private final MailService mailService;
/**
* Verarbeitet den Login und gibt den JWT-Token als String zurück.
@ -62,9 +63,23 @@ public class AuthService {
});
User newUser = userMapper.mapToEntity(request, new User());
String token = UUID.randomUUID().toString();
newUser.setVerificationToken(token);
userRepository.save(newUser);
eventPublisher.publishEvent(new UserRegisteredEvent(newUser.getId(), request.atNumber()));
mailService.sendVerificationEmail(newUser.getEmail(), token);
}
@Transactional
public void verifyEmail(String token) {
User user = userRepository.findByVerificationToken(token)
.orElseThrow(() -> new IllegalArgumentException("Ungültiger oder abgelaufener Token"));
user.setEmailVerified(true);
user.setVerificationToken(null);
userRepository.save(user);
}
/**

View file

@ -0,0 +1,29 @@
package at.mueller.eeg.backend.iam.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
@Service
@Profile("dev")
@Slf4j
public class ConsoleMailService implements MailService {
@Override
public void sendVerificationEmail(String toEmail, String token) {
String verificationUrl = "http://localhost:4200/verify-email?token=" + token;
log.info("""
========================================================================
📧 SIMULIERTER E-MAIL-VERSAND (Profile: DEV)
========================================================================
An: {}
Betreff: Bitte bestätige deine E-Mail-Adresse für das EEG-Portal
Klicke auf den folgenden Link, um die Registrierung abzuschließen:
{}
========================================================================
""", toEmail, verificationUrl);
}
}

View file

@ -0,0 +1,5 @@
package at.mueller.eeg.backend.iam.service;
public interface MailService {
void sendVerificationEmail(String toEmail, String token);
}

View file

@ -0,0 +1,27 @@
package at.mueller.eeg.backend.iam.service;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
@Profile("prod")
@RequiredArgsConstructor
public class SmtpMailService implements MailService {
private final JavaMailSender mailSender;
@Override
public void sendVerificationEmail(String toEmail, String token) {
String verificationUrl = "http://localhost:4200/verify-email?token=" + token;
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(toEmail);
message.setSubject("Bitte bestätige deine E-Mail-Adresse für das EEG-Portal");
message.setText("Willkommen!\n\nBitte klicke auf den folgenden Link:\n" + verificationUrl);
mailSender.send(message);
}
}

View file

@ -1,8 +1,6 @@
<!-- Der Header bleibt immer fix oben stehen -->
@if (!isAdminRoute) {
@if (!isDashboardRoute) {
<app-header></app-header>
}
<!-- Hier drin werden je nach Route die Login-, Register- oder Dashboard-Seiten geladen -->
<main class="min-h-[calc(100vh-4rem)] bg-gray-50">
<router-outlet></router-outlet>
</main>

View file

@ -3,31 +3,33 @@ import {LandingPage} from './pages/landing-page/landing-page';
import {Register} from './pages/register/register';
import {LoginComponent} from './pages/login/login';
import {AdminUserApprovalComponent} from './pages/admin-user-approval/admin-user-approval';
import {adminAuthGuard} from './guards/admin-auth-guard';
import {AppLayout} from './layout/app-layout/app-layout';
import {authGuard} from './guards/auth-guard';
import {roleGuard} from './guards/role-guard';
import {DashboardLayout} from './layout/dashboard-layout/dashboard-layout';
import {DashboardOverview} from './pages/dashboard-overview/dashboard-overview';
import {AdminEnergyCommunity} from './pages/admin-energy-community/admin-energy-community';
import {VerifyEmailComponent} from './pages/verify-email/verify-email';
export const routes: Routes = [
{ path: '', component: LandingPage },
{ path: 'register', component: Register },
{ path: 'login', component: LoginComponent},
{ path: 'login', component: LoginComponent },
{ path: 'verify-email', component: VerifyEmailComponent },
{
path: 'admin',
component: AppLayout,
canActivate: [adminAuthGuard],
path: 'dashboard',
component: DashboardLayout,
canActivate: [authGuard],
children: [
{
path: '',
redirectTo: 'approvals',
pathMatch: 'full'
},
{ path: '', component: DashboardOverview },
{
path: 'approvals',
component: AdminUserApprovalComponent
component: AdminUserApprovalComponent,
canActivate: [roleGuard(['ADMIN'])]
},
{
path: 'energy-communities',
component: AdminEnergyCommunity
component: AdminEnergyCommunity,
canActivate: [roleGuard(['ADMIN'])]
}
]
},

View file

@ -11,12 +11,12 @@ import {HeaderComponent} from './layout/header/header';
export class App {
protected readonly title = signal('eeg_frontend');
isAdminRoute = false;
isDashboardRoute = false;
constructor(private router: Router) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.isAdminRoute = event.urlAfterRedirects.startsWith('/admin');
this.isDashboardRoute = event.urlAfterRedirects.startsWith('/dashboard');
}
});
}

View file

@ -1,11 +1,11 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';
import {TestBed} from '@angular/core/testing';
import {CanActivateFn} from '@angular/router';
import { adminAuthGuard } from './admin-auth-guard';
import {authGuard} from './auth-guard';
describe('adminAuthGuard', () => {
describe('authGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => adminAuthGuard(...guardParameters));
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});

View file

@ -1,19 +0,0 @@
import {inject} from '@angular/core';
import {CanActivateFn, Router} from '@angular/router';
import {AuthService} from '../services/auth';
export const adminAuthGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
// Replace these with your actual authentication and role-checking methods
const isAuthenticated = authService.isLoggedIn();
const isAdmin = authService.currentUserRole() === 'ADMIN';
if (isAuthenticated && isAdmin) {
return true;
}
// Optional: Redirect to a generic 'unauthorized' page or back to the landing page/login
console.warn('Access denied: Admin privileges required.');
return router.createUrlTree(['/login']);
};

View file

@ -0,0 +1,15 @@
import {inject} from '@angular/core';
import {CanActivateFn, Router} from '@angular/router';
import {AuthService} from '../services/auth';
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
console.warn('Zugriff verweigert: Login erforderlich.');
return router.createUrlTree(['/login']);
};

View file

@ -0,0 +1,18 @@
import {inject} from '@angular/core';
import {CanActivateFn, Router} from '@angular/router';
import {AuthService} from '../services/auth';
export function roleGuard(allowedRoles: string[]): CanActivateFn {
return () => {
const authService = inject(AuthService);
const router = inject(Router);
const role = authService.currentUserRole();
if (role && allowedRoles.includes(role)) {
return true;
}
console.warn(`Zugriff verweigert: benötigt eine der Rollen [${allowedRoles.join(', ')}].`);
return router.createUrlTree(['/dashboard']);
};
}

View file

@ -1,11 +0,0 @@
import {Component} from '@angular/core';
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
@Component({
selector: 'app-admin-layout',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
templateUrl: './app-layout.html',
styleUrl: './app-layout.scss'
})
export class AppLayout {}

View file

@ -13,28 +13,30 @@
</div>
<nav class="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
<a routerLink="/admin/approvals" routerLinkActive="bg-emerald-50 text-emerald-700 font-medium"
class="block px-4 py-2.5 rounded-lg text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">
Benutzerverwaltung
</a>
<a routerLink="/admin/energy-communities" routerLinkActive="bg-emerald-50 text-emerald-700 font-medium"
class="block px-4 py-2.5 rounded-lg text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">
Energiegemeinschaften
</a>
@for (item of visibleNavItems(); track item.path) {
<a [routerLink]="item.path"
routerLinkActive="bg-emerald-50 text-emerald-700 font-medium"
[routerLinkActiveOptions]="{exact: item.path === '/dashboard'}"
class="block px-4 py-2.5 rounded-lg text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">
{{ item.label }}
</a>
}
</nav>
</aside>
<main class="flex-1 flex flex-col min-w-0 overflow-hidden">
<header class="h-16 bg-white border-b border-slate-200 flex items-center px-8 justify-between shrink-0">
<h1 class="text-lg font-semibold text-slate-800">Verwaltung</h1>
<h1 class="text-lg font-semibold text-slate-800">
{{ authService.currentUserRole() === 'ADMIN' ? 'Verwaltung' : 'Mein Bereich' }}
</h1>
<div class="flex items-center gap-2 text-sm text-slate-600 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200">
<span class="relative flex h-2.5 w-2.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</span>
Admin
{{ authService.currentUserRole() }}
</div>
</header>

View file

@ -1,17 +1,17 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {AppLayout} from './app-layout';
import {DashboardLayout} from './dashboard-layout';
describe('AdminLayout', () => {
let component: AppLayout;
let fixture: ComponentFixture<AppLayout>;
describe('DashboardLayout', () => {
let component: DashboardLayout;
let fixture: ComponentFixture<DashboardLayout>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppLayout],
imports: [DashboardLayout],
}).compileComponents();
fixture = TestBed.createComponent(AppLayout);
fixture = TestBed.createComponent(DashboardLayout);
component = fixture.componentInstance;
await fixture.whenStable();
});

View file

@ -0,0 +1,20 @@
import {Component, computed, inject} from '@angular/core';
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {AuthService} from '../../services/auth';
import {NAV_ITEMS} from './dashboard-nav-items';
@Component({
selector: 'app-dashboard-layout',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
templateUrl: './dashboard-layout.html',
styleUrl: './dashboard-layout.scss'
})
export class DashboardLayout {
authService = inject(AuthService);
visibleNavItems = computed(() => {
const role = this.authService.currentUserRole();
return NAV_ITEMS.filter(item => role !== null && item.roles.includes(role));
});
}

View file

@ -0,0 +1,13 @@
export interface NavItem {
label: string;
path: string;
roles: string[];
}
// Neue Einträge einfach hinzufügen, z.B. für Member-spezifische Seiten:
// {label: 'Meine Zählpunkte', path: '/dashboard/metering-points', roles: ['MEMBER']}
export const NAV_ITEMS: NavItem[] = [
{label: 'Übersicht', path: '/dashboard', roles: ['ADMIN', 'MEMBER']},
{label: 'Benutzerverwaltung', path: '/dashboard/approvals', roles: ['ADMIN']},
{label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']}
];

View file

@ -0,0 +1,33 @@
@if (isLoading()) {
<div class="text-slate-500">Lade Daten...</div>
} @else if (role === 'ADMIN' && adminStats(); as stats) {
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Energiegemeinschaften</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalCommunities }}</p>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Offene Freigaben</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingUsers }}</p>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Aktive Mitgliedschaften</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalActiveMemberships }}</p>
</div>
</div>
} @else if (userStats(); as stats) {
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Meine Zählpunkte</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalMeteringPoints }}</p>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Aktive Mitgliedschaften</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.activeMemberships }}</p>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Offene Anfragen</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingMemberships }}</p>
</div>
</div>
}

View file

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardOverview } from './dashboard-overview';
describe('DashboardOverview', () => {
let component: DashboardOverview;
let fixture: ComponentFixture<DashboardOverview>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DashboardOverview],
}).compileComponents();
fixture = TestBed.createComponent(DashboardOverview);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,32 @@
import {Component, inject, OnInit, signal} from '@angular/core';
import {AuthService} from '../../services/auth';
import {AdminStats, DashboardService, UserStats} from '../../services/dashboard';
@Component({
selector: 'app-dashboard-overview',
standalone: true,
templateUrl: './dashboard-overview.html'
})
export class DashboardOverview implements OnInit {
private authService = inject(AuthService);
private dashboardService = inject(DashboardService);
role = this.authService.currentUserRole();
adminStats = signal<AdminStats | null>(null);
userStats = signal<UserStats | null>(null);
isLoading = signal(true);
ngOnInit() {
if (this.role === 'ADMIN') {
this.dashboardService.getAdminStats().subscribe({
next: stats => { this.adminStats.set(stats); this.isLoading.set(false); },
error: () => this.isLoading.set(false)
});
} else {
this.dashboardService.getUserStats().subscribe({
next: stats => { this.userStats.set(stats); this.isLoading.set(false); },
error: () => this.isLoading.set(false)
});
}
}
}

View file

@ -40,17 +40,10 @@ export class LoginComponent {
}
private redirectBasedOnRole(role: string) {
switch (role) {
case 'ADMIN':
void this.router.navigate(['/admin/approvals']);
break;
case 'MEMBER':
void this.router.navigate(['/dashboard']);
break;
default:
// Fallback, falls die Rolle unbekannt ist
void this.router.navigate(['/']);
break;
if (role === 'ADMIN' || role === 'MEMBER') {
void this.router.navigate(['/dashboard']);
} else {
void this.router.navigate(['/']);
}
}
}

View file

@ -0,0 +1,34 @@
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="bg-white p-8 rounded-xl shadow-lg max-w-md w-full text-center">
@if (isLoading) {
<div class="text-slate-600">
<p>Deine E-Mail wird verifiziert...</p>
</div>
}
@if (!isLoading && isSuccess) {
<div class="text-emerald-600">
<svg class="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<h2 class="text-2xl font-bold mb-2">Verifizierung erfolgreich!</h2>
<p class="mb-6">Deine E-Mail-Adresse wurde bestätigt. Der Administrator prüft nun deinen Zählpunkt.</p>
<a routerLink="/login" class="bg-emerald-600 text-white px-6 py-2 rounded-md hover:bg-emerald-700 transition-colors">
Zum Login
</a>
</div>
}
@if (!isLoading && !isSuccess) {
<div class="text-red-600">
<h2 class="text-2xl font-bold mb-2">Fehler</h2>
<p class="mb-6">Der Link ist ungültig oder abgelaufen.</p>
<a routerLink="/register" class="text-emerald-600 hover:text-emerald-700 font-medium hover:underline">
Zurück zur Registrierung
</a>
</div>
}
</div>
</div>

View file

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VerifyEmail } from './verify-email';
describe('VerifyEmail', () => {
let component: VerifyEmail;
let fixture: ComponentFixture<VerifyEmail>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VerifyEmail],
}).compileComponents();
fixture = TestBed.createComponent(VerifyEmail);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,44 @@
import {Component, inject, OnInit} from '@angular/core';
import {ActivatedRoute, RouterLink} from '@angular/router';
import {HttpClient} from '@angular/common/http';
@Component({
selector: 'app-verify-email',
standalone: true,
imports: [RouterLink], // CommonModule wird für die neue Control Flow Syntax nicht mehr benötigt!
templateUrl: './verify-email.html',
styleUrl: './verify-email.scss'
})
export class VerifyEmailComponent implements OnInit {
private route = inject(ActivatedRoute);
// Tipp: Hier später idealerweise deinen OpenAPI-generierten Service verwenden,
// z.B. private authService = inject(AuthenticationService);
private http = inject(HttpClient);
isLoading = true;
isSuccess = false;
ngOnInit() {
const token = this.route.snapshot.queryParamMap.get('token');
// Ggf. atNumber auslesen, falls du sie hier mit übergibst
if (token) {
// Wenn du OpenAPI nutzt, kannst du hier die entsprechende Methode aufrufen.
// Fürs Beispiel als direkter HTTP-Call:
this.http.get(`http://localhost:8080/api/auth/verify-email?token=${token}`).subscribe({
next: () => {
this.isLoading = false;
this.isSuccess = true;
},
error: () => {
this.isLoading = false;
this.isSuccess = false;
}
});
} else {
this.isLoading = false;
this.isSuccess = false;
}
}
}

View file

@ -1,13 +1,13 @@
import { TestBed } from '@angular/core/testing';
import {TestBed} from '@angular/core/testing';
import { Auth } from './auth';
import {DashboardService} from './dashboard';
describe('Auth', () => {
let service: Auth;
let service: DashboardService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(Auth);
service = TestBed.inject(DashboardService);
});
it('should be created', () => {

View file

@ -0,0 +1,29 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
export interface AdminStats {
totalCommunities: number;
pendingUsers: number;
totalActiveMemberships: number;
}
export interface UserStats {
totalMeteringPoints: number;
activeMemberships: number;
pendingMemberships: number;
}
@Injectable({providedIn: 'root'})
export class DashboardService {
private http = inject(HttpClient);
private apiUrl = 'http://localhost:8080/api/dashboard';
getAdminStats(): Observable<AdminStats> {
return this.http.get<AdminStats>(`${this.apiUrl}/admin`);
}
getUserStats(): Observable<UserStats> {
return this.http.get<UserStats>(`${this.apiUrl}/user`);
}
}