diff --git a/src/main/java/backendlab/team4you/Team4youApplication.java b/src/main/java/backendlab/team4you/Team4youApplication.java index ad5d84e..6f9009f 100644 --- a/src/main/java/backendlab/team4you/Team4youApplication.java +++ b/src/main/java/backendlab/team4you/Team4youApplication.java @@ -1,7 +1,14 @@ package backendlab.team4you; +import backendlab.team4you.user.UserRepository; +import backendlab.team4you.user.UserEntity; +import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.webauthn.api.Bytes; @SpringBootApplication public class Team4youApplication { @@ -10,4 +17,24 @@ public static void main(String[] args) { SpringApplication.run(Team4youApplication.class, args); } + @Bean + @Profile("dev") + ApplicationRunner init(UserRepository repository, BCryptPasswordEncoder encoder) { + return args -> { + if (repository.count() == 0) { + + UserEntity devUser = new UserEntity( + Bytes.fromBase64("01"), + "dev", // name (username) + "Developer" // displayName + ); + + devUser.setPasswordHash(encoder.encode("123456")); + devUser.setRole("USER"); + devUser.setEmail("dev@gmail.com"); + + repository.save(devUser); + } + }; + } } diff --git a/src/main/java/backendlab/team4you/config/CustomAuthenticationSuccessHandler.java b/src/main/java/backendlab/team4you/config/CustomAuthenticationSuccessHandler.java index ef03e35..9827d6c 100644 --- a/src/main/java/backendlab/team4you/config/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/backendlab/team4you/config/CustomAuthenticationSuccessHandler.java @@ -32,7 +32,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, String username = authentication.getName(); - var userEntity = userService.findByEmail(username); + var userEntity = userService.findByName(username); if (userEntity != null){ var credentials = userCredentialRepository.findByUserId(userEntity.getId()); diff --git a/src/main/java/backendlab/team4you/config/SecurityConfig.java b/src/main/java/backendlab/team4you/config/SecurityConfig.java index 6d50f81..7a41a54 100644 --- a/src/main/java/backendlab/team4you/config/SecurityConfig.java +++ b/src/main/java/backendlab/team4you/config/SecurityConfig.java @@ -68,13 +68,13 @@ UserCredentialRepository userCredentialRepository(JdbcOperations jdbc) { @Bean public UserDetailsService userDetailsService(UserService userService){ return username -> { - UserEntity user = userService.findByEmail(username); + UserEntity user = userService.findByName(username); if (user == null) { throw new UsernameNotFoundException("User not found: " + username); } return User.builder() - .username(user.getEmail()) + .username(user.getName()) .password(user.getPasswordHash()) .roles(user.getRole()) .accountLocked(false) diff --git a/src/main/java/backendlab/team4you/controller/RegistrationController.java b/src/main/java/backendlab/team4you/controller/RegistrationController.java index ea7382e..2cb1c11 100644 --- a/src/main/java/backendlab/team4you/controller/RegistrationController.java +++ b/src/main/java/backendlab/team4you/controller/RegistrationController.java @@ -25,7 +25,7 @@ public RegistrationController(UserService userService){ @GetMapping("/register") public String showRegistrationForm(Model model) { - model.addAttribute("user", new UserRegistrationDTO("", "", "", "", "", "")); + model.addAttribute("user", new UserRegistrationDTO("","", "", "", "", "", "")); return "register"; } @@ -68,8 +68,8 @@ public String loginPage(@RequestParam(value = "registered", required = false) St public String welcome(Model model, Principal principal) { if (principal == null) return "redirect:/login"; // Säkerhetskoll - String email = principal.getName(); - UserEntity user = userService.findByEmail(email); + String name = principal.getName(); + UserEntity user = userService.findByName(name); if (user == null) { diff --git a/src/main/java/backendlab/team4you/controller/SignupController.java b/src/main/java/backendlab/team4you/controller/SignupController.java index 4b46fb6..02e4187 100644 --- a/src/main/java/backendlab/team4you/controller/SignupController.java +++ b/src/main/java/backendlab/team4you/controller/SignupController.java @@ -2,10 +2,8 @@ import backendlab.team4you.user.UserEntity; import backendlab.team4you.user.UserService; -import backendlab.team4you.user.UserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -13,7 +11,6 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; -import org.springframework.security.web.webauthn.api.Bytes; import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -21,16 +18,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.server.ResponseStatusException; -import java.security.SecureRandom; import java.util.List; @Controller public class SignupController { private final PublicKeyCredentialUserEntityRepository users; - private final SecureRandom random = new SecureRandom(); private final UserService userService; public SignupController(PublicKeyCredentialUserEntityRepository users, @@ -58,30 +52,16 @@ String signup(org.springframework.security.web.csrf.CsrfToken token, Model model @ResponseBody public void signup(@RequestBody SignupRequest req, HttpServletRequest request, HttpServletResponse response) { - if (req.username == null || req.username.isBlank()) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username is required"); - } - - if (userService.findByEmail(req.username) != null) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "Username already exists"); - } - - byte[] idBytes = new byte[32]; - random.nextBytes(idBytes); - - UserEntity userEntity = new UserEntity( - new Bytes(idBytes), - req.username, - req.displayName + UserEntity userEntity = userService.registerWebAuthnUser( + req.getUsername(), + req.getDisplayName(), + req.getEmail(), + req.getFirstName(), + req.getLastName() ); - String assignedRole = req.getUsername().endsWith("@team4you.com") ? "ADMIN" : "USER"; - userEntity.setRole(assignedRole); - - users.save(userEntity); - Authentication auth = new UsernamePasswordAuthenticationToken( - userEntity.getName(), null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + userEntity.getName(), null, List.of(new SimpleGrantedAuthority("ROLE_" + userEntity.getRole()))); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(auth); @@ -93,6 +73,9 @@ public void signup(@RequestBody SignupRequest req, HttpServletRequest request, H public static class SignupRequest { private String username; private String displayName; + private String email; + private String firstName; + private String lastName; public SignupRequest() { } @@ -112,6 +95,30 @@ public String getDisplayName() { public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } } diff --git a/src/main/java/backendlab/team4you/dto/UserRegistrationDTO.java b/src/main/java/backendlab/team4you/dto/UserRegistrationDTO.java index e89a711..2fd259e 100644 --- a/src/main/java/backendlab/team4you/dto/UserRegistrationDTO.java +++ b/src/main/java/backendlab/team4you/dto/UserRegistrationDTO.java @@ -2,7 +2,9 @@ import java.time.LocalDateTime; -public record UserRegistrationDTO( String firstName, +public record UserRegistrationDTO( + String name, + String firstName, String lastName, String email, String phoneNumber, diff --git a/src/main/java/backendlab/team4you/mapper/UserMapper.java b/src/main/java/backendlab/team4you/mapper/UserMapper.java index 8971c96..3bcd862 100644 --- a/src/main/java/backendlab/team4you/mapper/UserMapper.java +++ b/src/main/java/backendlab/team4you/mapper/UserMapper.java @@ -13,6 +13,7 @@ public static UserEntity toEntity(UserRegistrationDTO dto) { if (dto == null) return null; UserEntity entity = new UserEntity(); + entity.setName(dto.name()); entity.setFirstName(dto.firstName()); entity.setLastName(dto.lastName()); entity.setEmail(dto.email()); @@ -36,4 +37,3 @@ public static UserDTO toDto(UserEntity entity) { } - diff --git a/src/main/java/backendlab/team4you/user/UserEntity.java b/src/main/java/backendlab/team4you/user/UserEntity.java index 55f1e5b..44b1dc8 100644 --- a/src/main/java/backendlab/team4you/user/UserEntity.java +++ b/src/main/java/backendlab/team4you/user/UserEntity.java @@ -4,6 +4,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.jspecify.annotations.Nullable; import org.springframework.security.web.webauthn.api.Bytes; import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; @@ -18,9 +19,15 @@ public class UserEntity implements PublicKeyCredentialUserEntity { private String id; @Column(name = "name", nullable = false, unique = true) - private String email; + private String name; @Column(name = "display_name") + private String displayName; + + @Column(name = "email") + private String email; + + @Column(name = "first_name") private String firstName; @Column(name = "last_name") @@ -41,10 +48,10 @@ public class UserEntity implements PublicKeyCredentialUserEntity { public UserEntity() { } - public UserEntity(Bytes id, String email, String firstName) { + public UserEntity(Bytes id, String name, String displayName) { this.id = id != null ? id.toBase64UrlString() : null; - this.email = email; - this.firstName = firstName; + this.name = name; + this.displayName = displayName; } @Override @@ -58,14 +65,21 @@ public void setId(Bytes id) { @Override public String getName() { - return this.email; + return name; + } + + public void setName(String name) { + this.name = name; } @Override - public String getDisplayName() { - return (this.firstName != null ? this.firstName : "") + " " + (this.lastName != null ? this.lastName : ""); + public @Nullable String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; } - public LocalDateTime getCreatedAt() { return createdAt; diff --git a/src/main/java/backendlab/team4you/user/UserRepository.java b/src/main/java/backendlab/team4you/user/UserRepository.java index a01d6f4..536ff8a 100644 --- a/src/main/java/backendlab/team4you/user/UserRepository.java +++ b/src/main/java/backendlab/team4you/user/UserRepository.java @@ -1,10 +1,14 @@ package backendlab.team4you.user; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; +@Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); -} + Optional findByName(String name); +} diff --git a/src/main/java/backendlab/team4you/user/UserService.java b/src/main/java/backendlab/team4you/user/UserService.java index 5288bb3..aaf6d69 100644 --- a/src/main/java/backendlab/team4you/user/UserService.java +++ b/src/main/java/backendlab/team4you/user/UserService.java @@ -4,10 +4,15 @@ import backendlab.team4you.exceptions.DuplicateEmailException; import backendlab.team4you.exceptions.UserNotFoundException; import jakarta.transaction.Transactional; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.webauthn.api.Bytes; import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import java.security.SecureRandom; import java.util.List; @@ -17,6 +22,7 @@ public class UserService { UserRepository userRepository; private final BCryptPasswordEncoder passwordEncoder; + private final SecureRandom random = new SecureRandom(); public UserService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder){ @@ -55,14 +61,24 @@ public UserEntity update(UserEntity userEntity){ public void registerUser(UserRegistrationDTO dto) { - if (userRepository.findByEmail(dto.email()).isPresent()) { - throw new RuntimeException("E-posten är redan tagen"); + if (dto.name() == null || dto.name().isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"Username is required"); + } + if (userRepository.findByName(dto.name().trim()).isPresent()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST ,"Username already exists"); + } + + String cleanEmail = dto.email() != null ? dto.email().trim() : null; + + if (cleanEmail != null && userRepository.findByEmail(cleanEmail).isPresent()) { + throw new DuplicateEmailException("E-posten är redan tagen"); } UserEntity user = new UserEntity(); + user.setName(dto.name().trim()); user.setFirstName(dto.firstName()); user.setLastName(dto.lastName()); - user.setEmail(dto.email()); + user.setEmail(cleanEmail); user.setPhoneNumber(dto.phoneNumber()); @@ -73,10 +89,54 @@ public void registerUser(UserRegistrationDTO dto) { userRepository.save(user); } + public UserEntity registerWebAuthnUser(String username, String displayName, String email, String firstName, String lastName){ + + if (username == null || username.isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username is required"); + } + if (email == null || email.isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email is required"); + } + String cleanName = username.trim(); + String cleanEmail = email.trim(); + + if(userRepository.findByName(cleanName).isPresent()) + throw new ResponseStatusException(HttpStatus.CONFLICT, "Användarnamnet är redan taget"); + if(userRepository.findByEmail(cleanEmail).isPresent()) + throw new ResponseStatusException(HttpStatus.CONFLICT, "E-posten är redan tagen"); + + byte[] idBytes = new byte[32]; + random.nextBytes(idBytes); + + UserEntity userEntity = new UserEntity( + new Bytes(idBytes), + cleanName, + displayName + ); + + userEntity.setEmail(cleanEmail); + userEntity.setFirstName(firstName); + userEntity.setLastName(lastName); + + //Every user that register themselves will automatically get the role USER assigned + String assignedRole = "USER"; + userEntity.setRole(assignedRole); + + try { + return userRepository.save(userEntity); + } catch (DataIntegrityViolationException e) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "Username or email already taken"); + } + } + public UserEntity findByEmail(String email) { return userRepository.findByEmail(email).orElse(null); } + public UserEntity findByName(String name){ + return userRepository.findByName(name.trim()).orElse(null); + } + @Transactional public void deleteUser(String id) { if (!userRepository.existsById(id)) { diff --git a/src/main/java/backendlab/team4you/webauthn/WebAuthnCredential.java b/src/main/java/backendlab/team4you/webauthn/WebAuthnCredential.java index 6dccd34..6ef9d14 100644 --- a/src/main/java/backendlab/team4you/webauthn/WebAuthnCredential.java +++ b/src/main/java/backendlab/team4you/webauthn/WebAuthnCredential.java @@ -18,10 +18,10 @@ public class WebAuthnCredential implements CredentialRecord { @Id - @Column(name = "id", length = 512) + @Column(name = "credential_id", length = 512) private String credentialId; - @Column(name = "user_entity_id", length = 255) + @Column(name = "user_entity_user_id", length = 255) private String userEntityUserId; @Column(name = "public_key", columnDefinition = "bytea") diff --git a/src/main/resources/db/migration/V9__add_column_email_and_firstname.sql b/src/main/resources/db/migration/V9__add_column_email_and_firstname.sql new file mode 100644 index 0000000..6a50ee2 --- /dev/null +++ b/src/main/resources/db/migration/V9__add_column_email_and_firstname.sql @@ -0,0 +1,10 @@ +ALTER TABLE user_credentials RENAME COLUMN id TO credential_id; +ALTER TABLE user_credentials RENAME COLUMN user_entity_id TO user_entity_user_id; + +ALTER TABLE user_entities + ADD COLUMN IF NOT EXISTS email VARCHAR(255), + ADD COLUMN IF NOT EXISTS first_name VARCHAR(255); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_user_entities_email + ON user_entities (email) + WHERE email IS NOT NULL; diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index cf7a4f1..a693ffe 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -10,8 +10,8 @@

Logga in

- - + + @@ -27,4 +27,4 @@

Logga in

- \ No newline at end of file +