feat(settings): 관리자 설정 기반 신규 사용자 자동승인 + 기본 롤 부여
- AppSetting 엔티티 + Repository (key-value 설정 저장소) - SettingsService (자동승인 조회/수정) - AdminSettingsController (GET/PUT /api/admin/settings/registration) - Role.defaultGrant 컬럼 + AdminRoleController default-grant 토글 - AuthController: 신규 사용자 생성 시 자동승인 + 기본롤 부여 로직 - data.sql: WING_PERMIT 롤 시드 + auto-approve 설정 시드 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
04f3de3890
커밋
ce6e88e221
@ -3,9 +3,12 @@ package com.gcsc.guide.auth;
|
||||
import com.gcsc.guide.dto.AuthResponse;
|
||||
import com.gcsc.guide.dto.GoogleLoginRequest;
|
||||
import com.gcsc.guide.dto.UserResponse;
|
||||
import com.gcsc.guide.entity.Role;
|
||||
import com.gcsc.guide.entity.User;
|
||||
import com.gcsc.guide.repository.RoleRepository;
|
||||
import com.gcsc.guide.repository.UserRepository;
|
||||
import com.gcsc.guide.service.ActivityService;
|
||||
import com.gcsc.guide.service.SettingsService;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
@ -18,6 +21,10 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -36,7 +43,9 @@ public class AuthController {
|
||||
private final GoogleTokenVerifier googleTokenVerifier;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserRepository userRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
private final ActivityService activityService;
|
||||
private final SettingsService settingsService;
|
||||
|
||||
@Operation(summary = "Google 로그인",
|
||||
description = "Google ID Token을 검증하고 JWT를 발급합니다. "
|
||||
@ -119,6 +128,16 @@ public class AuthController {
|
||||
newUser.activate();
|
||||
newUser.grantAdmin();
|
||||
log.info("관리자 자동 승인: {}", email);
|
||||
} else if (settingsService.isAutoApproveEnabled()) {
|
||||
newUser.activate();
|
||||
log.info("자동 승인 (설정 활성화): {}", email);
|
||||
}
|
||||
|
||||
List<Role> defaultRoles = roleRepository.findByDefaultGrantTrue();
|
||||
if (!defaultRoles.isEmpty()) {
|
||||
newUser.updateRoles(new HashSet<>(defaultRoles));
|
||||
log.info("기본 롤 부여: {} → {}", email,
|
||||
defaultRoles.stream().map(Role::getName).toList());
|
||||
}
|
||||
|
||||
newUser.updateLastLogin();
|
||||
|
||||
@ -3,6 +3,7 @@ package com.gcsc.guide.controller;
|
||||
import com.gcsc.guide.dto.AddPermissionRequest;
|
||||
import com.gcsc.guide.dto.CreateRoleRequest;
|
||||
import com.gcsc.guide.dto.RoleResponse;
|
||||
import com.gcsc.guide.dto.UpdateDefaultGrantRequest;
|
||||
import com.gcsc.guide.service.RoleService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@ -119,6 +120,22 @@ public class AdminRoleController {
|
||||
return ResponseEntity.ok(roleService.addPermission(id, request.urlPattern()));
|
||||
}
|
||||
|
||||
@Operation(summary = "롤 기본 부여 토글",
|
||||
description = "신규 가입자에게 해당 롤을 기본 부여할지 설정합니다. defaultGrant=true인 롤은 첫 로그인 시 자동 할당됩니다.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "수정 성공",
|
||||
content = @Content(schema = @Schema(implementation = RoleResponse.class))),
|
||||
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
|
||||
@ApiResponse(responseCode = "403", description = "관리자 권한 필요", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "롤을 찾을 수 없음", content = @Content)
|
||||
})
|
||||
@PutMapping("/{id}/default-grant")
|
||||
public ResponseEntity<RoleResponse> updateDefaultGrant(
|
||||
@Parameter(description = "롤 ID", required = true) @PathVariable Long id,
|
||||
@Valid @RequestBody UpdateDefaultGrantRequest request) {
|
||||
return ResponseEntity.ok(roleService.updateDefaultGrant(id, request.defaultGrant()));
|
||||
}
|
||||
|
||||
@Operation(summary = "URL 패턴 삭제",
|
||||
description = "특정 URL 패턴(권한)을 삭제합니다.")
|
||||
@ApiResponses({
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
package com.gcsc.guide.controller;
|
||||
|
||||
import com.gcsc.guide.dto.RegistrationSettingsResponse;
|
||||
import com.gcsc.guide.dto.UpdateRegistrationSettingsRequest;
|
||||
import com.gcsc.guide.service.SettingsService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/settings")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "07. 관리자 - 설정", description = "신규 가입 자동승인, 기본 롤 부여 등 시스템 설정 관리")
|
||||
@SecurityRequirement(name = "Bearer JWT")
|
||||
public class AdminSettingsController {
|
||||
|
||||
private final SettingsService settingsService;
|
||||
|
||||
@Operation(summary = "가입 설정 조회",
|
||||
description = "자동 승인 여부와 기본 부여 롤 목록을 조회합니다.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "조회 성공",
|
||||
content = @Content(schema = @Schema(implementation = RegistrationSettingsResponse.class))),
|
||||
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
|
||||
@ApiResponse(responseCode = "403", description = "관리자 권한 필요", content = @Content)
|
||||
})
|
||||
@GetMapping("/registration")
|
||||
public ResponseEntity<RegistrationSettingsResponse> getRegistrationSettings() {
|
||||
return ResponseEntity.ok(settingsService.getRegistrationSettings());
|
||||
}
|
||||
|
||||
@Operation(summary = "가입 설정 수정",
|
||||
description = "자동 승인 여부를 변경합니다. true로 설정하면 신규 가입자가 즉시 ACTIVE 상태가 됩니다.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "수정 성공",
|
||||
content = @Content(schema = @Schema(implementation = RegistrationSettingsResponse.class))),
|
||||
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
|
||||
@ApiResponse(responseCode = "403", description = "관리자 권한 필요", content = @Content)
|
||||
})
|
||||
@PutMapping("/registration")
|
||||
public ResponseEntity<RegistrationSettingsResponse> updateRegistrationSettings(
|
||||
@Valid @RequestBody UpdateRegistrationSettingsRequest request) {
|
||||
return ResponseEntity.ok(settingsService.updateAutoApprove(request.autoApprove()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.gcsc.guide.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RegistrationSettingsResponse(
|
||||
boolean autoApprove,
|
||||
List<RoleResponse> defaultRoles
|
||||
) {}
|
||||
@ -9,7 +9,8 @@ public record RoleResponse(
|
||||
Long id,
|
||||
String name,
|
||||
String description,
|
||||
List<String> urlPatterns
|
||||
List<String> urlPatterns,
|
||||
boolean defaultGrant
|
||||
) {
|
||||
|
||||
public static RoleResponse from(Role role) {
|
||||
@ -21,7 +22,8 @@ public record RoleResponse(
|
||||
role.getId(),
|
||||
role.getName(),
|
||||
role.getDescription(),
|
||||
patterns
|
||||
patterns,
|
||||
role.isDefaultGrant()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
package com.gcsc.guide.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record UpdateDefaultGrantRequest(
|
||||
@NotNull Boolean defaultGrant
|
||||
) {}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.gcsc.guide.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record UpdateRegistrationSettingsRequest(
|
||||
@NotNull Boolean autoApprove
|
||||
) {}
|
||||
46
src/main/java/com/gcsc/guide/entity/AppSetting.java
Normal file
46
src/main/java/com/gcsc/guide/entity/AppSetting.java
Normal file
@ -0,0 +1,46 @@
|
||||
package com.gcsc.guide.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "app_settings")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class AppSetting {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "setting_key", nullable = false, unique = true, length = 100)
|
||||
private String settingKey;
|
||||
|
||||
@Column(name = "setting_value", nullable = false, length = 500)
|
||||
private String settingValue;
|
||||
|
||||
@Column(length = 255)
|
||||
private String description;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public AppSetting(String settingKey, String settingValue, String description) {
|
||||
this.settingKey = settingKey;
|
||||
this.settingValue = settingValue;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void updateValue(String value) {
|
||||
this.settingValue = value;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,9 @@ public class Role {
|
||||
@Column(length = 255)
|
||||
private String description;
|
||||
|
||||
@Column(name = "default_grant")
|
||||
private boolean defaultGrant = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@ -40,6 +43,10 @@ public class Role {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void updateDefaultGrant(boolean defaultGrant) {
|
||||
this.defaultGrant = defaultGrant;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package com.gcsc.guide.repository;
|
||||
|
||||
import com.gcsc.guide.entity.AppSetting;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AppSettingRepository extends JpaRepository<AppSetting, Long> {
|
||||
|
||||
Optional<AppSetting> findBySettingKey(String settingKey);
|
||||
}
|
||||
@ -16,4 +16,6 @@ public interface RoleRepository extends JpaRepository<Role, Long> {
|
||||
|
||||
@Query("SELECT r FROM Role r LEFT JOIN FETCH r.urlPatterns WHERE r.id = :id")
|
||||
Optional<Role> findByIdWithUrlPatterns(Long id);
|
||||
|
||||
List<Role> findByDefaultGrantTrue();
|
||||
}
|
||||
|
||||
@ -81,6 +81,14 @@ public class RoleService {
|
||||
roleUrlPatternRepository.deleteById(permissionId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RoleResponse updateDefaultGrant(Long roleId, boolean defaultGrant) {
|
||||
Role role = roleRepository.findByIdWithUrlPatterns(roleId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("롤", roleId));
|
||||
role.updateDefaultGrant(defaultGrant);
|
||||
return RoleResponse.from(roleRepository.save(role));
|
||||
}
|
||||
|
||||
private Role findRoleById(Long roleId) {
|
||||
return roleRepository.findById(roleId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("롤", roleId));
|
||||
|
||||
47
src/main/java/com/gcsc/guide/service/SettingsService.java
Normal file
47
src/main/java/com/gcsc/guide/service/SettingsService.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.gcsc.guide.service;
|
||||
|
||||
import com.gcsc.guide.dto.RegistrationSettingsResponse;
|
||||
import com.gcsc.guide.dto.RoleResponse;
|
||||
import com.gcsc.guide.entity.AppSetting;
|
||||
import com.gcsc.guide.repository.AppSettingRepository;
|
||||
import com.gcsc.guide.repository.RoleRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SettingsService {
|
||||
|
||||
private static final String AUTO_APPROVE_KEY = "registration.auto-approve";
|
||||
|
||||
private final AppSettingRepository appSettingRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean isAutoApproveEnabled() {
|
||||
return appSettingRepository.findBySettingKey(AUTO_APPROVE_KEY)
|
||||
.map(s -> Boolean.parseBoolean(s.getSettingValue()))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RegistrationSettingsResponse getRegistrationSettings() {
|
||||
boolean autoApprove = isAutoApproveEnabled();
|
||||
List<RoleResponse> defaultRoles = roleRepository.findByDefaultGrantTrue().stream()
|
||||
.map(RoleResponse::from)
|
||||
.toList();
|
||||
return new RegistrationSettingsResponse(autoApprove, defaultRoles);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RegistrationSettingsResponse updateAutoApprove(boolean autoApprove) {
|
||||
AppSetting setting = appSettingRepository.findBySettingKey(AUTO_APPROVE_KEY)
|
||||
.orElseGet(() -> new AppSetting(AUTO_APPROVE_KEY, "false", "신규 가입자 자동 승인 여부"));
|
||||
setting.updateValue(String.valueOf(autoApprove));
|
||||
appSettingRepository.save(setting);
|
||||
return getRegistrationSettings();
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,17 @@
|
||||
-- 초기 롤 시드 데이터
|
||||
INSERT INTO roles (name, description, created_at) VALUES
|
||||
('ADMIN', '전체 접근 권한 (관리자 페이지 포함)', NOW()),
|
||||
('DEVELOPER', '전체 개발 가이드 접근', NOW()),
|
||||
('FRONT_DEV', '프론트엔드 개발 가이드만', NOW());
|
||||
INSERT INTO roles (name, description, default_grant, created_at) VALUES
|
||||
('ADMIN', '전체 접근 권한 (관리자 페이지 포함)', false, NOW()),
|
||||
('DEVELOPER', '전체 개발 가이드 접근', false, NOW()),
|
||||
('FRONT_DEV', '프론트엔드 개발 가이드만', false, NOW()),
|
||||
('WING_PERMIT', 'Wing 데모 사이트 접근 권한', true, NOW());
|
||||
|
||||
-- 롤별 URL 패턴
|
||||
INSERT INTO role_url_patterns (role_id, url_pattern, created_at) VALUES
|
||||
((SELECT id FROM roles WHERE name = 'ADMIN'), '/**', NOW()),
|
||||
((SELECT id FROM roles WHERE name = 'DEVELOPER'), '/dev/**', NOW()),
|
||||
((SELECT id FROM roles WHERE name = 'FRONT_DEV'), '/dev/front/**', NOW());
|
||||
((SELECT id FROM roles WHERE name = 'FRONT_DEV'), '/dev/front/**', NOW()),
|
||||
((SELECT id FROM roles WHERE name = 'WING_PERMIT'), '/wing/**', NOW());
|
||||
|
||||
-- 시스템 설정
|
||||
INSERT INTO app_settings (setting_key, setting_value, description, updated_at) VALUES
|
||||
('registration.auto-approve', 'true', '신규 가입자 자동 승인 여부', NOW());
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user