diff --git a/src/main/java/app/mealsmadeeasy/api/GreetingController.java b/src/main/java/app/mealsmadeeasy/api/GreetingController.java index c8bc97f..961fd58 100644 --- a/src/main/java/app/mealsmadeeasy/api/GreetingController.java +++ b/src/main/java/app/mealsmadeeasy/api/GreetingController.java @@ -4,8 +4,8 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; -@Controller("/") -public class GreetingController { +@Controller +public final class GreetingController { @GetMapping("/greeting") @ResponseBody diff --git a/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java b/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java index b2478ad..0c27b5e 100644 --- a/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java +++ b/src/main/java/app/mealsmadeeasy/api/MealsMadeEasyApiApplication.java @@ -1,15 +1,12 @@ package app.mealsmadeeasy.api; -import io.jsonwebtoken.Jwts; +import app.mealsmadeeasy.api.user.UserService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; -import javax.crypto.SecretKey; +import java.util.Set; @SpringBootApplication public class MealsMadeEasyApiApplication { @@ -18,21 +15,17 @@ public class MealsMadeEasyApiApplication { SpringApplication.run(MealsMadeEasyApiApplication.class, args); } - @Bean - public SecretKey secretKey() { - return Jwts.SIG.HS256.key().build(); + private final UserService userService; + + public MealsMadeEasyApiApplication(UserService userService) { + this.userService = userService; } @Bean - public UserDetailsService userDetailsService() { - @SuppressWarnings("deprecation") - UserDetails testUser = User - .withDefaultPasswordEncoder() - .username("test") - .password("test") - .roles("USER") - .build(); - return new InMemoryUserDetailsManager(testUser); + public CommandLineRunner addTestUser() { + return args -> { + this.userService.createUser("test", "test@test.com", "test", Set.of()); + }; } } diff --git a/src/main/java/app/mealsmadeeasy/api/auth/AuthController.java b/src/main/java/app/mealsmadeeasy/api/auth/AuthController.java new file mode 100644 index 0000000..2d9be88 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/AuthController.java @@ -0,0 +1,44 @@ +package app.mealsmadeeasy.api.auth; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +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; + +@RestController +@RequestMapping("/auth") +public final class AuthController { + + private final AuthService authService; + + public AuthController(AuthService authService) { + this.authService = authService; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginBody loginBody, HttpServletResponse response) { + try { + final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword()); + final String serializedToken = loginDetails.getRefreshToken().getToken(); + final ResponseCookie refreshCookie = ResponseCookie.from("refresh-token", serializedToken) + .httpOnly(true) + .secure(true) + .maxAge(loginDetails.getRefreshToken().getLifetime()) + .build(); + final LoginView loginView = new LoginView( + loginDetails.getUsername(), loginDetails.getAccessToken().getToken() + ); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) + .body(loginView); + } catch (LoginException loginException) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/auth/AuthService.java b/src/main/java/app/mealsmadeeasy/api/auth/AuthService.java new file mode 100644 index 0000000..2c40370 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/AuthService.java @@ -0,0 +1,5 @@ +package app.mealsmadeeasy.api.auth; + +public interface AuthService { + LoginDetails login(String username, String password) throws LoginException; +} diff --git a/src/main/java/app/mealsmadeeasy/api/auth/AuthServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/auth/AuthServiceImpl.java new file mode 100644 index 0000000..89f37f3 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/AuthServiceImpl.java @@ -0,0 +1,36 @@ +package app.mealsmadeeasy.api.auth; + +import app.mealsmadeeasy.api.security.JwtService; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.stereotype.Service; + +@Service +public final class AuthServiceImpl implements AuthService { + + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + + public AuthServiceImpl(AuthenticationManager authenticationManager, JwtService jwtService) { + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + } + + @Override + public LoginDetails login(String username, String password) throws LoginException { + try { + this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( + username, + password + )); + return new LoginDetails( + username, + this.jwtService.generateAccessToken(username), + this.jwtService.generateRefreshToken(username) + ); + } catch (Exception e) { + throw new LoginException(e); + } + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/auth/LoginBody.java b/src/main/java/app/mealsmadeeasy/api/auth/LoginBody.java new file mode 100644 index 0000000..1cf91a2 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/LoginBody.java @@ -0,0 +1,24 @@ +package app.mealsmadeeasy.api.auth; + +public final class LoginBody { + + private String username; + private String password; + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/auth/LoginDetails.java b/src/main/java/app/mealsmadeeasy/api/auth/LoginDetails.java new file mode 100644 index 0000000..60f5139 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/LoginDetails.java @@ -0,0 +1,29 @@ +package app.mealsmadeeasy.api.auth; + +import app.mealsmadeeasy.api.security.AuthToken; + +public final class LoginDetails { + + private final String username; + private final AuthToken accessToken; + private final AuthToken refreshToken; + + public LoginDetails(String username, AuthToken accessToken, AuthToken refreshToken) { + this.username = username; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public String getUsername() { + return this.username; + } + + public AuthToken getAccessToken() { + return this.accessToken; + } + + public AuthToken getRefreshToken() { + return this.refreshToken; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/auth/LoginException.java b/src/main/java/app/mealsmadeeasy/api/auth/LoginException.java new file mode 100644 index 0000000..33578d4 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/LoginException.java @@ -0,0 +1,17 @@ +package app.mealsmadeeasy.api.auth; + +public final class LoginException extends Exception { + + public LoginException(String message) { + super(message); + } + + public LoginException(String message, Throwable cause) { + super(message, cause); + } + + public LoginException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/auth/LoginView.java b/src/main/java/app/mealsmadeeasy/api/auth/LoginView.java new file mode 100644 index 0000000..b517385 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/LoginView.java @@ -0,0 +1,21 @@ +package app.mealsmadeeasy.api.auth; + +public final class LoginView { + + private final String username; + private final String accessToken; + + public LoginView(String username, String accessToken) { + this.username = username; + this.accessToken = accessToken; + } + + public String getUsername() { + return this.username; + } + + public String getAccessToken() { + return this.accessToken; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/AuthToken.java b/src/main/java/app/mealsmadeeasy/api/security/AuthToken.java new file mode 100644 index 0000000..0a17a8a --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/AuthToken.java @@ -0,0 +1,21 @@ +package app.mealsmadeeasy.api.security; + +public final class AuthToken { + + private final String token; + private final long lifetime; + + public AuthToken(String token, long lifetime) { + this.token = token; + this.lifetime = lifetime; + } + + public String getToken() { + return this.token; + } + + public long getLifetime() { + return this.lifetime; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/JpaUserDetailsService.java b/src/main/java/app/mealsmadeeasy/api/security/JpaUserDetailsService.java new file mode 100644 index 0000000..4e8ef8b --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/JpaUserDetailsService.java @@ -0,0 +1,11 @@ +package app.mealsmadeeasy.api.security; + +import app.mealsmadeeasy.api.user.User; +import org.springframework.security.core.userdetails.UserDetailsService; + +public interface JpaUserDetailsService extends UserDetailsService { + User createUser(User user); + User updateUser(User user); + void deleteUser(String username); + void deleteUser(User user); +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/JpaUserDetailsServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/security/JpaUserDetailsServiceImpl.java new file mode 100644 index 0000000..d8b02f4 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/JpaUserDetailsServiceImpl.java @@ -0,0 +1,47 @@ +package app.mealsmadeeasy.api.security; + +import app.mealsmadeeasy.api.user.User; +import app.mealsmadeeasy.api.user.UserEntity; +import app.mealsmadeeasy.api.user.UserRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public final class JpaUserDetailsServiceImpl implements JpaUserDetailsService { + + private final UserRepository userRepository; + + public JpaUserDetailsServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public User createUser(User user) { + return this.userRepository.save((UserEntity) user); + } + + @Override + public User updateUser(User user) { + return this.userRepository.save((UserEntity) user); + } + + @Override + public void deleteUser(String username) { + final UserEntity user = this.userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("No such User with username: " + username)); + this.userRepository.delete(user); + } + + @Override + public void deleteUser(User user) { + this.userRepository.delete((UserEntity) user); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return this.userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("No such User with username: " + username)); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/JwtService.java b/src/main/java/app/mealsmadeeasy/api/security/JwtService.java index d16e49c..06e9d7e 100644 --- a/src/main/java/app/mealsmadeeasy/api/security/JwtService.java +++ b/src/main/java/app/mealsmadeeasy/api/security/JwtService.java @@ -1,5 +1,6 @@ package app.mealsmadeeasy.api.security; public interface JwtService { - String generateToken(String username); + AuthToken generateAccessToken(String username); + AuthToken generateRefreshToken(String username); } diff --git a/src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java index 7c68794..6b1cc2f 100644 --- a/src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java +++ b/src/main/java/app/mealsmadeeasy/api/security/JwtServiceImpl.java @@ -2,6 +2,7 @@ package app.mealsmadeeasy.api.security; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.jackson.io.JacksonSerializer; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -9,34 +10,52 @@ import org.springframework.stereotype.Service; import javax.crypto.SecretKey; import java.time.Instant; import java.util.Date; +import java.util.Map; @Service public final class JwtServiceImpl implements JwtService { - private final ObjectMapper objectMapper; - private final long tokenLifetime; + private final Serializer> serializer; + private final long accessTokenLifetime; + private final long refreshTokenLifetime; private final SecretKey secretKey; public JwtServiceImpl( ObjectMapper objectMapper, - @Value("${app.mealsmadeeasy.api.security.token-lifetime}") Long tokenLifetime, + @Value("${app.mealsmadeeasy.api.security.access-token-lifetime}") Long accessTokenLifetime, + @Value("${app.mealsmadeeasy.api.security.refresh-token-lifetime}") Long refreshTokenLifetime, SecretKey secretKey ) { - this.objectMapper = objectMapper; - this.tokenLifetime = tokenLifetime; + this.serializer = new JacksonSerializer<>(); + this.accessTokenLifetime = accessTokenLifetime; + this.refreshTokenLifetime = refreshTokenLifetime; this.secretKey = secretKey; } @Override - public String generateToken(String username) { + public AuthToken generateAccessToken(String username) { final Instant now = Instant.now(); - return Jwts.builder() + final String token = Jwts.builder() .subject(username) .issuedAt(Date.from(now)) - .expiration(Date.from(Instant.now().plusSeconds(this.tokenLifetime))) + .expiration(Date.from(now.plusSeconds(this.accessTokenLifetime))) .signWith(this.secretKey) - .json(new JacksonSerializer<>(this.objectMapper)) + .json(this.serializer) .compact(); + return new AuthToken(token, this.accessTokenLifetime); + } + + @Override + public AuthToken generateRefreshToken(String username) { + final Instant now = Instant.now(); + final String token = Jwts.builder() + .subject(username) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(this.refreshTokenLifetime))) + .signWith(this.secretKey) + .json(this.serializer) + .compact(); + return new AuthToken(token, this.refreshTokenLifetime); } } diff --git a/src/main/java/app/mealsmadeeasy/api/security/KeyConfiguration.java b/src/main/java/app/mealsmadeeasy/api/security/KeyConfiguration.java new file mode 100644 index 0000000..6b16c80 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/KeyConfiguration.java @@ -0,0 +1,17 @@ +package app.mealsmadeeasy.api.security; + +import io.jsonwebtoken.Jwts; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.crypto.SecretKey; + +@Configuration +public class KeyConfiguration { + + @Bean + public SecretKey secretKey() { + return Jwts.SIG.HS256.key().build(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java b/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java index 8c85e15..6478fc9 100644 --- a/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java +++ b/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java @@ -3,6 +3,9 @@ package app.mealsmadeeasy.api.security; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -10,6 +13,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -20,16 +25,16 @@ import javax.crypto.SecretKey; public class SecurityConfiguration { private final SecretKey secretKey; - private final UserDetailsService userDetailsService; + private final JpaUserDetailsService jpaUserDetailsService; - public SecurityConfiguration(SecretKey secretKey, UserDetailsService userDetailsService) { + public SecurityConfiguration(SecretKey secretKey, JpaUserDetailsService jpaUserDetailsService) { this.secretKey = secretKey; - this.userDetailsService = userDetailsService; + this.jpaUserDetailsService = jpaUserDetailsService; } @Bean public WebSecurityCustomizer webSecurityCustomizer() { - return web -> web.ignoring().requestMatchers("/greeting"); + return web -> web.ignoring().requestMatchers("/greeting", "/auth/login"); } @Bean @@ -46,10 +51,33 @@ public class SecurityConfiguration { }); }); httpSecurity.addFilterBefore( - new JwtFilter(this.secretKey, this.userDetailsService), + new JwtFilter(this.secretKey, this.jpaUserDetailsService), UsernamePasswordAuthenticationFilter.class ); return httpSecurity.build(); } + @Bean + public UserDetailsService userDetailsService() { + return this.jpaUserDetailsService; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(10); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + final var provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(this.userDetailsService()); + provider.setPasswordEncoder(this.passwordEncoder()); + return provider; + } + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager(this.daoAuthenticationProvider()); + } + } diff --git a/src/main/java/app/mealsmadeeasy/api/user/User.java b/src/main/java/app/mealsmadeeasy/api/user/User.java new file mode 100644 index 0000000..e8837a6 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/User.java @@ -0,0 +1,18 @@ +package app.mealsmadeeasy.api.user; + +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Set; + +public sealed interface User extends UserDetails permits UserEntity { + + Long getId(); + + String getEmail(); + void setEmail(String email); + + void addAuthority(UserGrantedAuthority userGrantedAuthority); + void addAuthorities(Set userGrantedAuthorities); + void removeAuthority(UserGrantedAuthority userGrantedAuthority); + +} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserEntity.java b/src/main/java/app/mealsmadeeasy/api/user/UserEntity.java new file mode 100644 index 0000000..475236f --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/UserEntity.java @@ -0,0 +1,144 @@ +package app.mealsmadeeasy.api.user; + +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +@Entity(name = "User") +public final class UserEntity implements User { + + public static UserEntity getDefaultDraft() { + final var user = new UserEntity(); + user.setEnabled(true); + user.setExpired(false); + user.setLocked(false); + user.setCredentialsExpired(false); + return user; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "userEntity") + private final Set authorities = new HashSet<>(); + + @Column(nullable = false) + private Boolean enabled; + + @Column(nullable = false) + private Boolean expired; + + @Column(nullable = false) + private Boolean locked; + + @Column(nullable = false) + private Boolean credentialsExpired; + + @Override + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getEmail() { + return this.email; + } + + @Override + public void setEmail(String email) { + this.email = email; + } + + @Override + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public void addAuthority(UserGrantedAuthority userGrantedAuthority) { + this.authorities.add((UserGrantedAuthorityEntity) userGrantedAuthority); + } + + @Override + public void addAuthorities(Set userGrantedAuthorities) { + userGrantedAuthorities.forEach(this::addAuthority); + } + + @Override + public void removeAuthority(UserGrantedAuthority userGrantedAuthority) { + this.authorities.remove((UserGrantedAuthorityEntity) userGrantedAuthority); + } + + @Override + public boolean isAccountNonExpired() { + return !this.expired; + } + + public void setExpired(Boolean expired) { + this.expired = expired; + } + + @Override + public boolean isAccountNonLocked() { + return !this.locked; + } + + public void setLocked(Boolean locked) { + this.locked = locked; + } + + @Override + public boolean isCredentialsNonExpired() { + return !this.credentialsExpired; + } + + public void setCredentialsExpired(Boolean credentialsExpired) { + this.credentialsExpired = credentialsExpired; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthority.java b/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthority.java new file mode 100644 index 0000000..7ab6852 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthority.java @@ -0,0 +1,5 @@ +package app.mealsmadeeasy.api.user; + +import org.springframework.security.core.GrantedAuthority; + +public interface UserGrantedAuthority extends GrantedAuthority {} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthorityEntity.java b/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthorityEntity.java new file mode 100644 index 0000000..acf5f39 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthorityEntity.java @@ -0,0 +1,24 @@ +package app.mealsmadeeasy.api.user; + +import jakarta.persistence.*; + +@Entity +public final class UserGrantedAuthorityEntity implements UserGrantedAuthority { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long id; + + private String authority; + + @ManyToOne + @JoinColumn(name = "user_entity_id") + private UserEntity userEntity; + + @Override + public String getAuthority() { + return this.authority; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthorityRepository.java b/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthorityRepository.java new file mode 100644 index 0000000..21a3ee0 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/UserGrantedAuthorityRepository.java @@ -0,0 +1,5 @@ +package app.mealsmadeeasy.api.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserGrantedAuthorityRepository extends JpaRepository {} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserRepository.java b/src/main/java/app/mealsmadeeasy/api/user/UserRepository.java new file mode 100644 index 0000000..a3a9d41 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/UserRepository.java @@ -0,0 +1,9 @@ +package app.mealsmadeeasy.api.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserService.java b/src/main/java/app/mealsmadeeasy/api/user/UserService.java new file mode 100644 index 0000000..dad8c41 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/UserService.java @@ -0,0 +1,10 @@ +package app.mealsmadeeasy.api.user; + +import java.util.Set; + +public interface UserService { + User createUser(String username, String email, String rawPassword, Set authorities); + User updateUser(User user); + void deleteUser(User user); + void deleteUser(String username); +} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/user/UserServiceImpl.java new file mode 100644 index 0000000..8134b3e --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/user/UserServiceImpl.java @@ -0,0 +1,50 @@ +package app.mealsmadeeasy.api.user; + +import app.mealsmadeeasy.api.security.JpaUserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Service +public final class UserServiceImpl implements UserService { + + private final JpaUserDetailsService jpaUserDetailsService; + private final PasswordEncoder passwordEncoder; + + public UserServiceImpl(JpaUserDetailsService jpaUserDetailsService, PasswordEncoder passwordEncoder) { + this.jpaUserDetailsService = jpaUserDetailsService; + this.passwordEncoder = passwordEncoder; + } + + @Override + public User createUser( + String username, + String email, + String rawPassword, + Set authorities + ) { + final UserEntity draft = UserEntity.getDefaultDraft(); + draft.setUsername(username); + draft.setEmail(email); + draft.setPassword(this.passwordEncoder.encode(rawPassword)); + draft.addAuthorities(authorities); + return this.jpaUserDetailsService.createUser(draft); + } + + @Override + public User updateUser(User user) { + return this.jpaUserDetailsService.updateUser(user); + } + + @Override + public void deleteUser(User user) { + this.jpaUserDetailsService.deleteUser(user); + } + + @Override + public void deleteUser(String username) { + this.jpaUserDetailsService.deleteUser(username); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 03c8309..dd957bf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,8 @@ spring.application.name=meals-made-easy-api -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=create-drop spring.datasource.url=jdbc:mysql://localhost:55001/meals_made_easy_api spring.datasource.username=meals-made-easy-api-user spring.datasource.password=devpass spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -app.mealsmadeeasy.api.security.token-lifetime=60 +app.mealsmadeeasy.api.security.access-token-lifetime=60 +app.mealsmadeeasy.api.security.refresh-token-lifetime=120