Skip to content

Commit 0ccebe5

Browse files
Dev (#6)
* bugfix: translate navbar to support German and English * feat: implement email verification feature with token management and user notifications (#5) Co-authored-by: Minh Le <vuminhle@outlook.com> --------- Co-authored-by: Minh Le <vuminhle@outlook.com>
1 parent 83c5cc8 commit 0ccebe5

18 files changed

Lines changed: 821 additions & 40 deletions

File tree

‎.DS_Store‎

2 KB
Binary file not shown.

‎backend/build.gradle‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,4 @@ tasks.register('runPdfLister', JavaExec) {
7373

7474
tasks.named('test') {
7575
useJUnitPlatform()
76-
}
76+
}

‎backend/src/main/java/org/example/springboot/controller/authController/AuthController.java‎

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@
3030
import org.springframework.security.core.Authentication;
3131
import org.springframework.security.core.AuthenticationException;
3232
import org.springframework.security.crypto.password.PasswordEncoder;
33-
import org.springframework.web.bind.annotation.PostMapping;
34-
import org.springframework.web.bind.annotation.RequestBody;
35-
import org.springframework.web.bind.annotation.RequestMapping;
36-
import org.springframework.web.bind.annotation.RestController;
33+
import org.springframework.web.bind.annotation.*;
3734

3835
import java.util.Map;
3936
import java.util.Optional;
@@ -113,7 +110,7 @@ public AuthController(
113110
* Dieser Entpunkt ist zuständig für die Registrierung
114111
*/
115112
@PostMapping("/register")
116-
@Operation(summary = "Registriert einen neuen Benutzer", description = "Speichert den Benutzer mit einem zufällig generierten Passwort und sendet die Zugangsdaten per E-Mail.")
113+
@Operation(summary = "Registriert einen neuen Benutzer", description = "Speichert den Benutzer und sendet eine E-Mail zur Verifizierung der E-Mail-Adresse.")
117114
public ResponseEntity<?> registerUser(@Valid @RequestBody RegistrationRequest request) {
118115
if (userRepository.existsByUsername(request.getUsername())) {
119116
return new ResponseEntity<>("Benutzername ist bereits vergeben!", HttpStatus.BAD_REQUEST);
@@ -122,8 +119,6 @@ public ResponseEntity<?> registerUser(@Valid @RequestBody RegistrationRequest re
122119
return new ResponseEntity<>("E-Mail wird bereits verwendet!", HttpStatus.BAD_REQUEST);
123120
}
124121

125-
String temporaryPassword = generateRandomPassword();
126-
127122
User user = new User();
128123
user.setUsername(request.getUsername());
129124
user.setName(request.getName());
@@ -132,17 +127,27 @@ public ResponseEntity<?> registerUser(@Valid @RequestBody RegistrationRequest re
132127
user.setTelefonnummer(request.getTelefonnummer());
133128
user.setTeam(request.getTeam());
134129
user.setPassword(passwordEncoder.encode(request.getPassword()));
130+
user.setEmailVerified(false); // User muss E-Mail bestätigen
135131

136132
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
137133
.orElseThrow(() -> new RuntimeException("Fehler: Rolle nicht gefunden."));
138134
user.getRoles().add(userRole);
139-
userRepository.save(user);
135+
User savedUser = userRepository.save(user);
140136

141-
emailService.sendWelcomeEmailWithCredentials(user.getEmail(), user.getName(), user.getEmail(), request.getPassword());
137+
// Erstelle Verifizierungs-Token und sende E-Mail
138+
try {
139+
userService.sendVerificationEmail(savedUser);
140+
log.info("Verification email sent to user: {}", savedUser.getEmail());
141+
} catch (Exception e) {
142+
log.error("Failed to send verification email to: {}", savedUser.getEmail(), e);
143+
// User wurde bereits gespeichert, daher geben wir trotzdem Erfolg zurück
144+
}
142145

143-
return new ResponseEntity<>("Benutzer erfolgreich registriert! Überprüfen Sie Ihre E-Mails für die Zugangsdaten.", HttpStatus.CREATED);
146+
return new ResponseEntity<>(
147+
"Benutzer erfolgreich registriert! Bitte überprüfe deine E-Mails und bestätige deine E-Mail-Adresse, um dich anmelden zu können.",
148+
HttpStatus.CREATED);
144149
}
145-
150+
146151
private String generateRandomPassword() {
147152
final String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+";
148153
SecureRandom random = new SecureRandom();
@@ -174,6 +179,14 @@ public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest request) {
174179

175180
User userDetails = (User) authentication.getPrincipal();
176181

182+
// Prüfe, ob E-Mail verifiziert ist
183+
if (!userDetails.isEmailVerified()) {
184+
log.warn("Login attempt with unverified email: {}", userDetails.getEmail());
185+
return new ResponseEntity<>(
186+
"Bitte verifiziere zuerst deine E-Mail-Adresse. Überprüfe deinen Posteingang für den Verifizierungslink.",
187+
HttpStatus.FORBIDDEN);
188+
}
189+
177190
String token = jwtUtil.generateToken(userDetails);
178191
RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails);
179192

@@ -310,4 +323,43 @@ public ResponseEntity<?> resetPassword(@Valid @RequestBody ResetPasswordRequest
310323
})
311324
.orElseThrow(() -> new ResourceNotFoundException("Invalid token"));
312325
}
326+
327+
/**
328+
* Verifiziert die E-Mail-Adresse eines Users mit dem Token
329+
*/
330+
@PostMapping("/verify-email")
331+
@Operation(summary = "E-Mail verifizieren", description = "Verifiziert die E-Mail-Adresse eines Users mit dem Verifizierungs-Token.")
332+
public ResponseEntity<?> verifyEmail(@RequestParam String token) {
333+
try {
334+
userService.verifyEmail(token);
335+
log.info("Email verification successful for token: {}", token);
336+
return ResponseEntity.ok("E-Mail-Adresse wurde erfolgreich verifiziert. Du kannst dich jetzt anmelden.");
337+
} catch (IllegalArgumentException e) {
338+
log.error("Email verification failed: {}", e.getMessage());
339+
return ResponseEntity.badRequest().body(e.getMessage());
340+
}
341+
}
342+
343+
/**
344+
* Sendet erneut eine Verifizierungs-E-Mail
345+
*/
346+
@PostMapping("/resend-verification")
347+
@Operation(summary = "Verifizierungs-E-Mail erneut senden", description = "Sendet erneut eine E-Mail mit einem Verifizierungslink.")
348+
public ResponseEntity<?> resendVerificationEmail(@RequestBody Map<String, String> request) {
349+
String email = request.get("email");
350+
351+
if (email == null || email.isEmpty()) {
352+
return ResponseEntity.badRequest().body("E-Mail-Adresse ist erforderlich.");
353+
}
354+
355+
try {
356+
userService.resendVerificationEmail(email);
357+
log.info("Verification email resent to: {}", email);
358+
return ResponseEntity
359+
.ok("Verifizierungs-E-Mail wurde erneut gesendet. Bitte überprüfe deinen Posteingang.");
360+
} catch (IllegalArgumentException e) {
361+
log.error("Failed to resend verification email: {}", e.getMessage());
362+
return ResponseEntity.badRequest().body(e.getMessage());
363+
}
364+
}
313365
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.example.springboot.model;
2+
3+
import jakarta.persistence.*;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.time.Instant;
9+
10+
/**
11+
* 📧 Email Verification Token Entity
12+
* Speichert Tokens für die E-Mail-Verifizierung neuer User.
13+
* Ein Token ist 24 Stunden gültig und kann nur einmal verwendet werden.
14+
*/
15+
@Getter
16+
@Setter
17+
@NoArgsConstructor
18+
@Entity
19+
@Table(name = "email_verification_tokens")
20+
public class EmailVerificationToken {
21+
22+
@Id
23+
@GeneratedValue(strategy = GenerationType.AUTO)
24+
private Long id;
25+
26+
@Column(nullable = false, unique = true)
27+
private String token;
28+
29+
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
30+
@JoinColumn(nullable = false, name = "user_id")
31+
private User user;
32+
33+
@Column(nullable = false)
34+
private Instant expiryDate;
35+
36+
@Column(nullable = false)
37+
private boolean used = false;
38+
39+
public EmailVerificationToken(User user, String token, Instant expiryDate) {
40+
this.user = user;
41+
this.token = token;
42+
this.expiryDate = expiryDate;
43+
this.used = false;
44+
}
45+
46+
public boolean isExpired() {
47+
return Instant.now().isAfter(this.expiryDate);
48+
}
49+
}

‎backend/src/main/java/org/example/springboot/model/User.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public class User implements UserDetails {
4949
@Column(name = "account_gesperrt_bis")
5050
private java.time.LocalDateTime accountGesperrtBis;
5151

52+
@Column(name = "is_email_verified", columnDefinition = "boolean default false")
53+
private boolean isEmailVerified = false;
54+
5255
@ManyToMany(fetch = FetchType.EAGER)
5356
@JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
5457
private Set<Role> roles = new HashSet<>();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.example.springboot.repository;
2+
3+
import org.example.springboot.model.EmailVerificationToken;
4+
import org.example.springboot.model.User;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.stereotype.Repository;
7+
8+
import java.util.Optional;
9+
10+
@Repository
11+
public interface EmailVerificationTokenRepository extends JpaRepository<EmailVerificationToken, Long> {
12+
13+
Optional<EmailVerificationToken> findByToken(String token);
14+
15+
Optional<EmailVerificationToken> findByUser(User user);
16+
17+
void deleteByUser(User user);
18+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package org.example.springboot.service.auth;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.example.springboot.model.EmailVerificationToken;
6+
import org.example.springboot.model.User;
7+
import org.example.springboot.repository.EmailVerificationTokenRepository;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
import java.time.Instant;
13+
import java.util.Optional;
14+
import java.util.UUID;
15+
16+
/**
17+
* 📧 Email Verification Token Service
18+
* Verwaltet die Erstellung, Validierung und Löschung von
19+
* E-Mail-Verifizierungs-Tokens.
20+
* Tokens sind 24 Stunden gültig und können nur einmal verwendet werden.
21+
*/
22+
@Slf4j
23+
@Service
24+
@RequiredArgsConstructor
25+
public class EmailVerificationTokenService {
26+
27+
private final EmailVerificationTokenRepository tokenRepository;
28+
29+
@Value("${email.verification.token.expiration.hours:24}")
30+
private long tokenExpirationHours;
31+
32+
/**
33+
* Erstellt einen neuen Verifizierungs-Token für einen User
34+
*/
35+
@Transactional
36+
public EmailVerificationToken createVerificationToken(User user) {
37+
// Lösche alte Tokens für diesen User
38+
tokenRepository.findByUser(user).ifPresent(tokenRepository::delete);
39+
40+
// Erstelle neuen Token
41+
String token = UUID.randomUUID().toString();
42+
Instant expiryDate = Instant.now().plusSeconds(tokenExpirationHours * 3600);
43+
44+
EmailVerificationToken verificationToken = new EmailVerificationToken(user, token, expiryDate);
45+
EmailVerificationToken saved = tokenRepository.save(verificationToken);
46+
47+
log.info("Email verification token created for user: {}", user.getUsername());
48+
return saved;
49+
}
50+
51+
/**
52+
* Validiert einen Token und gibt den zugehörigen User zurück
53+
*/
54+
public Optional<User> validateToken(String token) {
55+
Optional<EmailVerificationToken> tokenOptional = tokenRepository.findByToken(token);
56+
57+
if (tokenOptional.isEmpty()) {
58+
log.warn("Email verification token not found: {}", token);
59+
return Optional.empty();
60+
}
61+
62+
EmailVerificationToken verificationToken = tokenOptional.get();
63+
64+
if (verificationToken.isUsed()) {
65+
log.warn("Email verification token already used: {}", token);
66+
return Optional.empty();
67+
}
68+
69+
if (verificationToken.isExpired()) {
70+
log.warn("Email verification token expired: {}", token);
71+
return Optional.empty();
72+
}
73+
74+
return Optional.of(verificationToken.getUser());
75+
}
76+
77+
/**
78+
* Markiert einen Token als verwendet
79+
*/
80+
@Transactional
81+
public void markTokenAsUsed(String token) {
82+
tokenRepository.findByToken(token).ifPresent(verificationToken -> {
83+
verificationToken.setUsed(true);
84+
tokenRepository.save(verificationToken);
85+
log.info("Email verification token marked as used: {}", token);
86+
});
87+
}
88+
89+
/**
90+
* Löscht einen Token
91+
*/
92+
@Transactional
93+
public void deleteToken(String token) {
94+
tokenRepository.findByToken(token).ifPresent(verificationToken -> {
95+
tokenRepository.delete(verificationToken);
96+
log.info("Email verification token deleted: {}", token);
97+
});
98+
}
99+
100+
/**
101+
* Löscht alle Tokens für einen User
102+
*/
103+
@Transactional
104+
public void deleteTokensByUser(User user) {
105+
tokenRepository.findByUser(user).ifPresent(tokenRepository::delete);
106+
log.info("All email verification tokens deleted for user: {}", user.getUsername());
107+
}
108+
109+
/**
110+
* Prüft, ob ein User bereits einen gültigen Token hat
111+
*/
112+
public boolean hasValidToken(User user) {
113+
Optional<EmailVerificationToken> tokenOptional = tokenRepository.findByUser(user);
114+
115+
if (tokenOptional.isEmpty()) {
116+
return false;
117+
}
118+
119+
EmailVerificationToken token = tokenOptional.get();
120+
return !token.isUsed() && !token.isExpired();
121+
}
122+
}

0 commit comments

Comments
 (0)