diff --git a/api/floaty-open-api.yml b/api/floaty-open-api.yml index 795323f..dc2a7f7 100644 --- a/api/floaty-open-api.yml +++ b/api/floaty-open-api.yml @@ -46,7 +46,6 @@ paths: application/json: schema: $ref: '#/components/schemas/User' - /flights/{userId}: get: tags: @@ -68,7 +67,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Flight' + type: array + items: + $ref: '#/components/schemas/Flight' /flights/{flightId}: delete: tags: @@ -270,7 +271,9 @@ components: User: type: object required: + - id - name + - emailVerified properties: id: type: string @@ -282,6 +285,10 @@ components: type: string example: 'Free Willy' description: 'The name of the user.' + emailVerified: + type: boolean + example: false + description: 'If the email has been verified by the user already or not.' Flight: type: object required: diff --git a/docker-compose.yml b/docker-compose.yml index 0556dac..8743fd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: - "5001:80" # Expose the web UI on localhost:5001 - "2525:25" # Expose the SMTP server on localhost:2525 floaty-backend: - image: matthaeusheer/floaty-backend:latest + image: matthaeusheer/floaty-backend:0.0.4 build: . ports: - "8080:8080" diff --git a/src/main/java/ch/floaty/domain/model/TimedToken.java b/src/main/java/ch/floaty/domain/model/TimedToken.java index ed6b8c5..11eb141 100644 --- a/src/main/java/ch/floaty/domain/model/TimedToken.java +++ b/src/main/java/ch/floaty/domain/model/TimedToken.java @@ -18,11 +18,13 @@ public abstract class TimedToken { @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String token; + private boolean revoked; public TimedToken(User user, Supplier tokenGenerator, int ttlMinutes) { this.user = user; this.token = tokenGenerator.get(); this.expirationTime = LocalDateTime.now().plusMinutes(ttlMinutes); + this.revoked = false; } @OneToOne diff --git a/src/main/java/ch/floaty/domain/service/AuthenticationExceptions.java b/src/main/java/ch/floaty/domain/service/AuthenticationExceptions.java index e0914c8..c2aa9cb 100644 --- a/src/main/java/ch/floaty/domain/service/AuthenticationExceptions.java +++ b/src/main/java/ch/floaty/domain/service/AuthenticationExceptions.java @@ -4,6 +4,7 @@ public class AuthenticationExceptions { public static class UserNotFoundException extends RuntimeException { public UserNotFoundException() { + super("Authentication failed."); } } diff --git a/src/main/java/ch/floaty/domain/service/AuthenticationService.java b/src/main/java/ch/floaty/domain/service/AuthenticationService.java index 96ad6ae..c983abd 100644 --- a/src/main/java/ch/floaty/domain/service/AuthenticationService.java +++ b/src/main/java/ch/floaty/domain/service/AuthenticationService.java @@ -10,6 +10,7 @@ import ch.floaty.domain.model.SessionToken; import ch.floaty.domain.model.User; import ch.floaty.infrastructure.EmailService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -20,6 +21,7 @@ @Service @Transactional +@Slf4j public class AuthenticationService implements IAuthenticationService{ @Value("${app.base.url}") @@ -111,6 +113,7 @@ public void verifyEmail(String inputEmailVerificationToken) throws EmailAlreadyV user.setEmailVerified(true); userRepository.save(user); } + log.info("Verified email '{}' for user '{}'", user.getEmail(), user.getUsername()); } @Override @@ -123,6 +126,7 @@ public Optional initiatePasswordReset(String email) { String eMailText = "Your password reset code: " + passwordResetToken.getToken(); String eMailSubject = "Floaty Password Reset"; emailService.sendSimpleEmail(user.getEmail(), eMailSubject, eMailText); + log.info("Initiated password reset for user '{}' and sent mail with reset token to '{}'", user.getUsername(), user.getEmail()); return Optional.of(passwordResetToken); } return Optional.empty(); @@ -132,25 +136,30 @@ public Optional initiatePasswordReset(String email) { public void resetPassword(String inputPasswordResetToken, String newPassword) throws UserNotFoundException { PasswordResetToken passwordResetToken = this.passwordResetTokenRepository.findByToken(inputPasswordResetToken) .orElseThrow(() -> new InvalidPasswordResetTokenException("Token not found.")); + User user = passwordResetToken.getUser(); if (passwordResetToken.hasExpired()) { + log.info("Password reset for user '{}': token expired.", user.getUsername()); throw new TokenExpiredException(); } if (passwordResetToken.isUsed()) { + log.info("Password reset for user '{}': token already used.", user.getUsername()); throw new InvalidPasswordResetTokenException("Token already used."); } validatePasswordStrength(newPassword); - User user = passwordResetToken.getUser(); + passwordResetToken.setUsed(true); sessionTokenRepository.findByUserId(user.getId()).forEach(sessionTokenRepository::delete); passwordResetTokenRepository.save(passwordResetToken); user.setHashedPassword(bCryptPasswordEncoder.encode(newPassword)); userRepository.save(user); + log.info("Did reset password for user '{}'", user.getUsername()); } @Override public void logout() { // TODO: invalidate any empty session tokens & potentially unset cookie or what do we do here? + log.warn("Logout not yet implemented!"); } @@ -159,6 +168,7 @@ private void validatePasswordStrength(String password) throws InsecurePasswordEx if (password == null || password.length() < 8) { throw new InsecurePasswordException(); } + } private void validateEmailForm(String email) throws EmailInvalidException { diff --git a/src/main/java/ch/floaty/infrastructure/AuthenticationController.java b/src/main/java/ch/floaty/infrastructure/AuthenticationController.java index 594dcb3..754acfb 100644 --- a/src/main/java/ch/floaty/infrastructure/AuthenticationController.java +++ b/src/main/java/ch/floaty/infrastructure/AuthenticationController.java @@ -1,5 +1,6 @@ package ch.floaty.infrastructure; +import ch.floaty.domain.repository.IUserRepository; import ch.floaty.domain.service.AuthenticationExceptions.UserNotFoundException; import ch.floaty.domain.service.AuthenticationExceptions.WrongPasswordException; import ch.floaty.domain.service.AuthenticationService; @@ -9,6 +10,7 @@ import ch.floaty.generated.RegisterRequestDto; import ch.floaty.generated.ResetPasswordRequestDto; import ch.floaty.generated.UserDto; +import lombok.extern.slf4j.Slf4j; import org.modelmapper.ModelMapper; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -23,13 +25,16 @@ import static org.springframework.http.HttpStatus.UNAUTHORIZED; @RestController +@Slf4j public class AuthenticationController { AuthenticationService authenticationService; + IUserRepository userRepository; ModelMapper modelMapper = new ModelMapper(); - public AuthenticationController(AuthenticationService authenticationService, EmailService emailService) { + public AuthenticationController(AuthenticationService authenticationService, IUserRepository userRepository) { this.authenticationService = authenticationService; + this.userRepository = userRepository; } @PostMapping("/auth/register") @@ -37,22 +42,26 @@ public ResponseEntity register(@RequestBody RegisterRequestDto register User newUser = authenticationService.register(registerRequestDto.getUsername(), registerRequestDto.getEmail(), registerRequestDto.getPassword()); URI location = URI.create("/users/" + newUser.getId()); UserDto responseUserDto = modelMapper.map(newUser, UserDto.class); + log.info("Registered user '{}' and sent email verification link to {}.", registerRequestDto.getUsername(), registerRequestDto.getEmail()); return ResponseEntity.created(location).body(responseUserDto); } @PostMapping("/auth/login") - public ResponseEntity login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) { + public ResponseEntity login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) { SessionToken sessionToken; try { sessionToken = authenticationService.login(loginRequestDto.getName(), loginRequestDto.getPassword()); } catch (UserNotFoundException | WrongPasswordException exception) { + log.info("Login attempt failed for '{}'. Reason: {} {}", loginRequestDto.getName(), exception.getClass().getName(), exception.getMessage()); return ResponseEntity.status(UNAUTHORIZED).body(null); } Cookie cookie = new Cookie("sessionToken", sessionToken.getToken()); cookie.setHttpOnly(true); cookie.setPath("/"); // all endpoints shall return new session cookie response.addCookie(cookie); - return ResponseEntity.ok().build(); + UserDto responseUserDto = modelMapper.map(this.userRepository.findByName(loginRequestDto.getName()), UserDto.class); + log.info("User '{}' logged in successfully.", loginRequestDto.getName()); + return ResponseEntity.ok().body(responseUserDto); } @PostMapping("/auth/verify-email/{token}") diff --git a/src/main/java/ch/floaty/infrastructure/SessionTokenFilter.java b/src/main/java/ch/floaty/infrastructure/SessionTokenFilter.java index a34005c..65070b5 100644 --- a/src/main/java/ch/floaty/infrastructure/SessionTokenFilter.java +++ b/src/main/java/ch/floaty/infrastructure/SessionTokenFilter.java @@ -40,7 +40,7 @@ protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull Ht } if (request.getRequestURI().startsWith("/auth/")) { - logger.info("Bypassing security filter for URI: " + request.getRequestURI()); + logger.debug("Bypassing security filter for URI: " + request.getRequestURI()); chain.doFilter(request, response); return; } diff --git a/src/main/java/ch/floaty/infrastructure/UserSecurity.java b/src/main/java/ch/floaty/infrastructure/UserSecurity.java index 7e0d272..2bc5453 100644 --- a/src/main/java/ch/floaty/infrastructure/UserSecurity.java +++ b/src/main/java/ch/floaty/infrastructure/UserSecurity.java @@ -1,11 +1,13 @@ package ch.floaty.infrastructure; import ch.floaty.domain.model.User; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @Component +@Slf4j public class UserSecurity { public boolean hasUserIdOrAdmin(Long userId) { @@ -19,6 +21,10 @@ public boolean hasUserIdOrAdmin(Long userId) { boolean isAdmin = user.getAuthorities().stream() .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ADMIN")); boolean hasUserId = user.getId().equals(userId); - return isAdmin || hasUserId; + boolean hasUserIdOrIsAdmin = isAdmin || hasUserId; + if (!hasUserIdOrIsAdmin) { + log.info("Unauthorized request: hasUserIdOrIsAdmin is false."); + } + return hasUserIdOrIsAdmin; } } diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..90042bd --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,47 @@ + + + + + + + + + %white(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable + + + + + + ${LOGS}/spring-boot-logger.log + + %d %p %C{1} [%t] %m%n + + + + + ${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log + + + 10MB + + + + + + + + + + + + + + + + +