generalized dashboard for member and admin
This commit is contained in:
parent
caf05d3103
commit
59a0b6b9b7
38 changed files with 528 additions and 93 deletions
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
) {}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package at.mueller.eeg.backend.iam.service;
|
||||
|
||||
public interface MailService {
|
||||
void sendVerificationEmail(String toEmail, String token);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'])]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({});
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
};
|
||||
15
eeg_frontend/src/app/guards/auth-guard.ts
Normal file
15
eeg_frontend/src/app/guards/auth-guard.ts
Normal 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']);
|
||||
};
|
||||
18
eeg_frontend/src/app/guards/role-guard.ts
Normal file
18
eeg_frontend/src/app/guards/role-guard.ts
Normal 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']);
|
||||
};
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
@ -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']}
|
||||
];
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(['/']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
eeg_frontend/src/app/pages/verify-email/verify-email.html
Normal file
34
eeg_frontend/src/app/pages/verify-email/verify-email.html
Normal 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>
|
||||
22
eeg_frontend/src/app/pages/verify-email/verify-email.spec.ts
Normal file
22
eeg_frontend/src/app/pages/verify-email/verify-email.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
44
eeg_frontend/src/app/pages/verify-email/verify-email.ts
Normal file
44
eeg_frontend/src/app/pages/verify-email/verify-email.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
29
eeg_frontend/src/app/services/dashboard.ts
Normal file
29
eeg_frontend/src/app/services/dashboard.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue