This commit is contained in:
mindol1004
2024-10-18 13:03:05 +09:00
parent 6dfa661394
commit 4baccedad5
36 changed files with 422 additions and 274 deletions

View File

@@ -7,6 +7,7 @@ import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -17,9 +18,11 @@ public class GlobalErrorController implements ErrorController {
@GetMapping("/error") @GetMapping("/error")
public String handleError(HttpServletRequest request, Model model) { public String handleError(HttpServletRequest request, Model model) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
String errorMessage = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_MESSAGE));
String statusMsg = status.toString(); String statusMsg = status.toString();
HttpStatus httpStatus = HttpStatus.valueOf(Integer.valueOf(statusMsg)); HttpStatus httpStatus = HttpStatus.valueOf(Integer.valueOf(statusMsg));
model.addAttribute("message", statusMsg + " " + httpStatus.getReasonPhrase()); model.addAttribute("message", statusMsg + " " + httpStatus.getReasonPhrase());
if (StringUtils.hasText(errorMessage)) model.addAttribute("errorMessage", errorMessage);
return "pages/error/error"; return "pages/error/error";
} }

View File

@@ -1,4 +1,4 @@
package com.spring.common.advice; package com.spring.common.support;
import java.time.LocalDateTime; import java.time.LocalDateTime;

View File

@@ -1,4 +1,4 @@
package com.spring.common.advice; package com.spring.common.support;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -8,6 +8,9 @@ import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@@ -15,6 +18,7 @@ import com.spring.common.converter.CommHttpMessageConverter;
import com.spring.common.error.ErrorResponse; import com.spring.common.error.ErrorResponse;
import com.spring.common.error.ErrorRule; import com.spring.common.error.ErrorRule;
import com.spring.common.error.GlobalExceptionHandler; import com.spring.common.error.GlobalExceptionHandler;
import com.spring.common.validation.CollectionValidator;
import com.spring.infra.security.error.SecurityExceptionHandler; import com.spring.infra.security.error.SecurityExceptionHandler;
@RestControllerAdvice( @RestControllerAdvice(
@@ -22,6 +26,17 @@ import com.spring.infra.security.error.SecurityExceptionHandler;
basePackageClasses = { GlobalExceptionHandler.class, SecurityExceptionHandler.class } basePackageClasses = { GlobalExceptionHandler.class, SecurityExceptionHandler.class }
) )
public class ResponseWrapper implements ResponseBodyAdvice<Object> { public class ResponseWrapper implements ResponseBodyAdvice<Object> {
private final LocalValidatorFactoryBean validator;
public ResponseWrapper(LocalValidatorFactoryBean validator) {
this.validator = validator;
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addValidators(new CollectionValidator(validator));
}
@Override @Override
public boolean supports( public boolean supports(

View File

@@ -0,0 +1,32 @@
package com.spring.common.validation;
import java.util.Collection;
import org.springframework.lang.NonNull;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
public class CollectionValidator implements Validator {
private final Validator validator;
public CollectionValidator(LocalValidatorFactoryBean validatorFactory) {
this.validator = validatorFactory;
}
@Override
public boolean supports(@NonNull Class<?> clazz) {
return true;
}
@Override
public void validate(@NonNull Object target, @NonNull Errors errors) {
if (target instanceof Collection<?>) {
Collection<?> collection = (Collection<?>) target;
for (Object object : collection) {
ValidationUtils.invokeValidator(validator, object, errors);
}
}
}
}

View File

@@ -4,6 +4,7 @@ import java.util.List;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -30,31 +31,37 @@ public class ScheduleJobApi {
private final ScheduleControlService scheduleControlService; private final ScheduleControlService scheduleControlService;
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
public List<ScheduleJobResponse> getAllJobs(@RequestParam(required = false) String groupName, @RequestParam(required = false) String jobName) { public List<ScheduleJobResponse> getAllJobs(@RequestParam(required = false) String groupName, @RequestParam(required = false) String jobName) {
return findScheduleJobService.getAllJobs(groupName, jobName); return findScheduleJobService.getAllJobs(groupName, jobName);
} }
@GetMapping("/{groupName}/{jobName}") @GetMapping("/{groupName}/{jobName}")
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
public ScheduleJobResponse getJobDetail(@PathVariable String groupName, @PathVariable String jobName) { public ScheduleJobResponse getJobDetail(@PathVariable String groupName, @PathVariable String jobName) {
return findScheduleJobService.getJobDetail(groupName, jobName); return findScheduleJobService.getJobDetail(groupName, jobName);
} }
@GetMapping("/pause/{groupName}/{jobName}") @GetMapping("/pause/{groupName}/{jobName}")
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
public void pauseJob(@PathVariable String groupName, @PathVariable String jobName) { public void pauseJob(@PathVariable String groupName, @PathVariable String jobName) {
scheduleControlService.pauseJob(groupName, jobName); scheduleControlService.pauseJob(groupName, jobName);
} }
@GetMapping("/resume/{groupName}/{jobName}") @GetMapping("/resume/{groupName}/{jobName}")
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
public void resumeJob(@PathVariable String groupName, @PathVariable String jobName) { public void resumeJob(@PathVariable String groupName, @PathVariable String jobName) {
scheduleControlService.resumeJob(groupName, jobName); scheduleControlService.resumeJob(groupName, jobName);
} }
@GetMapping("/trigger/{groupName}/{jobName}") @GetMapping("/trigger/{groupName}/{jobName}")
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
public void triggerJob(@PathVariable String groupName, @PathVariable String jobName) { public void triggerJob(@PathVariable String groupName, @PathVariable String jobName) {
scheduleControlService.triggerJob(groupName, jobName); scheduleControlService.triggerJob(groupName, jobName);
} }
@PostMapping("/reschedule") @PostMapping("/reschedule")
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
public boolean rescheduleJob(@Valid @RequestBody ReScheduleJobRequest request) { public boolean rescheduleJob(@Valid @RequestBody ReScheduleJobRequest request) {
return reScheduleJobService.rescheduleJob(request); return reScheduleJobService.rescheduleJob(request);
} }

View File

@@ -4,7 +4,10 @@ import java.util.List;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -25,13 +28,21 @@ public class UserManagementApi {
private final UserManagementService userManagementService; private final UserManagementService userManagementService;
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('SUPER')")
public List<UserManagementResponse> getUsers(UserFindRequest request) { public List<UserManagementResponse> getUsers(UserFindRequest request) {
return userManagementService.getUsers(request); return userManagementService.getUsers(request);
} }
@PutMapping("/change-role-approve") @PutMapping("/change-role-approve")
@PreAuthorize("hasAnyRole('SUPER')")
public void changeRoleApprove(@RequestBody @Valid List<ChangeUserRoleApproveRequest> requests) { public void changeRoleApprove(@RequestBody @Valid List<ChangeUserRoleApproveRequest> requests) {
userManagementService.changeRoleApprove(requests); userManagementService.changeRoleApprove(requests);
} }
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('SUPER')")
public void deleteUser(@PathVariable String id) {
userManagementService.deleteUser(id);
}
} }

View File

@@ -20,6 +20,6 @@ public class ChangeUserRoleApproveRequest {
private final AgentUserRole userRole; private final AgentUserRole userRole;
@NotNull(message = "승인 여부는 필수값 입니다.") @NotNull(message = "승인 여부는 필수값 입니다.")
private final boolean isApproved; private final boolean approved;
} }

View File

@@ -38,10 +38,10 @@ public class SignUpRequest {
public AgentUser toEntity() { public AgentUser toEntity() {
return AgentUser.builder() return AgentUser.builder()
.userId(userId) .userId(userId)
.userPassword(userPassword) .password(userPassword)
.userName(userName) .name(userName)
.email(email) .email(email)
.isApproved(false) .approved(false)
.userRole(userRole) .userRole(userRole)
.build(); .build();
} }

View File

@@ -26,7 +26,7 @@ public class UserFindRequest {
} }
public static boolean matchesUserName(AgentUser user, String userName) { public static boolean matchesUserName(AgentUser user, String userName) {
return userName == null || userName.isEmpty() || user.getMemberName().contains(userName); return userName == null || userName.isEmpty() || user.getName().contains(userName);
} }
public static boolean matchesEmail(AgentUser user, String email) { public static boolean matchesEmail(AgentUser user, String email) {

View File

@@ -21,7 +21,7 @@ public class UserManagementResponse {
return new UserManagementResponse( return new UserManagementResponse(
user.getId().toString(), user.getId().toString(),
user.getUserId(), user.getUserId(),
user.getMemberName(), user.getName(),
user.getEmail(), user.getEmail(),
user.isApproved(), user.isApproved(),
user.getUserRole() user.getUserRole()

View File

@@ -1,9 +1,6 @@
package com.spring.domain.user.entity; package com.spring.domain.user.entity;
import java.util.Arrays;
import java.util.Collection;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
@@ -14,10 +11,6 @@ import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import com.spring.infra.security.domain.UserPrincipal;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Builder; import lombok.Builder;
@@ -28,7 +21,7 @@ import lombok.NoArgsConstructor;
@Table(name = "AGENT_USER") @Table(name = "AGENT_USER")
@Getter @Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AgentUser implements UserPrincipal { public class AgentUser {
@Id @Id
@GeneratedValue(generator = "uuid2") @GeneratedValue(generator = "uuid2")
@@ -39,91 +32,42 @@ public class AgentUser implements UserPrincipal {
@Column(name = "USER_ID", nullable = false, length = 50) @Column(name = "USER_ID", nullable = false, length = 50)
private String userId; private String userId;
@Column(name = "USER_PASSWORD", nullable = false, length = 128) @Column(name = "PASSWORD", nullable = false, length = 128)
private String userPassword; private String password;
@Column(name = "USER_NAME", nullable = false, length = 50) @Column(name = "NAME", nullable = false, length = 50)
private String userName; private String name;
@Column(name = "EMAIL", nullable = false, length = 100) @Column(name = "EMAIL", nullable = false, length = 100)
private String email; private String email;
@Column(name = "IS_APPROVED", nullable = false) @Column(name = "APPROVED", nullable = false)
private boolean isApproved; private boolean approved;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "USER_ROLE", nullable = false, length = 50) @Column(name = "USER_ROLE", nullable = false, length = 50)
private AgentUserRole userRole; private AgentUserRole userRole;
@Builder @Builder
public AgentUser(String userId, String userPassword, String userName, AgentUserRole userRole, String email, boolean isApproved) { public AgentUser(String userId, String password, String name, AgentUserRole userRole, String email, boolean approved) {
this.userId = userId; this.userId = userId;
this.userPassword = userPassword; this.password = password;
this.userName = userName; this.name = name;
this.userRole = userRole; this.userRole = userRole;
this.email = email; this.email = email;
this.isApproved = isApproved; this.approved = approved;
} }
public void changePassword(String newPassword) { public void changePassword(String newPassword) {
this.userPassword = newPassword; this.password = newPassword;
} }
public void changeUserRole(AgentUserRole userRole) { public void changeUserRole(AgentUserRole userRole) {
this.userRole = userRole; this.userRole = userRole;
} }
public void changeApproved(boolean isApproved) { public void changeApproved(boolean approved) {
this.isApproved = isApproved; this.approved = approved;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.stream(AgentUserRole.values())
.filter(role -> Arrays.asList(this.userRole).contains(role))
.map(AgentUserRole::getRole)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getPassword() {
return this.userPassword;
}
@Override
public String getUsername() {
return this.userId;
}
@Override
public String getKey() {
return this.id.toString();
}
@Override
public String getMemberName() {
return this.userName;
} }
} }

View File

@@ -40,4 +40,11 @@ public class UserManagementService {
} }
} }
@Transactional
public void deleteUser(String id) {
AgentUser user = agentUserRepository.findById(UUID.fromString(id))
.orElseThrow(UserNotFoundException::new);
agentUserRepository.delete(user);
}
} }

View File

@@ -25,15 +25,18 @@ public class UserPrincipalService implements UserDetailsService, UserAuthenticat
@Transactional(readOnly = true) @Transactional(readOnly = true)
@Override @Override
public UserPrincipal loadUserByUsername(String username) throws UsernameNotFoundException { public UserPrincipal loadUserByUsername(String username) throws UsernameNotFoundException {
return agentUserRepository.findByUserId(username) return UserPrincipal.valueOf(
.orElseThrow(() -> new SecurityAuthException(SecurityExceptionRule.USER_UNAUTHORIZED)); agentUserRepository.findByUserId(username)
.orElseThrow(() -> new SecurityAuthException(SecurityExceptionRule.USER_UNAUTHORIZED))
);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@Override @Override
public UserPrincipal getUserDetails(String key) { public UserPrincipal getUserDetails(String key) {
return agentUserRepository.findById(UUID.fromString(key)) return UserPrincipal.valueOf(
.orElseThrow(UserNotFoundException::new); agentUserRepository.findById(UUID.fromString(key)).orElseThrow(UserNotFoundException::new)
);
} }
} }

View File

@@ -12,6 +12,7 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
@@ -71,6 +72,7 @@ public class SecurityConfig {
.csrf(CsrfConfigurer::disable) .csrf(CsrfConfigurer::disable)
.httpBasic(HttpBasicConfigurer::disable) .httpBasic(HttpBasicConfigurer::disable)
.formLogin(FormLoginConfigurer::disable) .formLogin(FormLoginConfigurer::disable)
.anonymous(AnonymousConfigurer::disable)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.antMatchers(Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).toArray(String[]::new)).permitAll() .antMatchers(Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).toArray(String[]::new)).permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()

View File

@@ -1,63 +0,0 @@
package com.spring.infra.security.domain;
import java.util.Collection;
import java.util.Collections;
import org.springframework.security.core.GrantedAuthority;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class JwtUserPrincipal implements UserPrincipal {
private final String userId;
private final String userName;
@Override
public String getKey() {
return null;
}
@Override
public String getMemberName() {
return null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@@ -1,11 +1,78 @@
package com.spring.infra.security.domain; package com.spring.infra.security.domain;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
public interface UserPrincipal extends UserDetails { import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.entity.AgentUserRole;
String getKey();
String getMemberName(); import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class UserPrincipal implements UserDetails {
private final transient AgentUser agentUser;
public static UserPrincipal valueOf(AgentUser agentUser) {
return new UserPrincipal(agentUser);
}
public static UserPrincipal of(String userId, String userName) {
return new UserPrincipal(AgentUser.builder().userId(userId).name(userName).build());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.stream(AgentUserRole.values())
.filter(role -> Arrays.asList(agentUser.getUserRole()).contains(role))
.map(AgentUserRole::getRole)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getPassword() {
return agentUser.getPassword();
}
@Override
public String getUsername() {
return agentUser.getUserId();
}
public String getKey() {
return agentUser.getId().toString();
}
public String getMemberName() {
return agentUser.getName();
}
} }

View File

@@ -9,6 +9,11 @@ public class SecurityAuthException extends AuthenticationException {
private final SecurityExceptionRule exceptionRule; private final SecurityExceptionRule exceptionRule;
public SecurityAuthException() {
super(SecurityExceptionRule.SYSTEM_ERROR.getMessage());
this.exceptionRule = SecurityExceptionRule.SYSTEM_ERROR;
}
public SecurityAuthException(String msg) { public SecurityAuthException(String msg) {
super(msg); super(msg);
this.exceptionRule = SecurityExceptionRule.SYSTEM_ERROR; this.exceptionRule = SecurityExceptionRule.SYSTEM_ERROR;

View File

@@ -1,27 +1,33 @@
package com.spring.infra.security.error; package com.spring.infra.security.error;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException; import io.jsonwebtoken.security.SignatureException;
import lombok.RequiredArgsConstructor;
@RestControllerAdvice @RestControllerAdvice
@RequiredArgsConstructor
public class SecurityExceptionHandler { public class SecurityExceptionHandler {
private final AccessDeniedHandler accessDeniedHandler;
@ExceptionHandler(SecurityAuthException.class) @ExceptionHandler(SecurityAuthException.class)
public SecurityErrorResponse handleAuthenticationException(SecurityAuthException e) { public SecurityErrorResponse handleAuthenticationException(SecurityAuthException e) {
return SecurityErrorResponse.valueOf(e.getExceptionRule()); return SecurityErrorResponse.valueOf(e.getExceptionRule());
} }
@ExceptionHandler(AccessDeniedException.class)
public SecurityErrorResponse handleAccessDeniedException() {
return SecurityErrorResponse.valueOf(SecurityExceptionRule.USER_FORBIDDEN);
}
@ExceptionHandler(AuthenticationException.class) @ExceptionHandler(AuthenticationException.class)
public SecurityErrorResponse handleAuthenticationException() { public SecurityErrorResponse handleAuthenticationException() {
return SecurityErrorResponse.valueOf(SecurityExceptionRule.USER_UNAUTHORIZED); return SecurityErrorResponse.valueOf(SecurityExceptionRule.USER_UNAUTHORIZED);
@@ -31,15 +37,22 @@ public class SecurityExceptionHandler {
public SecurityErrorResponse handleSignatureException() { public SecurityErrorResponse handleSignatureException() {
return SecurityErrorResponse.valueOf(SecurityExceptionRule.SIGNATURE_ERROR); return SecurityErrorResponse.valueOf(SecurityExceptionRule.SIGNATURE_ERROR);
} }
@ExceptionHandler(MalformedJwtException.class) @ExceptionHandler(MalformedJwtException.class)
public SecurityErrorResponse handleMalformedJwtException() { public SecurityErrorResponse handleMalformedJwtException() {
return SecurityErrorResponse.valueOf(SecurityExceptionRule.MALFORMED_JWT_ERROR); return SecurityErrorResponse.valueOf(SecurityExceptionRule.MALFORMED_JWT_ERROR);
} }
@ExceptionHandler(ExpiredJwtException.class) @ExceptionHandler(ExpiredJwtException.class)
public SecurityErrorResponse handleExpiredJwtException() { public SecurityErrorResponse handleExpiredJwtException() {
return SecurityErrorResponse.valueOf(SecurityExceptionRule.EXPIRED_JWT_ERROR); return SecurityErrorResponse.valueOf(SecurityExceptionRule.EXPIRED_JWT_ERROR);
} }
@ExceptionHandler(AccessDeniedException.class)
public void handleAccessDeniedException(
AccessDeniedException ex, HttpServletRequest request, HttpServletResponse response
) throws IOException, ServletException {
accessDeniedHandler.handle(request, response, ex);
}
} }

View File

@@ -17,18 +17,17 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.domain.user.entity.AgentUser;
import com.spring.infra.security.config.PermittedURI; import com.spring.infra.security.config.PermittedURI;
import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.dto.SignInRequest; import com.spring.infra.security.dto.SignInRequest;
import com.spring.infra.security.error.SecurityAuthException; import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule; import com.spring.infra.security.error.SecurityExceptionRule;
public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_LOGIN_REQUEST_URL = PermittedURI.USER_SIGN_IN.getUri(); private static final String DEFAULT_LOGIN_REQUEST_URI = PermittedURI.USER_SIGN_IN.getUri();
private static final String HTTP_METHOD = "POST"; private static final String HTTP_METHOD = "POST";
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER = private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URI, HTTP_METHOD);
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -38,8 +37,11 @@ public class AuthenticationProcessingFilter extends AbstractAuthenticationProces
} }
@Override @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) public Authentication attemptAuthentication(
throws AuthenticationException, IOException, ServletException { HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException, IOException, ServletException {
if (!isValidRequestType(request)) { if (!isValidRequestType(request)) {
throw new SecurityAuthException(SecurityExceptionRule.UNSUPPORTED_MEDIA_ERROR); throw new SecurityAuthException(SecurityExceptionRule.UNSUPPORTED_MEDIA_ERROR);
} }
@@ -49,9 +51,14 @@ public class AuthenticationProcessingFilter extends AbstractAuthenticationProces
} }
var token = new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword()); var token = new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword());
var authentication = this.getAuthenticationManager().authenticate(token); var authentication = this.getAuthenticationManager().authenticate(token);
if (!((AgentUser) authentication.getPrincipal()).isApproved()) {
throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_APPROVED); if (authentication.getPrincipal() instanceof UserPrincipal) {
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
if (!user.getAgentUser().isApproved()) {
throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_APPROVED);
}
} }
return authentication; return authentication;
} }

View File

@@ -13,7 +13,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import com.spring.infra.security.config.PermittedURI; import com.spring.infra.security.config.PermittedURI;
@@ -33,7 +32,6 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public final class JwtAuthenticationFilter extends OncePerRequestFilter { public final class JwtAuthenticationFilter extends OncePerRequestFilter {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final JwtTokenService jwtTokenService; private final JwtTokenService jwtTokenService;
private static final String EXCEPTION_ATTRIBUTE = "exception"; private static final String EXCEPTION_ATTRIBUTE = "exception";
@@ -53,9 +51,7 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
@NonNull FilterChain filterChain @NonNull FilterChain filterChain
) throws ServletException, IOException { ) throws ServletException, IOException {
String requestURI = request.getRequestURI(); if (isPermittedURI(request.getRequestURI())) {
if (Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).anyMatch(uri -> pathMatcher.match(uri, requestURI)) &&
!PermittedURI.ROOT_URI.getUri().equals(requestURI)) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
@@ -64,23 +60,20 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
String accessToken = parseBearerToken(request, HttpHeaders.AUTHORIZATION); String accessToken = parseBearerToken(request, HttpHeaders.AUTHORIZATION);
if (jwtTokenService.validateAccessToken(accessToken)) { if (jwtTokenService.validateAccessToken(accessToken)) {
setAuthenticationToContext(accessToken); setAuthenticationToContext(accessToken);
return; } else {
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
jwtTokenService.validateToken(refreshToken);
String reissuedAccessToken = jwtTokenService.getRefreshToken(refreshToken);
Authentication authentication = jwtTokenService.getAuthentication(reissuedAccessToken);
jwtTokenService.saveRefreshToken(authentication.getName(), jwtTokenService.generateRefreshToken(response, authentication));
setAuthenticationToContext(jwtTokenService.generateAccessToken(response, authentication));
} }
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
jwtTokenService.validateToken(refreshToken);
String reissuedAccessToken = jwtTokenService.getRefreshToken(refreshToken);
Authentication authentication = jwtTokenService.getAuthentication(reissuedAccessToken);
jwtTokenService.saveRefreshToken(authentication.getName(), jwtTokenService.generateRefreshToken(response, authentication));
setAuthenticationToContext(jwtTokenService.generateAccessToken(response, authentication));
} catch (Exception e) { } catch (Exception e) {
jwtTokenService.deleteCookie(response); jwtTokenService.deleteCookie(response);
request.setAttribute(EXCEPTION_ATTRIBUTE, e); request.setAttribute(EXCEPTION_ATTRIBUTE, e);
} finally {
filterChain.doFilter(request, response);
} }
filterChain.doFilter(request, response);
} }
@@ -101,4 +94,11 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
.orElse(jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX)); .orElse(jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX));
} }
private boolean isPermittedURI(String requestURI) {
return Arrays.stream(PermittedURI.values())
.map(PermittedURI::getUri)
.anyMatch(uri -> uri.equals(requestURI)) &&
!PermittedURI.ROOT_URI.getUri().equals(requestURI);
}
} }

View File

@@ -2,16 +2,24 @@ package com.spring.infra.security.handler;
import java.io.IOException; import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.HandlerExceptionResolver;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
import lombok.RequiredArgsConstructor;
/** /**
* JWT 인증에서 접근 거부 상황을 처리하는 핸들러 클래스입니다. * JWT 인증에서 접근 거부 상황을 처리하는 핸들러 클래스입니다.
* *
@@ -22,30 +30,29 @@ import org.springframework.web.servlet.HandlerExceptionResolver;
* @version 1.0 * @version 1.0
*/ */
@Component @Component
@RequiredArgsConstructor
public class SecurityAccessDeniedHandler implements AccessDeniedHandler { public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
private final HandlerExceptionResolver resolver; private final ApplicationContext applicationContext;
public SecurityAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
/**
* 접근 거부 상황을 처리합니다.
*
* <p>사용자가 접근 권한이 없는 리소스에 접근을 시도할 때 호출됩니다.
* 이 메소드는 SC_FORBIDDEN (403) 상태 코드를 응답으로 전송합니다.</p>
*
* @param request 현재 HTTP 요청
* @param response 현재 HTTP 응답
* @param accessDeniedException 발생한 접근 거부 예외
* @throws IOException 입출력 예외 발생 시
* @throws ServletException 서블릿 예외 발생 시
*/
@Override @Override
public void handle(HttpServletRequest request, HttpServletResponse response, public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException { AccessDeniedException accessDeniedException) throws IOException, ServletException {
resolver.resolveException(request, response, null, accessDeniedException); if (isApiRequest(request)) {
HandlerExceptionResolver resolver = applicationContext.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
resolver.resolveException(request, response, null, new SecurityAuthException(SecurityExceptionRule.USER_FORBIDDEN));
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpServletResponse.SC_FORBIDDEN);
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, SecurityExceptionRule.USER_FORBIDDEN.getMessage());
RequestDispatcher dispatcher = request.getRequestDispatcher("/error");
dispatcher.forward(request, response);
}
}
private boolean isApiRequest(HttpServletRequest request) {
String accept = request.getHeader(HttpHeaders.ACCEPT);
return accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE);
} }
} }

View File

@@ -35,8 +35,7 @@ public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoin
private final HttpRequestEndpointChecker endpointChecker; private final HttpRequestEndpointChecker endpointChecker;
@Override @Override
public void commence(HttpServletRequest request, HttpServletResponse response, public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
AuthenticationException authException) throws IOException, ServletException {
if (!endpointChecker.isEndpointExist(request)) { if (!endpointChecker.isEndpointExist(request)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND); response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else if (isApiRequest(request)) { } else if (isApiRequest(request)) {

View File

@@ -12,15 +12,15 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.HandlerExceptionResolver;
import lombok.RequiredArgsConstructor;
@Component @Component
@RequiredArgsConstructor
public class SigninFailureHandler implements AuthenticationFailureHandler { public class SigninFailureHandler implements AuthenticationFailureHandler {
@Qualifier("handlerExceptionResolver")
private final HandlerExceptionResolver resolver; private final HandlerExceptionResolver resolver;
public SigninFailureHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override @Override
public void onAuthenticationFailure( public void onAuthenticationFailure(
HttpServletRequest request, HttpServletRequest request,

View File

@@ -18,7 +18,7 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.spring.infra.security.domain.JwtUserPrincipal; import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.error.SecurityAuthException; import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule; import com.spring.infra.security.error.SecurityExceptionRule;
import com.spring.infra.security.service.RefreshTokenService; import com.spring.infra.security.service.RefreshTokenService;
@@ -173,7 +173,7 @@ public class JwtTokenService {
.map(String::valueOf) .map(String::valueOf)
.map(SimpleGrantedAuthority::new) .map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()); .collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(new JwtUserPrincipal(userId, userName), "", auths); return new UsernamePasswordAuthenticationToken(UserPrincipal.of(userId, userName), "", auths);
} }
/** /**

View File

@@ -1,32 +0,0 @@
package com.spring.web.advice;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import com.spring.infra.security.domain.JwtUserPrincipal;
import com.spring.infra.security.domain.UserPrincipal;
@ControllerAdvice(basePackages = "com.spring.web.controller")
public class GlobalControllerAdvice {
@ModelAttribute("userInfo")
public Map<String, String> userInfo() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Map<String, String> userInfo = new HashMap<>();
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof UserPrincipal) {
JwtUserPrincipal user = (JwtUserPrincipal) auth.getPrincipal();
userInfo.put("userId", user.getUserId());
userInfo.put("userName", user.getUsername());
} else {
userInfo.put("userId", "");
userInfo.put("userName", "");
}
return userInfo;
}
}

View File

@@ -0,0 +1,46 @@
package com.spring.web.constant;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.spring.domain.user.entity.AgentUserRole;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Menus {
DASHBOARD(
"/dashboard",
"Dashboard",
"bi bi-grid",
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN, AgentUserRole.ROLE_USER)
),
SCHEDULE(
"/schedule",
"Schedule",
"bi bi-menu-button-wide",
List.of(AgentUserRole.ROLE_SUPER, AgentUserRole.ROLE_ADMIN)
),
USER_MANAGEMENT(
"/user/management",
"User Management",
"bi bi-person",
List.of(AgentUserRole.ROLE_SUPER)
);
private final String menuUri;
private final String menuName;
private final String menuIcon;
private final List<AgentUserRole> roles;
public static List<Menus> fromRole(AgentUserRole role) {
return Arrays.stream(values())
.filter(menu -> menu.roles.contains(role))
.collect(Collectors.toList());
}
}

View File

@@ -1,5 +1,6 @@
package com.spring.web.controller; package com.spring.web.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -9,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
public class ScheduleController { public class ScheduleController {
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('SUPER', 'ADMIN')")
public String schedule() { public String schedule() {
return "pages/schedule/schedule"; return "pages/schedule/schedule";
} }

View File

@@ -1,8 +1,6 @@
package com.spring.web.controller; package com.spring.web.controller;
import java.util.List; import org.springframework.security.access.prepost.PreAuthorize;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -15,11 +13,9 @@ import com.spring.domain.user.entity.AgentUserRole;
public class UserController { public class UserController {
@GetMapping("/management") @GetMapping("/management")
@PreAuthorize("hasAnyRole('SUPER')")
public String management(Model model) { public String management(Model model) {
List<AgentUserRole> roles = List.of(AgentUserRole.values()).stream() model.addAttribute("roles", AgentUserRole.values());
.filter(role -> !role.getRole().equals(AgentUserRole.ROLE_SUPER.name()))
.collect(Collectors.toList());
model.addAttribute("roles", roles);
return "pages/user/user-management"; return "pages/user/user-management";
} }

View File

@@ -0,0 +1,47 @@
package com.spring.web.support;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import com.spring.domain.user.entity.AgentUserRole;
import com.spring.infra.security.domain.UserPrincipal;
import com.spring.web.constant.Menus;
@ControllerAdvice(basePackages = "com.spring.web.controller")
public class GlobalControllerAdvice {
@ModelAttribute("userInfo")
public Map<String, String> userInfo() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof UserPrincipal) {
UserPrincipal user = (UserPrincipal) auth.getPrincipal();
return Map.of("userId", user.getUsername(), "userName", user.getMemberName());
}
return Map.of("userId", "", "userName", "");
}
@ModelAttribute("menus")
public List<Map<String, String>> getMenus() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
AgentUserRole userRole = auth.getAuthorities().stream()
.map(role -> role.getAuthority())
.map(AgentUserRole::fromRole)
.findFirst()
.orElse(AgentUserRole.ROLE_USER);
return Menus.fromRole(userRole).stream()
.map(menu -> Map.of("menuUri", menu.getMenuUri(), "menuName", menu.getMenuName(), "menuIcon", menu.getMenuIcon()))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
}

View File

@@ -10,8 +10,8 @@ const userService = {
await apiClient.put('/api/user/change-role-approve', users); await apiClient.put('/api/user/change-role-approve', users);
}, },
deleteUser: async (userId) => { deleteUser: async (id) => {
await apiClient.delete(`/api/user/${userId}`); await apiClient.delete(`/api/user/${id}`);
} }
}; };

View File

@@ -4,4 +4,21 @@ export const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return '-'; if (!dateTimeString) return '-';
const date = new Date(dateTimeString); const date = new Date(dateTimeString);
return dayjs(date).format("YYYY-MM-DD ddd A HH:mm:ss"); return dayjs(date).format("YYYY-MM-DD ddd A HH:mm:ss");
}
export const getModifiedRows = (arr1, arr2, keyField) => {
const modifiedRows = [];
const map1 = new Map(arr1.map(item => [item[keyField], item]));
arr2.forEach(item2 => {
const item1 = map1.get(item2[keyField]);
if (item1 && !deepEqualSelectedFields(item1, item2)) {
modifiedRows.push(item2);
}
});
return modifiedRows;
}
export const deepEqualSelectedFields = (obj1, obj2) => {
const keysToCompare = Object.keys(obj2);
return keysToCompare.every(key => obj1[key] === obj2[key]);
} }

View File

@@ -1,3 +1,4 @@
import { getModifiedRows } from '../../common/common.js';
import userService from '../../apis/user-api.js'; import userService from '../../apis/user-api.js';
let users = []; let users = [];
@@ -12,7 +13,12 @@ const setupEventListeners = () => {
e.preventDefault(); e.preventDefault();
fetchDataAndRender(); fetchDataAndRender();
}); });
document.getElementById('saveChangesBtn').addEventListener('click', saveChanges); document.getElementById('updateUserBtn').addEventListener('click', () => {
const confirmUpdate = confirm('회원정보를 수정하시겠습니까?');
if (confirmUpdate) {
updateUser();
}
});
}; };
const fetchDataAndRender = async () => { const fetchDataAndRender = async () => {
@@ -24,49 +30,58 @@ const fetchDataAndRender = async () => {
const updateTable = (users) => { const updateTable = (users) => {
const tableBody = document.querySelector('tbody'); const tableBody = document.querySelector('tbody');
tableBody.innerHTML = users.map(user => ` tableBody.innerHTML = users.map(user => `
<tr> <tr data-key="${user.id}">
<td class="align-middle">${user.userId}</td> <td class="align-middle">${user.userId}</td>
<td class="align-middle">${user.userName}</td> <td class="align-middle">${user.userName}</td>
<td class="align-middle">${user.email}</td> <td class="align-middle">${user.email}</td>
<td class="align-middle"> <td class="align-middle">
<select class="form-select form-select-sm" data-user-id="${user.id}" data-user-role="${user.userRole}"> <select id="userRole-${user.id}" class="form-select form-select-sm">
${ROLES.length > 0 ? ROLES.map(role => ` ${ROLES.length > 0 ? ROLES.map(role => `
<option value="${role}" ${user.userRole === role ? 'selected' : ''}>${role}</option> <option value="${role}" ${user.userRole === role ? 'selected' : ''}>${role}</option>
`).join('') : '<option value=""></option>'} `).join('') : '<option value=""></option>'}
</select> </select>
</td> </td>
<td class="align-middle"> <td class="align-middle">
<input type="checkbox" ${user.approved ? 'checked' : ''} data-user-id="${user.id}" data-user-approved="${user.approved}" class="form-check-input"> <input id="approved-${user.id}" type="checkbox" ${user.approved ? 'checked' : ''} class="form-check-input">
</td> </td>
<td class="align-middle"> <td class="align-middle">
<button id="deleteUserBtn" onclick="deleteUser('${user.id}')" class="btn btn-sm btn-outline-danger" data-bs-toggle="tooltip" data-bs-placement="left" title="사용자 삭제"> <button class="btn btn-sm btn-outline-danger delete-btn" data-id="${user.id}" title="사용자 삭제">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</td> </td>
</tr> </tr>
`).join(''); `).join('');
document.querySelectorAll('.delete-btn').forEach(btn => btn.addEventListener('click', (e) => {
const confirmUpdate = confirm('회원정보를 삭제하시겠습니까?');
if (confirmUpdate) {
const {id} = e.target.closest('button').dataset;
deleteUser(id);
}
}));
}; };
const saveChanges = async () => { const updateUser = async () => {
const updatedUsers = users.map(user => { const updatedUsers = Array.from(document.querySelectorAll('tbody tr')).map(row => {
const selectElement = document.querySelector(`select[data-user-id="${user.id}"]`); const id = row.dataset.key;
const checkboxElement = document.querySelector(`input[data-user-approved="${user.approved}"]`); const selectElement = row.querySelector(`#userRole-${id}`);
const checkboxElement = row.querySelector(`#approved-${id}`);
const userRole = selectElement ? selectElement.value : null; const userRole = selectElement ? selectElement.value : null;
const isApproved = checkboxElement ? checkboxElement.checked : false; const isApproved = checkboxElement ? checkboxElement.checked : false;
return { return {
id: user.id, id: id,
userRole: userRole, userRole: userRole,
isApproved: isApproved approved: isApproved
}; };
}); });
await userService.changeRoleApprove(updatedUsers); await userService.changeRoleApprove(getModifiedRows(users, updatedUsers, "id"));
alert('회원정보가 수정 되었습니다.'); alert('회원정보가 수정 되었습니다.');
fetchDataAndRender(); fetchDataAndRender();
}; };
const deleteUser = async (userId) => { const deleteUser = async (id) => {
await userService.deleteUser(userId); await userService.deleteUser(id);
alert('사용자가 삭제되었습니다.'); alert('사용자가 삭제되었습니다.');
fetchDataAndRender(); fetchDataAndRender();
}; };

View File

@@ -9,6 +9,7 @@
userId: /*[[${userInfo?.userId ?: ''}]]*/ '', userId: /*[[${userInfo?.userId ?: ''}]]*/ '',
userName: /*[[${userInfo?.userName ?: ''}]]*/ '' userName: /*[[${userInfo?.userName ?: ''}]]*/ ''
}; };
const MENUS = /*[[${menus}]]*/ '';
/*]]>*/ /*]]>*/
</script> </script>
<script th:src="@{/js/lib/axios/axios.min.js}"></script> <script th:src="@{/js/lib/axios/axios.min.js}"></script>

View File

@@ -2,14 +2,11 @@
<html xmlns:th="http://www.thymeleaf.org" th:fragment="sidebar" lang="ko" xml:lang="ko"> <html xmlns:th="http://www.thymeleaf.org" th:fragment="sidebar" lang="ko" xml:lang="ko">
<aside id="sidebar" class="sidebar"> <aside id="sidebar" class="sidebar">
<ul class="sidebar-nav" id="sidebar-nav"> <ul class="sidebar-nav" id="sidebar-nav">
<li class="nav-item"> <li class="nav-item" th:each="menu : ${menus}">
<a class="nav-link" href="/dashboard"><i class="bi bi-grid"></i><span>Dashboard</span></a> <a class="nav-link" th:href="${menu.menuUri}">
</li> <i th:class="${menu.menuIcon}"></i>
<li class="nav-item"> <span th:text="${menu.menuName}"></span>
<a class="nav-link" href="/schedule"><i class="bi bi-menu-button-wide"></i><span>Schedule</span></a> </a>
</li>
<li class="nav-item"></li>
<a class="nav-link" href="/user/management"><i class="bi bi-person"></i><span>User Management</span></a>
</li> </li>
</ul> </ul>
</aside> </aside>

View File

@@ -15,7 +15,7 @@
</div> </div>
<h1 class="display-4 fw-bold text-danger mb-4" th:text="${#strings.substring(message, 0, 3)}">500</h1> <h1 class="display-4 fw-bold text-danger mb-4" th:text="${#strings.substring(message, 0, 3)}">500</h1>
<h2 class="h4 text-secondary mb-4" th:text="${#strings.substring(message, 4)}">내부 서버 오류</h2> <h2 class="h4 text-secondary mb-4" th:text="${#strings.substring(message, 4)}">내부 서버 오류</h2>
<p class="text-muted mb-4">죄송합니다. 문제가 발생했습니다. 기술팀이 이 문제를 해결하기 위해 노력하고 있습니다.</p> <p class="text-muted mb-4" th:text="${errorMessage}">죄송합니다. 문제가 발생했습니다. 기술팀이 이 문제를 해결하기 위해 노력하고 있습니다.</p>
<a href="/" class="btn btn-primary btn-lg d-inline-flex align-items-center"> <a href="/" class="btn btn-primary btn-lg d-inline-flex align-items-center">
<i class="bi bi-house-door-fill me-2"></i>홈으로 돌아가기 <i class="bi bi-house-door-fill me-2"></i>홈으로 돌아가기
</a> </a>

View File

@@ -79,7 +79,7 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title d-flex justify-content-between align-items-center"> <h5 class="card-title d-flex justify-content-between align-items-center">
<span class="fs-6 text-dark"><i class="bi bi-list-ul"></i> 사용자 목록</span> <span class="fs-6 text-dark"><i class="bi bi-list-ul"></i> 사용자 목록</span>
<button id="saveChangesBtn" class="btn btn-sm btn-outline-primary" data-bs-toggle="tooltip" data-bs-placement="left" title="사용자 수정"> <button id="updateUserBtn" class="btn btn-sm btn-outline-primary" data-bs-toggle="tooltip" data-bs-placement="left" title="사용자 수정">
<i class="bi bi-pencil-fill"></i> <i class="bi bi-pencil-fill"></i>
</button> </button>
</h5> </h5>