diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ef3150..e9801cc 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/eeg_backend/pom.xml b/eeg_backend/pom.xml index f740058..fd1106a 100644 --- a/eeg_backend/pom.xml +++ b/eeg_backend/pom.xml @@ -37,6 +37,10 @@ org.springframework.boot spring-boot-starter-oauth2-resource-server + + org.springframework.boot + spring-boot-starter-mail + org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/api/DashboardController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/api/DashboardController.java new file mode 100644 index 0000000..d183d13 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/api/DashboardController.java @@ -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 getAdminDashboard() { + return ResponseEntity.ok(dashboardService.getAdminStats()); + } + + @GetMapping("/user") + @PreAuthorize("hasRole('MEMBER')") + public ResponseEntity getUserDashboard(@AuthenticationPrincipal User user) { + // user.getId() funktioniert, da UserDetails implementiert ist + return ResponseEntity.ok(dashboardService.getUserStats(user.getId())); + } +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/api/dto/DashboardStatsDto.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/api/dto/DashboardStatsDto.java new file mode 100644 index 0000000..694e0d9 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/api/dto/DashboardStatsDto.java @@ -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 + ) {} +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/service/DashboardService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/service/DashboardService.java new file mode 100644 index 0000000..093b641 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/dashboard/service/DashboardService.java @@ -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); + } +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java index 851b8e1..b000225 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AuthController.java @@ -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 verifyEmail(@RequestParam String token) { + authService.verifyEmail(token); + return ResponseEntity.ok().build(); + } } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java index da6e2c0..5a47f99 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/domain/User.java @@ -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; diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java index 4e2baf2..c5011bb 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/mapper/UserMapper.java @@ -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); diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/repository/UserRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/repository/UserRepository.java index bb992bb..f37ac10 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/repository/UserRepository.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/repository/UserRepository.java @@ -12,5 +12,7 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); - List findAllByStatus(RegistrationStatus status); + List findAllByStatusAndEmailVerified(RegistrationStatus status, Boolean verified); + + Optional findByVerificationToken(String token); } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AdminIamService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AdminIamService.java index b3306cb..7acad0d 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AdminIamService.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AdminIamService.java @@ -23,7 +23,7 @@ public class AdminIamService { private final ApplicationEventPublisher eventPublisher; public List getPendingUsers() { - return userRepository.findAllByStatus(RegistrationStatus.PENDING); + return userRepository.findAllByStatusAndEmailVerified(RegistrationStatus.PENDING, true); } @Transactional diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java index 299570c..f0be05a 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AuthService.java @@ -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); + } /** diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/ConsoleMailService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/ConsoleMailService.java new file mode 100644 index 0000000..61e3369 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/ConsoleMailService.java @@ -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); + } +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/MailService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/MailService.java new file mode 100644 index 0000000..a9a977f --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/MailService.java @@ -0,0 +1,5 @@ +package at.mueller.eeg.backend.iam.service; + +public interface MailService { + void sendVerificationEmail(String toEmail, String token); +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/SmtpMailService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/SmtpMailService.java new file mode 100644 index 0000000..0dda31f --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/SmtpMailService.java @@ -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); + } +} \ No newline at end of file diff --git a/eeg_frontend/src/app/app.html b/eeg_frontend/src/app/app.html index c385ddd..558b19c 100644 --- a/eeg_frontend/src/app/app.html +++ b/eeg_frontend/src/app/app.html @@ -1,8 +1,6 @@ - -@if (!isAdminRoute) { +@if (!isDashboardRoute) { } -
diff --git a/eeg_frontend/src/app/app.routes.ts b/eeg_frontend/src/app/app.routes.ts index a951c8a..c22920a 100644 --- a/eeg_frontend/src/app/app.routes.ts +++ b/eeg_frontend/src/app/app.routes.ts @@ -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'])] } ] }, diff --git a/eeg_frontend/src/app/app.ts b/eeg_frontend/src/app/app.ts index 044bc87..5fc4d88 100644 --- a/eeg_frontend/src/app/app.ts +++ b/eeg_frontend/src/app/app.ts @@ -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'); } }); } diff --git a/eeg_frontend/src/app/guards/admin-auth-guard.spec.ts b/eeg_frontend/src/app/guards/admin-auth-guard.spec.ts index 7a95060..2c581e7 100644 --- a/eeg_frontend/src/app/guards/admin-auth-guard.spec.ts +++ b/eeg_frontend/src/app/guards/admin-auth-guard.spec.ts @@ -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({}); diff --git a/eeg_frontend/src/app/guards/admin-auth-guard.ts b/eeg_frontend/src/app/guards/admin-auth-guard.ts deleted file mode 100644 index 51da2c3..0000000 --- a/eeg_frontend/src/app/guards/admin-auth-guard.ts +++ /dev/null @@ -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']); -}; diff --git a/eeg_frontend/src/app/guards/auth-guard.ts b/eeg_frontend/src/app/guards/auth-guard.ts new file mode 100644 index 0000000..6d7e39a --- /dev/null +++ b/eeg_frontend/src/app/guards/auth-guard.ts @@ -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']); +}; diff --git a/eeg_frontend/src/app/guards/role-guard.ts b/eeg_frontend/src/app/guards/role-guard.ts new file mode 100644 index 0000000..06693b5 --- /dev/null +++ b/eeg_frontend/src/app/guards/role-guard.ts @@ -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']); + }; +} diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.ts b/eeg_frontend/src/app/layout/app-layout/app-layout.ts deleted file mode 100644 index 3cadcc2..0000000 --- a/eeg_frontend/src/app/layout/app-layout/app-layout.ts +++ /dev/null @@ -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 {} diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.html b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.html similarity index 70% rename from eeg_frontend/src/app/layout/app-layout/app-layout.html rename to eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.html index 19617de..01c8954 100644 --- a/eeg_frontend/src/app/layout/app-layout/app-layout.html +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.html @@ -13,28 +13,30 @@
-

Verwaltung

+

+ {{ authService.currentUserRole() === 'ADMIN' ? 'Verwaltung' : 'Mein Bereich' }} +

- Admin + {{ authService.currentUserRole() }}
diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.scss b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.scss similarity index 100% rename from eeg_frontend/src/app/layout/app-layout/app-layout.scss rename to eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.scss diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.spec.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.spec.ts similarity index 55% rename from eeg_frontend/src/app/layout/app-layout/app-layout.spec.ts rename to eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.spec.ts index 9ee1bd0..68bbeec 100644 --- a/eeg_frontend/src/app/layout/app-layout/app-layout.spec.ts +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.spec.ts @@ -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; +describe('DashboardLayout', () => { + let component: DashboardLayout; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AppLayout], + imports: [DashboardLayout], }).compileComponents(); - fixture = TestBed.createComponent(AppLayout); + fixture = TestBed.createComponent(DashboardLayout); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts new file mode 100644 index 0000000..fde8d4a --- /dev/null +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts @@ -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)); + }); +} diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts new file mode 100644 index 0000000..2758f97 --- /dev/null +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts @@ -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']} +]; diff --git a/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.html b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.html new file mode 100644 index 0000000..34e9e74 --- /dev/null +++ b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.html @@ -0,0 +1,33 @@ +@if (isLoading()) { +
Lade Daten...
+} @else if (role === 'ADMIN' && adminStats(); as stats) { +
+
+

Energiegemeinschaften

+

{{ stats.totalCommunities }}

+
+
+

Offene Freigaben

+

{{ stats.pendingUsers }}

+
+
+

Aktive Mitgliedschaften

+

{{ stats.totalActiveMemberships }}

+
+
+} @else if (userStats(); as stats) { +
+
+

Meine Zählpunkte

+

{{ stats.totalMeteringPoints }}

+
+
+

Aktive Mitgliedschaften

+

{{ stats.activeMemberships }}

+
+
+

Offene Anfragen

+

{{ stats.pendingMemberships }}

+
+
+} diff --git a/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.scss b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.scss new file mode 100644 index 0000000..e69de29 diff --git a/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.spec.ts b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.spec.ts new file mode 100644 index 0000000..dc264a4 --- /dev/null +++ b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardOverview } from './dashboard-overview'; + +describe('DashboardOverview', () => { + let component: DashboardOverview; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardOverview], + }).compileComponents(); + + fixture = TestBed.createComponent(DashboardOverview); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.ts b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.ts new file mode 100644 index 0000000..9fd5c52 --- /dev/null +++ b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.ts @@ -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(null); + userStats = signal(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) + }); + } + } +} diff --git a/eeg_frontend/src/app/pages/login/login.ts b/eeg_frontend/src/app/pages/login/login.ts index 3d7af8d..3b39c74 100644 --- a/eeg_frontend/src/app/pages/login/login.ts +++ b/eeg_frontend/src/app/pages/login/login.ts @@ -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(['/']); } } } diff --git a/eeg_frontend/src/app/pages/verify-email/verify-email.html b/eeg_frontend/src/app/pages/verify-email/verify-email.html new file mode 100644 index 0000000..aeb99bc --- /dev/null +++ b/eeg_frontend/src/app/pages/verify-email/verify-email.html @@ -0,0 +1,34 @@ +
+
+ + @if (isLoading) { +
+

Deine E-Mail wird verifiziert...

+
+ } + + @if (!isLoading && isSuccess) { +
+ + + +

Verifizierung erfolgreich!

+

Deine E-Mail-Adresse wurde bestätigt. Der Administrator prüft nun deinen Zählpunkt.

+ + Zum Login + +
+ } + + @if (!isLoading && !isSuccess) { +
+

Fehler

+

Der Link ist ungültig oder abgelaufen.

+ + Zurück zur Registrierung + +
+ } + +
+
diff --git a/eeg_frontend/src/app/pages/verify-email/verify-email.scss b/eeg_frontend/src/app/pages/verify-email/verify-email.scss new file mode 100644 index 0000000..e69de29 diff --git a/eeg_frontend/src/app/pages/verify-email/verify-email.spec.ts b/eeg_frontend/src/app/pages/verify-email/verify-email.spec.ts new file mode 100644 index 0000000..901eb04 --- /dev/null +++ b/eeg_frontend/src/app/pages/verify-email/verify-email.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VerifyEmail } from './verify-email'; + +describe('VerifyEmail', () => { + let component: VerifyEmail; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VerifyEmail], + }).compileComponents(); + + fixture = TestBed.createComponent(VerifyEmail); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/eeg_frontend/src/app/pages/verify-email/verify-email.ts b/eeg_frontend/src/app/pages/verify-email/verify-email.ts new file mode 100644 index 0000000..46c8339 --- /dev/null +++ b/eeg_frontend/src/app/pages/verify-email/verify-email.ts @@ -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; + } + } +} diff --git a/eeg_frontend/src/app/services/auth.spec.ts b/eeg_frontend/src/app/services/auth.spec.ts index 3a04d76..cf654c1 100644 --- a/eeg_frontend/src/app/services/auth.spec.ts +++ b/eeg_frontend/src/app/services/auth.spec.ts @@ -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', () => { diff --git a/eeg_frontend/src/app/services/dashboard.ts b/eeg_frontend/src/app/services/dashboard.ts new file mode 100644 index 0000000..e6506a1 --- /dev/null +++ b/eeg_frontend/src/app/services/dashboard.ts @@ -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 { + return this.http.get(`${this.apiUrl}/admin`); + } + + getUserStats(): Observable { + return this.http.get(`${this.apiUrl}/user`); + } +}