diff --git a/pom.xml b/pom.xml
index b541229..ba1eac8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,6 +78,11 @@
passay
${passay.version}
+
+ org.postgresql
+ postgresql
+ runtime
+
org.springframework.boot
diff --git a/src/main/java/com/backend/user/service/anyame/component/AuthorizationProperties.java b/src/main/java/com/backend/user/service/anyame/component/AuthorizationProperties.java
new file mode 100644
index 0000000..0fdd990
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/component/AuthorizationProperties.java
@@ -0,0 +1,104 @@
+package com.backend.user.service.anyame.component;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+
+@Component
+@ConfigurationProperties(prefix = "authorization")
+public class AuthorizationProperties {
+
+ private List roles;
+ private String hierarchy;
+ private String defaultRole;
+
+ public List getRoles() {
+ return roles;
+ }
+
+ public String getHierarchy() {
+ return hierarchy;
+ }
+
+ public String getDefaultRole() {
+ return defaultRole;
+ }
+
+ public AuthorizationProperties setRoles(List roles) {
+ this.roles = roles;
+ return this;
+ }
+
+ public AuthorizationProperties setHierarchy(String hierarchy) {
+ this.hierarchy = hierarchy;
+ return this;
+ }
+
+ public AuthorizationProperties setDefaultRole(String defaultRole) {
+ this.defaultRole = defaultRole;
+ return this;
+ }
+
+ public Optional getAccessExpiry(String roleName) {
+ return roles.stream()
+ .filter(role -> role.getName().equals(roleName))
+ .map(RoleConfig::getAccessExpiry)
+ .findFirst();
+ }
+
+ public Optional getRefreshExpiry(String roleName) {
+ return roles.stream()
+ .filter(role -> role.getName().equals(roleName))
+ .map(RoleConfig::getRefreshExpiry)
+ .findFirst();
+ }
+
+ public static class RoleConfig {
+
+ private String name;
+ private Duration accessExpiry;
+ private Duration refreshExpiry;
+ private List privileges;
+
+ public String getName() {
+ return name;
+ }
+
+ public Duration getAccessExpiry() {
+ return accessExpiry;
+ }
+
+ public Duration getRefreshExpiry() {
+ return refreshExpiry;
+ }
+
+ public List getPrivileges() {
+ return privileges;
+ }
+
+ public RoleConfig setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public RoleConfig setAccessExpiry(Duration accessExpiry) {
+ this.accessExpiry = accessExpiry;
+ return this;
+ }
+
+ public RoleConfig setRefreshExpiry(Duration refreshExpiry) {
+ this.refreshExpiry = refreshExpiry;
+ return this;
+ }
+
+ public RoleConfig setPrivileges(List privileges) {
+ this.privileges = privileges;
+ return this;
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/backend/user/service/anyame/component/DefaultRoleProvider.java b/src/main/java/com/backend/user/service/anyame/component/DefaultRoleProvider.java
new file mode 100644
index 0000000..11ee60c
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/component/DefaultRoleProvider.java
@@ -0,0 +1,28 @@
+package com.backend.user.service.anyame.component;
+
+import com.backend.user.service.anyame.entity.Role;
+import com.backend.user.service.anyame.repository.RoleRepository;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DefaultRoleProvider {
+ private final AuthorizationProperties authorizationProperties;
+ private final RoleRepository repository;
+ private Role defaultRole;
+
+ public DefaultRoleProvider(AuthorizationProperties authorizationProperties, RoleRepository repository) {
+ this.authorizationProperties = authorizationProperties;
+ this.repository = repository;
+ }
+
+ @PostConstruct
+ public void init() {
+ defaultRole = repository.findByName(authorizationProperties.getDefaultRole()).orElseThrow();
+ }
+
+ public Role getDefaultRole() {
+ return defaultRole;
+ }
+
+}
diff --git a/src/main/java/com/backend/user/service/anyame/component/JwtExpiryProperties.java b/src/main/java/com/backend/user/service/anyame/component/JwtExpiryProperties.java
deleted file mode 100644
index 9934d30..0000000
--- a/src/main/java/com/backend/user/service/anyame/component/JwtExpiryProperties.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.backend.user.service.anyame.component;
-
-import com.backend.user.service.anyame.entity.Role;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.Map;
-
-@Component
-@ConfigurationProperties(prefix = "jwt.expiry")
-public class JwtExpiryProperties {
-
- private Map access = new HashMap<>();
- private Map refresh = new HashMap<>();
-
- public Map getAccess() {
- return access;
- }
-
- public void setAccess(Map access) {
- this.access = access;
- }
-
- public Map getRefresh() {
- return refresh;
- }
-
- public void setRefresh(Map refresh) {
- this.refresh = refresh;
- }
-
- public Duration getAccessExpiry(Role role) {
- return access.getOrDefault(role, Duration.ofDays(1));
- }
-
- public Duration getRefreshExpiry(Role role) {
- return refresh.getOrDefault(role, Duration.ofDays(30));
- }
-
-}
diff --git a/src/main/java/com/backend/user/service/anyame/component/SetupDataLoader.java b/src/main/java/com/backend/user/service/anyame/component/SetupDataLoader.java
new file mode 100644
index 0000000..1e727bb
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/component/SetupDataLoader.java
@@ -0,0 +1,64 @@
+package com.backend.user.service.anyame.component;
+
+import com.backend.user.service.anyame.entity.Privilege;
+import com.backend.user.service.anyame.entity.Role;
+import com.backend.user.service.anyame.repository.PrivilegeRepository;
+import com.backend.user.service.anyame.repository.RoleRepository;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+@Component
+public class SetupDataLoader implements
+ ApplicationListener {
+
+ boolean alreadySetup = false;
+ private final AuthorizationProperties authorizationProperties;
+ private final RoleRepository roleRepository;
+ private final PrivilegeRepository privilegeRepository;
+
+ public SetupDataLoader(AuthorizationProperties authorizationProperties,
+ RoleRepository roleRepository,
+ PrivilegeRepository privilegeRepository) {
+ this.authorizationProperties = authorizationProperties;
+ this.roleRepository = roleRepository;
+ this.privilegeRepository = privilegeRepository;
+ }
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ if (alreadySetup)
+ return;
+ authorizationProperties.getRoles().forEach(roleConfig -> {
+ List privileges = roleConfig.getPrivileges()
+ .stream()
+ .map(this::createPrivilegeIfNotFound)
+ .toList();
+ createRoleIfNotFound(roleConfig.getName(), privileges);
+ });
+ }
+
+ Privilege createPrivilegeIfNotFound(String name) {
+ Optional privilegeOptional = privilegeRepository.findByName(name);
+ if (privilegeOptional.isEmpty()) {
+ privilegeOptional = Optional.of(new Privilege(name));
+ privilegeRepository.save(privilegeOptional.get());
+ }
+ return privilegeOptional.get();
+ }
+
+ Role createRoleIfNotFound(String name, Collection privileges) {
+ Optional roleOptional = roleRepository.findByName(name);
+ if (roleOptional.isEmpty()) {
+ roleOptional = Optional.of(new Role(name));
+ roleOptional.get().setPrivileges(privileges);
+ roleRepository.save(roleOptional.get());
+ }
+ return roleOptional.get();
+ }
+
+}
\ 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 c1b3130..6049794 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,5 +1,6 @@
package com.backend.user.service.anyame.config;
+import com.backend.user.service.anyame.component.AuthorizationProperties;
import org.passay.CharacterRule;
import org.passay.EnglishCharacterData;
import org.passay.LengthRule;
@@ -7,14 +8,24 @@ import org.passay.PasswordValidator;
import org.passay.WhitespaceRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
import java.util.Arrays;
@Configuration
public class SecurityConfig {
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(c -> c.anyRequest().permitAll()).build();
+ }
+
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -32,4 +43,9 @@ public class SecurityConfig {
));
}
+ @Bean
+ public RoleHierarchy roleHierarchy(AuthorizationProperties authorizationProperties) {
+ return RoleHierarchyImpl.fromHierarchy(authorizationProperties.getHierarchy());
+ }
+
}
diff --git a/src/main/java/com/backend/user/service/anyame/controller/AuthController.java b/src/main/java/com/backend/user/service/anyame/controller/AuthController.java
index 2f3070c..6453b89 100644
--- a/src/main/java/com/backend/user/service/anyame/controller/AuthController.java
+++ b/src/main/java/com/backend/user/service/anyame/controller/AuthController.java
@@ -3,8 +3,10 @@ package com.backend.user.service.anyame.controller;
import com.backend.user.service.anyame.dto.AuthResponse;
import com.backend.user.service.anyame.dto.LoginRequest;
import com.backend.user.service.anyame.dto.RegisterRequest;
-import com.backend.user.service.anyame.exception.EmailAlreadyExistsException;
+import com.backend.user.service.anyame.exception.UserAlreadyExistsException;
import com.backend.user.service.anyame.exception.InvalidCredentialsException;
+import com.backend.user.service.anyame.exception.InvalidRoleException;
+import com.backend.user.service.anyame.exception.NoExpiryDurationException;
import com.backend.user.service.anyame.exception.UnsafePasswordException;
import com.backend.user.service.anyame.service.AuthService;
import org.springframework.http.HttpStatus;
@@ -31,8 +33,12 @@ public class AuthController {
return ResponseEntity.ok(authService.register(request));
} catch (UnsafePasswordException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "unsafe password, length 8-64, 1 upper case, 1 lower case, 1 digit, 1 special symbol. No whitespaces in password.");
- } catch (EmailAlreadyExistsException e) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "email already registered");
+ } catch (UserAlreadyExistsException e) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "user already registered");
+ } catch (NoExpiryDurationException e) {
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "no expiry duration found, invalid server config");
+ } catch (InvalidRoleException e) {
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "invalid role defined: " + e.getInvalidRole());
}
}
@@ -42,6 +48,10 @@ public class AuthController {
return ResponseEntity.ok(authService.login(request));
} catch (InvalidCredentialsException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials", e);
+ } catch (NoExpiryDurationException e) {
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "no expiry duration found, invalid server config");
+ } catch (InvalidRoleException e) {
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "invalid role defined: " + e.getInvalidRole());
}
}
diff --git a/src/main/java/com/backend/user/service/anyame/dto/AuthResponse.java b/src/main/java/com/backend/user/service/anyame/dto/AuthResponse.java
index abd4a62..fceee93 100644
--- a/src/main/java/com/backend/user/service/anyame/dto/AuthResponse.java
+++ b/src/main/java/com/backend/user/service/anyame/dto/AuthResponse.java
@@ -1,6 +1,6 @@
package com.backend.user.service.anyame.dto;
-import com.backend.user.service.anyame.entity.Role;
+import java.util.List;
-public record AuthResponse(String accessToken, String refreshToken, long id, String email, String name, Role role) {
+public record AuthResponse(String accessToken, String refreshToken, long id, String email, String name, List roles) {
}
\ No newline at end of file
diff --git a/src/main/java/com/backend/user/service/anyame/entity/Privilege.java b/src/main/java/com/backend/user/service/anyame/entity/Privilege.java
new file mode 100644
index 0000000..686c895
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/entity/Privilege.java
@@ -0,0 +1,40 @@
+package com.backend.user.service.anyame.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToMany;
+
+import java.util.Collection;
+
+@Entity
+public class Privilege {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+ private String name;
+ @ManyToMany(mappedBy = "privileges")
+ private Collection roles;
+
+ protected Privilege() {
+ }
+
+ public Privilege(String name) {
+ this.name = name;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Collection getRoles() {
+ return roles;
+ }
+
+}
\ 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
index bab4465..8779eb8 100644
--- a/src/main/java/com/backend/user/service/anyame/entity/Role.java
+++ b/src/main/java/com/backend/user/service/anyame/entity/Role.java
@@ -1,9 +1,61 @@
package com.backend.user.service.anyame.entity;
-public enum Role {
- USER,
- MODER,
- ADMIN
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import java.util.Collection;
-}
+@Entity
+public class Role {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+
+ private String name;
+ @ManyToMany(mappedBy = "roles")
+ private Collection users;
+
+ @ManyToMany
+ @JoinTable(
+ name = "roles_privileges",
+ joinColumns = @JoinColumn(
+ name = "role_id", referencedColumnName = "id"),
+ inverseJoinColumns = @JoinColumn(
+ name = "privilege_id", referencedColumnName = "id"))
+ private Collection privileges;
+
+ protected Role() {
+ }
+
+ public Role(String name) {
+ this.name = name;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Collection getUsers() {
+ return users;
+ }
+
+ public Collection getPrivileges() {
+ return privileges;
+ }
+
+ public Role setPrivileges(Collection privileges) {
+ this.privileges = privileges;
+ return this;
+ }
+
+}
\ 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
index a0b359b..dd41ca9 100644
--- a/src/main/java/com/backend/user/service/anyame/entity/User.java
+++ b/src/main/java/com/backend/user/service/anyame/entity/User.java
@@ -2,13 +2,16 @@ package com.backend.user.service.anyame.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
+import java.util.Collection;
+
@Entity
@Table(name = "users")
public class User {
@@ -20,24 +23,29 @@ public class User {
@Column(unique = true, nullable = false)
private String email;
private String password;
- @Enumerated(EnumType.STRING)
- private Role role;
- private boolean emailVerified = true;
+ private boolean enabled = true;
+
+ @ManyToMany
+ @JoinTable(
+ name = "users_roles",
+ joinColumns = @JoinColumn(
+ name = "user_id", referencedColumnName = "id"),
+ inverseJoinColumns = @JoinColumn(
+ name = "role_id", referencedColumnName = "id"))
+ private Collection roles;
protected User() {
}
- public User(String name, String email, String password, Role role) {
+ public User(String name, String email, String password) {
this.name = name;
this.email = email;
this.password = password;
- this.role = role;
}
- public User(String name, String email, Role role) {
+ public User(String name, String email) {
this.name = name;
this.email = email;
- this.role = role;
}
public Long getId() {
@@ -61,16 +69,21 @@ public class User {
return this;
}
- public Role getRole() {
- return role;
+ public boolean isEnabled() {
+ return enabled;
}
- public boolean isEmailVerified() {
- return emailVerified;
+ public User setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
}
- public User setEmailVerified(boolean emailVerified) {
- this.emailVerified = emailVerified;
+ public Collection getRoles() {
+ return roles;
+ }
+
+ public User setRoles(Collection roles) {
+ this.roles = roles;
return this;
}
diff --git a/src/main/java/com/backend/user/service/anyame/exception/EmailAlreadyExistsException.java b/src/main/java/com/backend/user/service/anyame/exception/EmailAlreadyExistsException.java
deleted file mode 100644
index 20e35cb..0000000
--- a/src/main/java/com/backend/user/service/anyame/exception/EmailAlreadyExistsException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.backend.user.service.anyame.exception;
-
-public class EmailAlreadyExistsException extends Exception {
-
- public EmailAlreadyExistsException() {
- super("email already exists");
- }
-
-}
diff --git a/src/main/java/com/backend/user/service/anyame/exception/InvalidRoleException.java b/src/main/java/com/backend/user/service/anyame/exception/InvalidRoleException.java
new file mode 100644
index 0000000..153e343
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/exception/InvalidRoleException.java
@@ -0,0 +1,19 @@
+package com.backend.user.service.anyame.exception;
+
+public class InvalidRoleException extends Exception {
+ private final String invalidRole;
+
+ public InvalidRoleException(String invalidRole) {
+ this.invalidRole = invalidRole;
+ }
+
+ public InvalidRoleException(String message, String invalidRole) {
+ super(message);
+ this.invalidRole = invalidRole;
+ }
+
+ public String getInvalidRole() {
+ return invalidRole;
+ }
+
+}
diff --git a/src/main/java/com/backend/user/service/anyame/exception/NoExpiryDurationException.java b/src/main/java/com/backend/user/service/anyame/exception/NoExpiryDurationException.java
new file mode 100644
index 0000000..f159331
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/exception/NoExpiryDurationException.java
@@ -0,0 +1,5 @@
+package com.backend.user.service.anyame.exception;
+
+public class NoExpiryDurationException extends Exception {
+
+}
diff --git a/src/main/java/com/backend/user/service/anyame/exception/UserAlreadyExistsException.java b/src/main/java/com/backend/user/service/anyame/exception/UserAlreadyExistsException.java
new file mode 100644
index 0000000..fd83018
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/exception/UserAlreadyExistsException.java
@@ -0,0 +1,9 @@
+package com.backend.user.service.anyame.exception;
+
+public class UserAlreadyExistsException extends Exception {
+
+ public UserAlreadyExistsException() {
+ super("user already exists");
+ }
+
+}
diff --git a/src/main/java/com/backend/user/service/anyame/repository/PrivilegeRepository.java b/src/main/java/com/backend/user/service/anyame/repository/PrivilegeRepository.java
new file mode 100644
index 0000000..4f8a585
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/repository/PrivilegeRepository.java
@@ -0,0 +1,12 @@
+package com.backend.user.service.anyame.repository;
+
+import com.backend.user.service.anyame.entity.Privilege;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface PrivilegeRepository extends JpaRepository {
+
+ Optional findByName(String name);
+
+}
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..4204795
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/repository/RoleRepository.java
@@ -0,0 +1,12 @@
+package com.backend.user.service.anyame.repository;
+
+import com.backend.user.service.anyame.entity.Role;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface RoleRepository extends JpaRepository {
+
+ Optional findByName(String roleName);
+
+}
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
index a8f7147..0663e18 100644
--- a/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java
+++ b/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java
@@ -6,5 +6,9 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository {
+
Optional findByEmail(String email);
+
+ Optional findByEmailOrName(String email, String name);
+
}
diff --git a/src/main/java/com/backend/user/service/anyame/service/AuthService.java b/src/main/java/com/backend/user/service/anyame/service/AuthService.java
index 70e58b8..8195f97 100644
--- a/src/main/java/com/backend/user/service/anyame/service/AuthService.java
+++ b/src/main/java/com/backend/user/service/anyame/service/AuthService.java
@@ -1,17 +1,23 @@
package com.backend.user.service.anyame.service;
+import com.backend.user.service.anyame.component.DefaultRoleProvider;
import com.backend.user.service.anyame.dto.AuthResponse;
import com.backend.user.service.anyame.dto.LoginRequest;
import com.backend.user.service.anyame.dto.RegisterRequest;
import com.backend.user.service.anyame.entity.Role;
import com.backend.user.service.anyame.entity.User;
-import com.backend.user.service.anyame.exception.EmailAlreadyExistsException;
+import com.backend.user.service.anyame.exception.UserAlreadyExistsException;
import com.backend.user.service.anyame.exception.InvalidCredentialsException;
+import com.backend.user.service.anyame.exception.InvalidRoleException;
+import com.backend.user.service.anyame.exception.NoExpiryDurationException;
import com.backend.user.service.anyame.exception.UnsafePasswordException;
import com.backend.user.service.anyame.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
+import java.util.Collections;
+import java.util.List;
+
@Service
public class AuthService {
@@ -19,20 +25,28 @@ public class AuthService {
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final PasswordValidatorService passwordValidator;
+ private final DefaultRoleProvider defaultRoleProvider;
- public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService, PasswordValidatorService passwordValidator) {
+ public AuthService(UserRepository userRepository,
+ PasswordEncoder passwordEncoder,
+ JwtService jwtService,
+ PasswordValidatorService passwordValidator,
+ DefaultRoleProvider defaultRoleProvider) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
this.passwordValidator = passwordValidator;
+ this.defaultRoleProvider = defaultRoleProvider;
}
- public AuthResponse register(RegisterRequest request) throws UnsafePasswordException, EmailAlreadyExistsException {
- if (userRepository.findByEmail(request.email()).isPresent()) {
- throw new EmailAlreadyExistsException();
+ public AuthResponse register(
+ RegisterRequest request) throws UnsafePasswordException, UserAlreadyExistsException, NoExpiryDurationException, InvalidRoleException {
+ if (userRepository.findByEmailOrName(request.email(), request.name()).isPresent()) {
+ throw new UserAlreadyExistsException();
}
- User user = new User(request.name(), request.email(), Role.USER);
+ User user = new User(request.name(), request.email());
+ user.setRoles(Collections.singleton(defaultRoleProvider.getDefaultRole()));
if (request.password() != null && !request.password().isBlank()) {
if (!passwordValidator.validate(request.password())) {
@@ -40,11 +54,12 @@ public class AuthService {
}
user.setPassword(passwordEncoder.encode(request.password()));
}
+ user = userRepository.save(user);
return generateAuthResponse(user);
}
- public AuthResponse login(LoginRequest request) throws InvalidCredentialsException {
+ public AuthResponse login(LoginRequest request) throws InvalidCredentialsException, NoExpiryDurationException, InvalidRoleException {
User user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new RuntimeException("Invalid credentials"));
@@ -55,10 +70,11 @@ public class AuthService {
return generateAuthResponse(user);
}
- private AuthResponse generateAuthResponse(User user) {
+ private AuthResponse generateAuthResponse(User user) throws NoExpiryDurationException, InvalidRoleException {
String accessToken = jwtService.generateAccessToken(user);
String refreshToken = jwtService.generateRefreshToken(user);
- return new AuthResponse(accessToken, refreshToken, user.getId(), user.getEmail(), user.getName(), user.getRole());
+ List roles = user.getRoles().stream().map(Role::getName).toList();
+ return new AuthResponse(accessToken, refreshToken, user.getId(), user.getEmail(), user.getName(), roles);
}
}
diff --git a/src/main/java/com/backend/user/service/anyame/service/AuthorityResolver.java b/src/main/java/com/backend/user/service/anyame/service/AuthorityResolver.java
new file mode 100644
index 0000000..7679150
--- /dev/null
+++ b/src/main/java/com/backend/user/service/anyame/service/AuthorityResolver.java
@@ -0,0 +1,58 @@
+package com.backend.user.service.anyame.service;
+
+import com.backend.user.service.anyame.entity.Privilege;
+import com.backend.user.service.anyame.entity.Role;
+import com.backend.user.service.anyame.entity.User;
+import com.backend.user.service.anyame.exception.InvalidRoleException;
+import com.backend.user.service.anyame.repository.RoleRepository;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class AuthorityResolver {
+ private final RoleRepository roleRepository;
+ private final RoleHierarchy roleHierarchy;
+
+ public AuthorityResolver(RoleRepository roleRepository, RoleHierarchy roleHierarchy) {
+ this.roleRepository = roleRepository;
+ this.roleHierarchy = roleHierarchy;
+ }
+
+ public List getAuthorities(User user) throws InvalidRoleException {
+ List grantedAuthorities = user.getRoles().stream()
+ .map(role -> new SimpleGrantedAuthority(role.getName()))
+ .collect(Collectors.toList());
+
+ Collection extends GrantedAuthority> reachableAuthorities =
+ roleHierarchy.getReachableGrantedAuthorities(grantedAuthorities);
+
+ Set privileges = new HashSet<>();
+ for (GrantedAuthority authority : reachableAuthorities) {
+ String roleName = authority.getAuthority();
+ Role role = roleRepository.findByName(roleName)
+ .orElseThrow(() -> new InvalidRoleException(roleName));
+ if (role != null && role.getPrivileges() != null) {
+ privileges.addAll(role.getPrivileges().stream()
+ .map(Privilege::getName)
+ .collect(Collectors.toSet()));
+ }
+ }
+
+ Set allAuthorities = reachableAuthorities.stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.toSet());
+ allAuthorities.addAll(privileges);
+
+ return new ArrayList<>(allAuthorities);
+ }
+
+}
diff --git a/src/main/java/com/backend/user/service/anyame/service/JwtService.java b/src/main/java/com/backend/user/service/anyame/service/JwtService.java
index 579c137..f601764 100644
--- a/src/main/java/com/backend/user/service/anyame/service/JwtService.java
+++ b/src/main/java/com/backend/user/service/anyame/service/JwtService.java
@@ -1,36 +1,54 @@
package com.backend.user.service.anyame.service;
+import com.backend.user.service.anyame.component.AuthorizationProperties;
import com.backend.user.service.anyame.component.JWTSecretProvider;
-import com.backend.user.service.anyame.component.JwtExpiryProperties;
+import com.backend.user.service.anyame.entity.Role;
import com.backend.user.service.anyame.entity.User;
+import com.backend.user.service.anyame.exception.InvalidRoleException;
+import com.backend.user.service.anyame.exception.NoExpiryDurationException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
@Service
public class JwtService {
private final JWTSecretProvider secretProvider;
- private final JwtExpiryProperties expiryProperties;
+ private final AuthorizationProperties authorizationProperties;
+ private final AuthorityResolver authorityResolver;
- public JwtService(JWTSecretProvider secretProvider, JwtExpiryProperties expiryProperties) {
+ public JwtService(JWTSecretProvider secretProvider,
+ AuthorizationProperties authorizationProperties,
+ AuthorityResolver authorityResolver) {
this.secretProvider = secretProvider;
- this.expiryProperties = expiryProperties;
+ this.authorizationProperties = authorizationProperties;
+ this.authorityResolver = authorityResolver;
}
- public String generateRefreshToken(User user) {
+ public String generateRefreshToken(User user) throws InvalidRoleException, NoExpiryDurationException {
+ List authorities = authorityResolver.getAuthorities(user);
+ List roles = user.getRoles().stream()
+ .map(Role::getName)
+ .toList();
JwtBuilder builder = Jwts.builder()
.subject(user.getEmail())
.claim("name", user.getName())
.claim("id", user.getId())
- .claim("role", user.getRole())
+ .claim("roles", roles)
+ .claim("authorities", authorities)
.claim("type", "refresh");
- Duration refreshExpiry = expiryProperties.getRefreshExpiry(user.getRole());
+ Duration refreshExpiry = getMaxExpiry(user,
+ role -> authorizationProperties.getRefreshExpiry(role.getName()))
+ .orElseThrow(NoExpiryDurationException::new);
Date issuedAt = new Date();
Date expiryDate = Date.from(Instant.now().plus(refreshExpiry));
@@ -41,14 +59,21 @@ public class JwtService {
.compact();
}
- public String generateAccessToken(User user) {
+ public String generateAccessToken(User user) throws InvalidRoleException, NoExpiryDurationException {
+ List authorities = authorityResolver.getAuthorities(user);
+ List roles = user.getRoles().stream()
+ .map(Role::getName)
+ .toList();
JwtBuilder builder = Jwts.builder()
.subject(user.getEmail())
.claim("name", user.getName())
.claim("id", user.getId())
- .claim("role", user.getRole())
+ .claim("roles", roles)
+ .claim("authorities", authorities)
.claim("type", "access");
- Duration accessExpiry = expiryProperties.getAccessExpiry(user.getRole());
+ Duration accessExpiry = getMaxExpiry(user,
+ role -> authorizationProperties.getAccessExpiry(role.getName()))
+ .orElseThrow(NoExpiryDurationException::new);
Date issuedAt = new Date();
Date expiryDate = Date.from(Instant.now().plus(accessExpiry));
@@ -59,6 +84,14 @@ public class JwtService {
.compact();
}
+ private Optional getMaxExpiry(User user, Function> mapper) {
+ return user.getRoles().stream()
+ .map(mapper)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .max(Duration::compareTo);
+ }
+
public String extractEmail(String token) {
return getClaims(token).getSubject();
}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 7954564..4d4044c 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -1,11 +1,30 @@
jwt:
secret: ${JWT_SECRET}
- expiry:
- access:
- USER: 1d
- MODER: 12h
- ADMIN: 30m
- refresh:
- USER: 90d
- MODER: 7d
- ADMIN: 2h
\ No newline at end of file
+authorization:
+ roles:
+ - name: ROLE_USER
+ access-expiry: '1d'
+ refresh-expiry: '90d'
+ privileges:
+ - READ_PRIVILEGE
+ - name: ROLE_ADMIN
+ access-expiry: '30m'
+ refresh-expiry: '2h'
+ privileges:
+ - WRITE_PRIVILEGE
+ - CHANGE_PASSWORD_PRIVILEGE
+ hierarchy: |
+ ROLE_ADMIN > ROLE_USER
+ default-role: ROLE_USER
+
+spring:
+ datasource:
+ url: ${DATABASE_URL}
+ username: ${DATABASE_USERNAME}
+ password: ${DATABASE_PASSWORD}
+ jpa:
+ hibernate:
+ ddl-auto: none
+server:
+ error:
+ include-message: always