diff --git a/pom.xml b/pom.xml index f4f7a06..7e26128 100644 --- a/pom.xml +++ b/pom.xml @@ -1,113 +1,126 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.0 - - - com.backend.user.service - anyame-backend - 0.0.1-SNAPSHOT - anyame-backend - User service for anyame - - - - - - - - - - - - - - - 21 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + com.backend.user.service + anyame-backend + 0.0.1-SNAPSHOT + anyame-backend + User service for anyame + + + + + + + + + + + + + + + 21 - 0.12.6 - 4.0.0 - 1.6.6 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.security - spring-security-oauth2-authorization-server - + 0.12.6 + 4.0.0 + 1.6.6 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.security + spring-security-oauth2-authorization-server + - - io.jsonwebtoken - jjwt-api - ${jjwt.version} - - - io.jsonwebtoken - jjwt-impl - ${jjwt.version} - runtime - - - io.jsonwebtoken - jjwt-jackson - ${jjwt.version} - runtime - - - me.paulschwarz - spring-dotenv - ${spring-dotenv.version} - - - org.passay - passay - ${passay.version} - - - org.postgresql - postgresql - runtime - + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + - - org.springframework.boot - spring-boot-starter-test - test - + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + me.paulschwarz + spring-dotenv + ${spring-dotenv.version} + + + org.passay + passay + ${passay.version} + + + org.postgresql + postgresql + runtime + - - org.springframework.security - spring-security-test - test - - + + org.springframework.boot + spring-boot-starter-test + test + - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + org.springframework.security + spring-security-test + test + + - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/RoleDataInitializer.java b/src/main/java/com/backend/user/service/anyame/RoleDataInitializer.java new file mode 100644 index 0000000..98f3b33 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/RoleDataInitializer.java @@ -0,0 +1,101 @@ +package com.backend.user.service.anyame; + +import com.backend.user.service.anyame.entity.Permission; +import com.backend.user.service.anyame.entity.Role; +import com.backend.user.service.anyame.repository.PermissionRepository; +import com.backend.user.service.anyame.repository.RoleRepository; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +public class RoleDataInitializer implements ApplicationRunner { + private final RoleRepository roleRepository; + private final PermissionRepository permissionRepository; + + public RoleDataInitializer(RoleRepository roleRepository, PermissionRepository permissionRepository) { + this.roleRepository = roleRepository; + this.permissionRepository = permissionRepository; + } + + @Override + public void run(ApplicationArguments args) { + if (permissionRepository.count() == 0) { + initializePermissions(); + initializeRoles(); + } + } + + private void initializePermissions() { + savePermission("CREATE_ANIME"); + savePermission("EDIT_ANIME"); + savePermission("DELETE_ANIME"); + savePermission("APPROVE_ANIME"); + + savePermission("VIEW_USERS"); + savePermission("EDIT_USER"); + savePermission("BAN_USER"); + savePermission("DELETE_USER"); + + savePermission("MODERATE_COMMENTS"); + savePermission("MODERATE_REVIEWS"); + savePermission("HANDLE_REPORTS"); + savePermission("BLACKLIST_CONTENT"); + + savePermission("MANAGE_ROLES"); + savePermission("VIEW_ANALYTICS"); + savePermission("SYSTEM_CONFIG"); + savePermission("BACKUP_DATA"); + + savePermission("VIEW_PREMIUM_CONTENT"); + savePermission("DOWNLOAD_CONTENT"); + } + + private void initializeRoles() { + Role userRole = saveRole("USER"); + Role moderatorRole = saveRole("MODERATOR"); + Role adminRole = saveRole("ADMIN"); + + moderatorRole.getParentRoles().add(userRole); + moderatorRole.getPermissions().addAll(Arrays.asList( + findPermission("MODERATE_COMMENTS"), + findPermission("MODERATE_REVIEWS"), + findPermission("HANDLE_REPORTS"), + findPermission("EDIT_ANIME"), + findPermission("APPROVE_ANIME"), + findPermission("VIEW_USERS") + )); + + adminRole.getParentRoles().add(moderatorRole); + adminRole.getPermissions().addAll(Arrays.asList( + findPermission("DELETE_ANIME"), + findPermission("EDIT_USER"), + findPermission("BAN_USER"), + findPermission("DELETE_USER"), + findPermission("MANAGE_ROLES"), + findPermission("VIEW_ANALYTICS"), + findPermission("SYSTEM_CONFIG"), + findPermission("BACKUP_DATA"), + findPermission("BLACKLIST_CONTENT") + )); + + roleRepository.saveAll(Arrays.asList(moderatorRole, adminRole)); + } + + private Permission savePermission(String name) { + Permission permission = new Permission(name); + return permissionRepository.save(permission); + } + + private Role saveRole(String name) { + Role role = new Role(name); + return roleRepository.save(role); + } + + private Permission findPermission(String name) { + return permissionRepository.findByName(name) + .orElseThrow(() -> new RuntimeException("Permission not found: " + name)); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/component/AuthorizedUserMapper.java b/src/main/java/com/backend/user/service/anyame/component/AuthorizedUserMapper.java new file mode 100644 index 0000000..1068924 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/component/AuthorizedUserMapper.java @@ -0,0 +1,43 @@ +package com.backend.user.service.anyame.component; + +import com.backend.user.service.anyame.entity.User; +import com.backend.user.service.anyame.model.AuthorizedUser; +import com.backend.user.service.anyame.service.UserAuthorityService; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +@Component +public class AuthorizedUserMapper { + private final UserAuthorityService authorityService; + + public AuthorizedUserMapper(UserAuthorityService authorityService) { + this.authorityService = authorityService; + } + + public AuthorizedUser mapFormLogin(User userEntity) { + Set authorities = authorityService.getUserAuthorities(userEntity); + return new AuthorizedUser.Builder(userEntity.getEmail(), userEntity.getPassword(), Collections.emptyList()) + .id(userEntity.getId()) + .username(userEntity.getName()) + .active(userEntity.isActive()) + .authorities(authorities) + .accountNonExpired(true) + .credentialsNonExpired(true) + .accountNonLocked(true) + .build(); + } + + public AuthorizedUser mapOAuth2User(User userEntity, Map attributes) { + Set authorities = authorityService.getUserAuthorities(userEntity); + return new AuthorizedUser.Builder(userEntity.getEmail(), "", Collections.emptyList()) + .id(userEntity.getId()) + .username(userEntity.getName()) + .active(userEntity.isActive()) + .authorities(authorities) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java b/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java index 23cbf0d..60549a8 100644 --- a/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java +++ b/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java @@ -1,22 +1,35 @@ package com.backend.user.service.anyame.config; +import com.backend.user.service.anyame.service.CustomOAuth2UserService; +import com.backend.user.service.anyame.service.CustomUserDetailsService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -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 org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfigurationSource; import static org.springframework.security.config.Customizer.withDefaults; @Configuration(proxyBeanMethods = false) +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { + private final CustomUserDetailsService userDetailsService; + private final CustomOAuth2UserService oauth2UserService; + + public SecurityConfig(CustomUserDetailsService userDetailsService, + CustomOAuth2UserService oauth2UserService) { + this.userDetailsService = userDetailsService; + this.oauth2UserService = oauth2UserService; + } + @Bean @Order(2) public SecurityFilterChain filterChain(HttpSecurity http, @Qualifier("corsConfigurationSource") CorsConfigurationSource configurationSource) throws Exception { @@ -26,17 +39,17 @@ public class SecurityConfig { c.anyRequest().authenticated() ) .formLogin(withDefaults()) + .oauth2Login(c -> + c.userInfoEndpoint(userInfo -> userInfo + .userService(oauth2UserService) + ) + ) + .userDetailsService(userDetailsService) .build(); } @Bean - public UserDetailsService users() { - UserDetails user = User.builder() - .username("admin") - .password("{noop}password") - .roles("USER") - .build(); - return new InMemoryUserDetailsManager(user); + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); } - } \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/configurer/SocialConfigurer.java b/src/main/java/com/backend/user/service/anyame/configurer/SocialConfigurer.java new file mode 100644 index 0000000..0e21c08 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/configurer/SocialConfigurer.java @@ -0,0 +1,25 @@ +package com.backend.user.service.anyame.configurer; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; + +public class SocialConfigurer extends AbstractHttpConfigurer { + private AuthenticationFailureHandler failureHandler; + private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + + + @Override + public void init(HttpSecurity http) throws Exception { + http.oauth2Login(c -> { + if (this.successHandler != null) { + c.successHandler(this.successHandler); + } + if (this.failureHandler != null) { + c.failureHandler(this.failureHandler); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/controller/HelloController.java b/src/main/java/com/backend/user/service/anyame/controller/HelloController.java index 8b5ffaa..1943684 100644 --- a/src/main/java/com/backend/user/service/anyame/controller/HelloController.java +++ b/src/main/java/com/backend/user/service/anyame/controller/HelloController.java @@ -1,5 +1,7 @@ package com.backend.user.service.anyame.controller; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -11,4 +13,11 @@ public class HelloController { public String hello() { return "Hello World"; } -} + + @GetMapping("/principalhello") + public String principalhello() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication.getName(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/entity/Permission.java b/src/main/java/com/backend/user/service/anyame/entity/Permission.java new file mode 100644 index 0000000..e5d56e1 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/entity/Permission.java @@ -0,0 +1,28 @@ +package com.backend.user.service.anyame.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "permissions") +public class Permission { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(unique = true, nullable = false) + private String name; + + protected Permission() { + } + + public Permission(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/entity/Role.java b/src/main/java/com/backend/user/service/anyame/entity/Role.java new file mode 100644 index 0000000..d9c5b55 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/entity/Role.java @@ -0,0 +1,64 @@ +package com.backend.user.service.anyame.entity; + +import jakarta.persistence.*; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "roles") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(unique = true, nullable = false) + private String name; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "role_hierarchy", + joinColumns = @JoinColumn(name = "child_role_id"), + inverseJoinColumns = @JoinColumn(name = "parent_role_id") + ) + private Set parentRoles = new HashSet<>(); + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "role_permissions", + joinColumns = @JoinColumn(name = "role_id"), + inverseJoinColumns = @JoinColumn(name = "permission_id") + ) + private Set permissions = new HashSet<>(); + + protected Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Set getParentRoles() { + return parentRoles; + } + + public Set getPermissions() { + return permissions; + } + + public Set getAllPermissions() { + Set allPermissions = new HashSet<>(permissions); + + for (Role parentRole : parentRoles) { + allPermissions.addAll(parentRole.getAllPermissions()); + } + + return allPermissions; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/entity/User.java b/src/main/java/com/backend/user/service/anyame/entity/User.java new file mode 100644 index 0000000..9843379 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/entity/User.java @@ -0,0 +1,88 @@ +package com.backend.user.service.anyame.entity; + +import jakarta.persistence.*; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(unique = true, nullable = false) + private String name; + @Column(unique = true) + private String email; + private String password; + private boolean active; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + private Set roles = new HashSet<>(); + + private String provider; + private String providerId; + + protected User() { + } + + public User(String name, String email, String provider, String providerId) { + this.name = name; + this.email = email; + this.provider = provider; + this.providerId = providerId; + } + + public Set getRoles() { + return roles; + } + + public Set getAllPermissions() { + Set allPermissions = new HashSet<>(); + + for (Role role : roles) { + allPermissions.addAll(role.getAllPermissions()); + } + + return allPermissions; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public String getProvider() { + return provider; + } + + public String getProviderId() { + return providerId; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/model/AuthorizedUser.java b/src/main/java/com/backend/user/service/anyame/model/AuthorizedUser.java new file mode 100644 index 0000000..1ef8547 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/model/AuthorizedUser.java @@ -0,0 +1,136 @@ +package com.backend.user.service.anyame.model; + +import com.backend.user.service.anyame.entity.Role; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class AuthorizedUser extends User implements OAuth2User { + private Long id; + private String email; + private boolean active; + private Set roles; + private Map attributes; + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public boolean isActive() { + return active; + } + + private AuthorizedUser(Builder builder) { + super( + builder.username, + builder.password, + builder.enabled, + builder.accountNonExpired, + builder.credentialsNonExpired, + builder.accountNonLocked, + builder.authorities + ); + this.id = builder.id; + this.email = builder.email; + this.active = builder.active; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return getUsername(); + } + + public static class Builder { + private String email; + private String password; + private Collection authorities; + private Long id; + private String username; + private boolean active; + private boolean enabled = true; + private boolean accountNonExpired = true; + private boolean credentialsNonExpired = true; + private boolean accountNonLocked = true; + + public Builder(String email, String password, Collection authorities) { + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public Builder id(Long id) { + this.id = id; + return this; + } + + public Builder username(String username) { + this.username = username; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + public Builder email(String email) { + this.email = email; + return this; + } + + public Builder active(boolean active) { + this.active = active; + return this; + } + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder accountNonExpired(boolean accountNonExpired) { + this.accountNonExpired = accountNonExpired; + return this; + } + + public Builder credentialsNonExpired(boolean credentialsNonExpired) { + this.credentialsNonExpired = credentialsNonExpired; + return this; + } + + public Builder accountNonLocked(boolean accountNonLocked) { + this.accountNonLocked = accountNonLocked; + return this; + } + + public Builder authorities(Collection authorities) { + this.authorities = authorities; + return this; + } + + public AuthorizedUser build() { + return new AuthorizedUser(this); + } + } + + public AuthorizedUser(String username, String password, Collection authorities) { + super(username, password, authorities); + } + + public AuthorizedUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/repository/PermissionRepository.java b/src/main/java/com/backend/user/service/anyame/repository/PermissionRepository.java new file mode 100644 index 0000000..630df1f --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/repository/PermissionRepository.java @@ -0,0 +1,21 @@ +package com.backend.user.service.anyame.repository; + +import com.backend.user.service.anyame.entity.Permission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.Set; + +@Repository +public interface PermissionRepository extends JpaRepository { + Optional findByName(String name); + + @Query("SELECT DISTINCT p FROM User u " + + "JOIN u.roles r " + + "JOIN r.permissions p " + + "WHERE u.name = :name") + Set findByUserName(@Param("name") String name); +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/repository/RoleRepository.java b/src/main/java/com/backend/user/service/anyame/repository/RoleRepository.java new file mode 100644 index 0000000..9e1dcf6 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/repository/RoleRepository.java @@ -0,0 +1,22 @@ +package com.backend.user.service.anyame.repository; + +import com.backend.user.service.anyame.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); + + @Query("SELECT r FROM Role r " + + "LEFT JOIN FETCH r.permissions " + + "LEFT JOIN FETCH r.parentRoles " + + "WHERE r.id IN :roleIds") + List findRolesWithPermissionsAndParents(@Param("roleIds") Set roleIds); +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java b/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java new file mode 100644 index 0000000..7c42e95 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java @@ -0,0 +1,21 @@ +package com.backend.user.service.anyame.repository; + +import com.backend.user.service.anyame.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + + Optional findByEmailOrName(String email, String name); + + @Query("SELECT u FROM User u " + + "LEFT JOIN FETCH u.roles " + + "WHERE u.name = :name") + Optional findByNameWithRoles(@Param("name") String name); +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/CustomOAuth2UserService.java b/src/main/java/com/backend/user/service/anyame/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..8252ddb --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/CustomOAuth2UserService.java @@ -0,0 +1,77 @@ +package com.backend.user.service.anyame.service; + +import com.backend.user.service.anyame.component.AuthorizedUserMapper; +import com.backend.user.service.anyame.entity.Role; +import com.backend.user.service.anyame.entity.User; +import com.backend.user.service.anyame.repository.RoleRepository; +import com.backend.user.service.anyame.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +public class CustomOAuth2UserService implements OAuth2UserService { + private static final Logger log = LoggerFactory.getLogger(CustomOAuth2UserService.class); + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); + private final AuthorizedUserMapper authorizedUserMapper; + + public CustomOAuth2UserService(UserRepository userRepository, RoleRepository roleRepository, AuthorizedUserMapper authorizedUserMapper) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.authorizedUserMapper = authorizedUserMapper; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oauth2User = delegate.loadUser(userRequest); + + return processOAuth2User(userRequest, oauth2User); + } + + private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) { + if (!oauth2User.getAttributes().containsKey("id")) { + throw new OAuth2AuthenticationException("oauth2User id is not provided"); + } + if (!oauth2User.getAttributes().containsKey("name")) { + throw new OAuth2AuthenticationException("oauth2User name is not provided"); + } + log.info("NAME IS: {}, {}, {}", oauth2User.getName(), oauth2User.getAttribute("login"), oauth2User.getAttribute("name")); + log.info("NAME IS: {}, {}, {}", oauth2User.getName(), oauth2User.getAttribute("login"), oauth2User.getAttribute("name")); + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + String email = oauth2User.getAttribute("email"); + String name = oauth2User.getAttribute("name"); + String providerId = oauth2User.getAttribute("id").toString(); + + User user = userRepository.findByEmail(email) + .orElseGet(() -> createNewUser(email, name, registrationId, providerId)); + + // TODO: Should be toggleable nor documented behaviour + // First user becomes admin + if (userRepository.count() == 1) { + Role adminRole = roleRepository.findByName("ADMIN") + .orElseThrow(() -> new RuntimeException("Admin role not found")); + user.getRoles().add(adminRole); + userRepository.save(user); + } + + return authorizedUserMapper.mapOAuth2User(user, oauth2User.getAttributes()); + } + + private User createNewUser(String email, String name, String provider, String providerId) { + User user = new User(name, email, provider, providerId); + user.setActive(true); + + Role userRole = roleRepository.findByName("USER") + .orElseThrow(() -> new RuntimeException("User role not found")); + user.getRoles().add(userRole); + + return userRepository.save(user); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/CustomUserDetailsService.java b/src/main/java/com/backend/user/service/anyame/service/CustomUserDetailsService.java new file mode 100644 index 0000000..cecb620 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package com.backend.user.service.anyame.service; + +import com.backend.user.service.anyame.component.AuthorizedUserMapper; +import com.backend.user.service.anyame.entity.User; +import com.backend.user.service.anyame.repository.UserRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + private final AuthorizedUserMapper authorizedUserMapper; + + public CustomUserDetailsService(UserRepository userRepository, AuthorizedUserMapper authorizedUserMapper) { + this.userRepository = userRepository; + this.authorizedUserMapper = authorizedUserMapper; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Optional userOptional = userRepository.findByEmailOrName(username, username); + if (userOptional.isEmpty()) { + throw new UsernameNotFoundException("user not found " + username); + } + return authorizedUserMapper.mapFormLogin(userOptional.get()); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/UserAuthorityService.java b/src/main/java/com/backend/user/service/anyame/service/UserAuthorityService.java new file mode 100644 index 0000000..889ebb0 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/UserAuthorityService.java @@ -0,0 +1,39 @@ +package com.backend.user.service.anyame.service; + +import com.backend.user.service.anyame.entity.User; +import com.backend.user.service.anyame.repository.PermissionRepository; +import com.backend.user.service.anyame.repository.UserRepository; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.Set; + +@Service +@Transactional(readOnly = true) +public class UserAuthorityService { + + private final UserRepository userRepository; + private final PermissionRepository permissionRepository; + + public UserAuthorityService(UserRepository userRepository, PermissionRepository permissionRepository) { + this.userRepository = userRepository; + this.permissionRepository = permissionRepository; + } + + public Set getUserAuthorities(User user) { + Set authorities = new HashSet<>(); + + user.getRoles().forEach(role -> + authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())) + ); + + permissionRepository.findByUserName(user.getName()).forEach(permission -> + authorities.add(new SimpleGrantedAuthority(permission.getName())) + ); + + return authorities; + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b39d7ef..a49f315 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -6,10 +6,27 @@ spring: authorizationserver: issuer-url: http://localhost:8080 introspection-endpoint: /oauth2/token-info + client: + registration: + github: + clientId: ${GITHUB_CLIENT_ID} + clientSecret: ${GITHUB_CLIENT_SECRET} + scope: + - user:email + - read:user + + google: + clientId: ${GOOGLE_CLIENT_ID} + clientSecret: ${GOOGLE_CLIENT_SECRET} datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} + flyway: + enabled: true + locations: classpath:db/migration/structure, classpath:db/migration/data + validate-on-migrate: true + default-schema: main jpa: hibernate: ddl-auto: create