add user profile management
- Add UserController with GET/PUT /me, PUT /me/password, PUT /me/email - Add UserService with profile update, password change, email change - Add UserServiceTest with 7 unit tests - Add IamDtos: UpdateProfileRequest, ChangePasswordRequest, ChangeEmailRequest - Add Angular profile page with edit profile, change password, change email - Add Angular UserService for profile API calls - Add profile route and navigation item
This commit is contained in:
parent
8f74dbde35
commit
52c6641b3a
@ -0,0 +1,55 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.api;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.common.security.CurrentUserId;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.ChangeEmailRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.ChangePasswordRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UpdateProfileRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UserProfileResponse;
|
||||||
|
import at.mueller.eeg.backend.iam.service.UserService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "User Profile", description = "Profilverwaltung für eingeloggte User")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
@Operation(summary = "Eigenes Profil abrufen")
|
||||||
|
public ResponseEntity<UserProfileResponse> getOwnProfile(@CurrentUserId String userId) {
|
||||||
|
return ResponseEntity.ok(userService.getProfile(java.util.UUID.fromString(userId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/me")
|
||||||
|
@Operation(summary = "Profil aktualisieren")
|
||||||
|
public ResponseEntity<UserProfileResponse> updateOwnProfile(
|
||||||
|
@CurrentUserId String userId,
|
||||||
|
@Valid @RequestBody UpdateProfileRequest request) {
|
||||||
|
return ResponseEntity.ok(userService.updateProfile(java.util.UUID.fromString(userId), request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/me/password")
|
||||||
|
@Operation(summary = "Passwort ändern")
|
||||||
|
public ResponseEntity<Void> changePassword(
|
||||||
|
@CurrentUserId String userId,
|
||||||
|
@Valid @RequestBody ChangePasswordRequest request) {
|
||||||
|
userService.changePassword(java.util.UUID.fromString(userId), request);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/me/email")
|
||||||
|
@Operation(summary = "E-Mail ändern (mit erneuter Verifizierung)")
|
||||||
|
public ResponseEntity<Void> changeEmail(
|
||||||
|
@CurrentUserId String userId,
|
||||||
|
@Valid @RequestBody ChangeEmailRequest request) {
|
||||||
|
userService.changeEmail(java.util.UUID.fromString(userId), request);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -53,4 +53,33 @@ public interface IamDtos {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
record ErrorResponse(String message, int status) {}
|
record ErrorResponse(String message, int status) {}
|
||||||
|
|
||||||
|
@Schema(description = "Payload für die Profilaktualisierung")
|
||||||
|
record UpdateProfileRequest(
|
||||||
|
@NotBlank
|
||||||
|
@Schema(description = "Vorname", example = "Max")
|
||||||
|
String firstName,
|
||||||
|
@NotBlank
|
||||||
|
@Schema(description = "Nachname", example = "Mustermann")
|
||||||
|
String lastName,
|
||||||
|
@Schema(description = "Nur bei juristischer Person: Firmen-, Vereins- oder Gemeindename")
|
||||||
|
String organizationName,
|
||||||
|
@NotNull
|
||||||
|
@Schema(description = "Privatperson oder juristische Person", example = "PRIVATE")
|
||||||
|
ParticipantType participantType
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Schema(description = "Payload für die Passwortänderung")
|
||||||
|
record ChangePasswordRequest(
|
||||||
|
@NotBlank
|
||||||
|
@Schema(description = "Aktuelles Passwort") String currentPassword,
|
||||||
|
@NotBlank @Size(min = 8)
|
||||||
|
@Schema(description = "Neues Passwort", example = "NeuesPasswort123!") String newPassword
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Schema(description = "Payload für die E-Mail-Änderung")
|
||||||
|
record ChangeEmailRequest(
|
||||||
|
@Email @NotBlank
|
||||||
|
@Schema(description = "Neue E-Mail-Adresse", example = "neu@example.com") String newEmail
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.service;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.ChangeEmailRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.ChangePasswordRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UpdateProfileRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UserProfileResponse;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.User;
|
||||||
|
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserProfileResponse getProfile(UUID userId) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("User nicht gefunden"));
|
||||||
|
return toProfileResponse(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserProfileResponse updateProfile(UUID userId, UpdateProfileRequest request) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("User nicht gefunden"));
|
||||||
|
|
||||||
|
user.setFirstName(request.firstName());
|
||||||
|
user.setLastName(request.lastName());
|
||||||
|
user.setOrganizationName(request.organizationName());
|
||||||
|
user.setParticipantType(request.participantType());
|
||||||
|
|
||||||
|
User saved = userRepository.save(user);
|
||||||
|
return toProfileResponse(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void changePassword(UUID userId, ChangePasswordRequest request) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("User nicht gefunden"));
|
||||||
|
|
||||||
|
if (!passwordEncoder.matches(request.currentPassword(), user.getPasswordHash())) {
|
||||||
|
throw new IllegalArgumentException("Aktuelles Passwort ist falsch");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(request.newPassword()));
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void changeEmail(UUID userId, ChangeEmailRequest request) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("User nicht gefunden"));
|
||||||
|
|
||||||
|
if (userRepository.findByEmail(request.newEmail()).isPresent()) {
|
||||||
|
throw new IllegalArgumentException("E-Mail-Adresse ist bereits vergeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setEmail(request.newEmail());
|
||||||
|
user.setEmailVerified(false);
|
||||||
|
user.setVerificationToken(UUID.randomUUID().toString());
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserProfileResponse toProfileResponse(User user) {
|
||||||
|
return new UserProfileResponse(
|
||||||
|
user.getId(),
|
||||||
|
user.getParticipantType(),
|
||||||
|
user.getFirstName(),
|
||||||
|
user.getLastName(),
|
||||||
|
user.getOrganizationName(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getStatus().name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
package at.mueller.eeg.backend.iam.service;
|
||||||
|
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.ChangeEmailRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.ChangePasswordRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UpdateProfileRequest;
|
||||||
|
import at.mueller.eeg.backend.iam.api.dto.IamDtos.UserProfileResponse;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.ParticipantType;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.User;
|
||||||
|
import at.mueller.eeg.backend.iam.domain.UserRole;
|
||||||
|
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
private User testUser;
|
||||||
|
private UUID testUserId;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
testUserId = UUID.randomUUID();
|
||||||
|
testUser = new User();
|
||||||
|
testUser.setId(testUserId);
|
||||||
|
testUser.setEmail("test@example.com");
|
||||||
|
testUser.setPasswordHash(passwordEncoder.encode("OldPass123!"));
|
||||||
|
testUser.setFirstName("Test");
|
||||||
|
testUser.setLastName("Benutzer");
|
||||||
|
testUser.setParticipantType(ParticipantType.PRIVATE);
|
||||||
|
testUser.setRole(UserRole.MEMBER);
|
||||||
|
testUser.setStatus(RegistrationStatus.APPROVED);
|
||||||
|
testUser.setEnabled(true);
|
||||||
|
testUser.setEmailVerified(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getProfile_returnsCorrectUser() {
|
||||||
|
when(userRepository.findById(testUserId)).thenReturn(Optional.of(testUser));
|
||||||
|
|
||||||
|
UserProfileResponse response = userService.getProfile(testUserId);
|
||||||
|
|
||||||
|
assertEquals(testUserId, response.id());
|
||||||
|
assertEquals("Test", response.firstName());
|
||||||
|
assertEquals("Benutzer", response.lastName());
|
||||||
|
assertEquals("test@example.com", response.email());
|
||||||
|
assertEquals(ParticipantType.PRIVATE, response.participantType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getProfile_throwsForUnknownUser() {
|
||||||
|
when(userRepository.findById(any())).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> userService.getProfile(UUID.randomUUID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateProfile_updatesFields() {
|
||||||
|
when(userRepository.findById(testUserId)).thenReturn(Optional.of(testUser));
|
||||||
|
when(userRepository.save(any())).thenReturn(testUser);
|
||||||
|
|
||||||
|
UpdateProfileRequest request = new UpdateProfileRequest(
|
||||||
|
"Max", "Mustermann", "Test GmbH", ParticipantType.COMPANY);
|
||||||
|
|
||||||
|
UserProfileResponse response = userService.updateProfile(testUserId, request);
|
||||||
|
|
||||||
|
assertEquals("Max", response.firstName());
|
||||||
|
assertEquals("Mustermann", response.lastName());
|
||||||
|
assertEquals("Test GmbH", response.organizationName());
|
||||||
|
assertEquals(ParticipantType.COMPANY, response.participantType());
|
||||||
|
verify(userRepository).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changePassword_throwsForWrongCurrentPassword() {
|
||||||
|
when(userRepository.findById(testUserId)).thenReturn(Optional.of(testUser));
|
||||||
|
when(passwordEncoder.matches("WrongPass", testUser.getPasswordHash())).thenReturn(false);
|
||||||
|
|
||||||
|
ChangePasswordRequest request = new ChangePasswordRequest("WrongPass", "NewPass123!");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> userService.changePassword(testUserId, request));
|
||||||
|
verify(userRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changePassword_updatesHash() {
|
||||||
|
when(userRepository.findById(testUserId)).thenReturn(Optional.of(testUser));
|
||||||
|
when(passwordEncoder.matches("OldPass123!", testUser.getPasswordHash())).thenReturn(true);
|
||||||
|
when(passwordEncoder.encode("NewPass123!")).thenReturn("encoded_new_password");
|
||||||
|
|
||||||
|
ChangePasswordRequest request = new ChangePasswordRequest("OldPass123!", "NewPass123!");
|
||||||
|
|
||||||
|
userService.changePassword(testUserId, request);
|
||||||
|
|
||||||
|
verify(passwordEncoder).encode("NewPass123!");
|
||||||
|
verify(userRepository).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changeEmail_throwsForDuplicateEmail() {
|
||||||
|
when(userRepository.findById(testUserId)).thenReturn(Optional.of(testUser));
|
||||||
|
when(userRepository.findByEmail("taken@example.com")).thenReturn(Optional.of(new User()));
|
||||||
|
|
||||||
|
ChangeEmailRequest request = new ChangeEmailRequest("taken@example.com");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> userService.changeEmail(testUserId, request));
|
||||||
|
verify(userRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changeEmail_setsVerificationToken() {
|
||||||
|
when(userRepository.findById(testUserId)).thenReturn(Optional.of(testUser));
|
||||||
|
when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
|
||||||
|
when(userRepository.save(any())).thenReturn(testUser);
|
||||||
|
|
||||||
|
ChangeEmailRequest request = new ChangeEmailRequest("new@example.com");
|
||||||
|
|
||||||
|
userService.changeEmail(testUserId, request);
|
||||||
|
|
||||||
|
verify(userRepository).save(argThat(user -> {
|
||||||
|
User u = (User) user;
|
||||||
|
return "new@example.com".equals(u.getEmail())
|
||||||
|
&& !u.isEmailVerified()
|
||||||
|
&& u.getVerificationToken() != null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import {DashboardOverview} from './pages/dashboard-overview/dashboard-overview';
|
|||||||
import {AdminEnergyCommunity} from './pages/admin-energy-community/admin-energy-community';
|
import {AdminEnergyCommunity} from './pages/admin-energy-community/admin-energy-community';
|
||||||
import {VerifyEmailComponent} from './pages/verify-email/verify-email';
|
import {VerifyEmailComponent} from './pages/verify-email/verify-email';
|
||||||
import {MeteringPointsComponent} from './pages/metering-points/metering-points';
|
import {MeteringPointsComponent} from './pages/metering-points/metering-points';
|
||||||
|
import {ProfileComponent} from './pages/profile/profile';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: LandingPage },
|
{ path: '', component: LandingPage },
|
||||||
@ -36,6 +37,11 @@ export const routes: Routes = [
|
|||||||
path: 'energy-communities',
|
path: 'energy-communities',
|
||||||
component: AdminEnergyCommunity,
|
component: AdminEnergyCommunity,
|
||||||
canActivate: [roleGuard(['ADMIN'])]
|
canActivate: [roleGuard(['ADMIN'])]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
component: ProfileComponent,
|
||||||
|
canActivate: [roleGuard(['ADMIN', 'MEMBER'])]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,11 +4,10 @@ export interface NavItem {
|
|||||||
roles: 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[] = [
|
export const NAV_ITEMS: NavItem[] = [
|
||||||
{label: 'Übersicht', path: '/dashboard', roles: ['ADMIN', 'MEMBER']},
|
{label: 'Übersicht', path: '/dashboard', roles: ['ADMIN', 'MEMBER']},
|
||||||
{label: 'Zählpunkte', path: '/dashboard/metering-points', roles: ['ADMIN', 'MEMBER']},
|
{label: 'Zählpunkte', path: '/dashboard/metering-points', roles: ['ADMIN', 'MEMBER']},
|
||||||
|
{label: 'Mein Profil', path: '/dashboard/profile', roles: ['ADMIN', 'MEMBER']},
|
||||||
{label: 'Benutzerverwaltung', path: '/dashboard/approvals', roles: ['ADMIN']},
|
{label: 'Benutzerverwaltung', path: '/dashboard/approvals', roles: ['ADMIN']},
|
||||||
{label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']}
|
{label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']}
|
||||||
];
|
];
|
||||||
|
|||||||
106
eeg_frontend/src/app/pages/profile/profile.html
Normal file
106
eeg_frontend/src/app/pages/profile/profile.html
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<div class="max-w-2xl mx-auto p-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Mein Profil</h1>
|
||||||
|
|
||||||
|
@if (isLoading()) {
|
||||||
|
<div class="text-center py-8 text-gray-500">Lade Profil...</div>
|
||||||
|
} @else if (profile()) {
|
||||||
|
<!-- Erfolg/Fehler-Meldungen -->
|
||||||
|
@if (successMessage()) {
|
||||||
|
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
||||||
|
{{ successMessage() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (errorMessage()) {
|
||||||
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||||
|
{{ errorMessage() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Profil-Informationen -->
|
||||||
|
<div class="bg-white shadow rounded-lg p-6 mb-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">Persönliche Daten</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Vorname</label>
|
||||||
|
<input type="text" [(ngModel)]="editFirstName"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Nachname</label>
|
||||||
|
<input type="text" [(ngModel)]="editLastName"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Teilnehmertyp</label>
|
||||||
|
<select [(ngModel)]="editParticipantType"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="PRIVATE">Privatperson</option>
|
||||||
|
<option value="COMPANY">Juristische Person</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
@if (editParticipantType === 'COMPANY') {
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Organisationsname</label>
|
||||||
|
<input type="text" [(ngModel)]="editOrganizationName"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">E-Mail</label>
|
||||||
|
<div class="text-gray-900">{{ profile()!.email }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button (click)="updateProfile()" [disabled]="isSaving()"
|
||||||
|
class="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||||
|
{{ isSaving() ? 'Speichere...' : 'Profil speichern' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passwort ändern -->
|
||||||
|
<div class="bg-white shadow rounded-lg p-6 mb-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">Passwort ändern</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Aktuelles Passwort</label>
|
||||||
|
<input type="password" [(ngModel)]="currentPassword"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Neues Passwort</label>
|
||||||
|
<input type="password" [(ngModel)]="newPassword"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Neues Passwort bestätigen</label>
|
||||||
|
<input type="password" [(ngModel)]="newPasswordConfirm"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button (click)="changePassword()" [disabled]="isSaving()"
|
||||||
|
class="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||||
|
{{ isSaving() ? 'Ändere...' : 'Passwort ändern' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- E-Mail ändern -->
|
||||||
|
<div class="bg-white shadow rounded-lg p-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">E-Mail-Adresse ändern</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Neue E-Mail-Adresse</label>
|
||||||
|
<input type="email" [(ngModel)]="newEmail" placeholder="neu@example.com"
|
||||||
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button (click)="changeEmail()" [disabled]="isSaving()"
|
||||||
|
class="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||||
|
{{ isSaving() ? 'Ändere...' : 'E-Mail ändern' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
122
eeg_frontend/src/app/pages/profile/profile.ts
Normal file
122
eeg_frontend/src/app/pages/profile/profile.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import {Component, inject, OnInit, signal} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {UserService, UserProfile, UpdateProfileRequest} from '../../services/user';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-profile',
|
||||||
|
standalone: true,
|
||||||
|
imports: [FormsModule],
|
||||||
|
templateUrl: './profile.html'
|
||||||
|
})
|
||||||
|
export class ProfileComponent implements OnInit {
|
||||||
|
private userService = inject(UserService);
|
||||||
|
|
||||||
|
profile = signal<UserProfile | null>(null);
|
||||||
|
isLoading = signal(true);
|
||||||
|
isSaving = signal(false);
|
||||||
|
successMessage = signal('');
|
||||||
|
errorMessage = signal('');
|
||||||
|
|
||||||
|
editFirstName = '';
|
||||||
|
editLastName = '';
|
||||||
|
editOrganizationName = '';
|
||||||
|
editParticipantType = 'PRIVATE';
|
||||||
|
|
||||||
|
currentPassword = '';
|
||||||
|
newPassword = '';
|
||||||
|
newPasswordConfirm = '';
|
||||||
|
|
||||||
|
newEmail = '';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.loadProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProfile() {
|
||||||
|
this.userService.getProfile().subscribe({
|
||||||
|
next: profile => {
|
||||||
|
this.profile.set(profile);
|
||||||
|
this.editFirstName = profile.firstName || '';
|
||||||
|
this.editLastName = profile.lastName || '';
|
||||||
|
this.editOrganizationName = profile.organizationName || '';
|
||||||
|
this.editParticipantType = profile.participantType;
|
||||||
|
this.isLoading.set(false);
|
||||||
|
},
|
||||||
|
error: () => this.isLoading.set(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProfile() {
|
||||||
|
if (this.isSaving()) return;
|
||||||
|
this.isSaving.set(true);
|
||||||
|
this.successMessage.set('');
|
||||||
|
this.errorMessage.set('');
|
||||||
|
|
||||||
|
const request: UpdateProfileRequest = {
|
||||||
|
firstName: this.editFirstName,
|
||||||
|
lastName: this.editLastName,
|
||||||
|
organizationName: this.editOrganizationName || undefined,
|
||||||
|
participantType: this.editParticipantType as 'PRIVATE' | 'COMPANY'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.userService.updateProfile(request).subscribe({
|
||||||
|
next: profile => {
|
||||||
|
this.profile.set(profile);
|
||||||
|
this.isSaving.set(false);
|
||||||
|
this.successMessage.set('Profil erfolgreich aktualisiert.');
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.isSaving.set(false);
|
||||||
|
this.errorMessage.set(err.error?.message || 'Fehler beim Aktualisieren.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changePassword() {
|
||||||
|
if (this.isSaving() || !this.currentPassword || !this.newPassword) return;
|
||||||
|
if (this.newPassword !== this.newPasswordConfirm) {
|
||||||
|
this.errorMessage.set('Passwörter stimmen nicht überein.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSaving.set(true);
|
||||||
|
this.successMessage.set('');
|
||||||
|
this.errorMessage.set('');
|
||||||
|
|
||||||
|
this.userService.changePassword({
|
||||||
|
currentPassword: this.currentPassword,
|
||||||
|
newPassword: this.newPassword
|
||||||
|
}).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.isSaving.set(false);
|
||||||
|
this.successMessage.set('Passwort erfolgreich geändert.');
|
||||||
|
this.currentPassword = '';
|
||||||
|
this.newPassword = '';
|
||||||
|
this.newPasswordConfirm = '';
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.isSaving.set(false);
|
||||||
|
this.errorMessage.set(err.error?.message || 'Fehler beim Ändern des Passworts.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeEmail() {
|
||||||
|
if (this.isSaving() || !this.newEmail) return;
|
||||||
|
this.isSaving.set(true);
|
||||||
|
this.successMessage.set('');
|
||||||
|
this.errorMessage.set('');
|
||||||
|
|
||||||
|
this.userService.changeEmail({newEmail: this.newEmail}).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.isSaving.set(false);
|
||||||
|
this.successMessage.set('E-Mail-Adresse geändert. Bitte neue Adresse verifizieren.');
|
||||||
|
this.newEmail = '';
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.isSaving.set(false);
|
||||||
|
this.errorMessage.set(err.error?.message || 'Fehler beim Ändern der E-Mail.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
51
eeg_frontend/src/app/services/user.ts
Normal file
51
eeg_frontend/src/app/services/user.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import {inject, Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
participantType: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
organizationName: string | null;
|
||||||
|
email: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProfileRequest {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
organizationName?: string;
|
||||||
|
participantType: 'PRIVATE' | 'COMPANY';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePasswordRequest {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeEmailRequest {
|
||||||
|
newEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
export class UserService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private apiUrl = 'http://localhost:8080/api';
|
||||||
|
|
||||||
|
getProfile(): Observable<UserProfile> {
|
||||||
|
return this.http.get<UserProfile>(`${this.apiUrl}/users/me`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProfile(request: UpdateProfileRequest): Observable<UserProfile> {
|
||||||
|
return this.http.put<UserProfile>(`${this.apiUrl}/users/me`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
changePassword(request: ChangePasswordRequest): Observable<void> {
|
||||||
|
return this.http.put<void>(`${this.apiUrl}/users/me/password`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeEmail(request: ChangeEmailRequest): Observable<void> {
|
||||||
|
return this.http.put<void>(`${this.apiUrl}/users/me/email`, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user