Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 36 additions & 36 deletions techeerzip/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ dependencies {

// GraphQL 클라이언트 (WebFlux)
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// macOS DNS resolver for Netty
runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.114.Final:osx-aarch_64'
}
Expand Down Expand Up @@ -190,43 +190,43 @@ def jacocoExcluded = [
'**/global/**' // Global 패키지
]

// jacocoReport
tasks.named('jacocoTestReport', JacocoReport) {
dependsOn test
reports {
xml.required = true
html.required = true
}
classDirectories.setFrom(files(
classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) }
))
}

// jacocoCoverage 확인
tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) {
classDirectories.setFrom(files(
classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) }
))

violationRules {
rule {
element = 'CLASS'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.00
}
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.00
}
}
}
}
// jacocoReport
tasks.named('jacocoTestReport', JacocoReport) {
dependsOn test
reports {
xml.required = true
html.required = true
}
classDirectories.setFrom(files(
classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) }
))
}

// jacocoCoverage 확인
tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) {
classDirectories.setFrom(files(
classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) }
))

violationRules {
rule {
element = 'CLASS'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.00
}
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.00
}
}
}
}

// 테스트 task 마무리 설정
tasks.test {
useJUnitPlatform()
finalizedBy tasks.jacocoTestReport
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package backend.techeerzip.domain.role.entity;

public enum RoleType {
ADMIN("ROLE_ADMIN"),
MENTOR("ROLE_MENTOR"),
TECHEER("ROLE_TECHEER"),
COMPANY("ROLE_COMPANY"),
BOOTCAMP("ROLE_BOOTCAMP");
ADMIN("ROLE_ADMIN", 1L),
MENTOR("ROLE_MENTOR", 2L),
TECHEER("ROLE_TECHEER", 3L),
COMPANY("ROLE_COMPANY", 4L),
BOOTCAMP("ROLE_BOOTCAMP", 5L);

private final String roleName;
private final Long roleId;

RoleType(String roleName) {
RoleType(String roleName, Long roleId) {
this.roleName = roleName;
this.roleId = roleId;
}

public String getRoleName() {
return roleName;
}

public Long getRoleId() {
return roleId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
indexes = {
@Index(
name = "idx_WeeklyGitContributions_user_year_month_week",
columnList = "userId year month week"),
columnList = "userId,year,month,week"),
@Index(
name = "idx_WeeklyGitContributions_user_weekStart",
columnList = "userId weekStart")
columnList = "userId,weekStart")
})
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public ResponseEntity<Void> signupExternal(
}

@DeleteMapping(value = "")
@PreAuthorize("hasPermission(#userId, 'User', 'DELETE')")
@Override
public ResponseEntity<Void> deleteUser(
@Valid @Parameter(hidden = true) @CurrentUser Long userId,
Expand Down Expand Up @@ -190,6 +191,7 @@ public ResponseEntity<GetUserResponse> getProfile(@PathVariable Long userId) {
}

@PatchMapping("")
@PreAuthorize("hasPermission(#userId, 'User', 'UPDATE')")
@Override
public ResponseEntity<Void> updateProfile(
@Valid @Parameter(hidden = true) @CurrentUser Long userId,
Expand All @@ -201,6 +203,7 @@ public ResponseEntity<Void> updateProfile(
}

@DeleteMapping("/experience/{experienceId}")
@PreAuthorize("hasPermission(#experienceId, 'UserExperience', 'DELETE')")
@Override
public ResponseEntity<Void> deleteExperience(@PathVariable Long experienceId) {
logger.info("경력 삭제 요청 처리 중 - experienceId: {}", experienceId, CONTEXT);
Expand All @@ -210,6 +213,7 @@ public ResponseEntity<Void> deleteExperience(@PathVariable Long experienceId) {
}

@PatchMapping("/nickname")
@PreAuthorize("hasPermission(#userId, 'User', 'UPDATE_NICKNAME')")
@Override
public ResponseEntity<Void> updateNickname(
@Valid @Parameter(hidden = true) @CurrentUser Long userId,
Expand Down Expand Up @@ -261,6 +265,7 @@ public ResponseEntity<BootcampMemberListResponse> getBootcampMemberProfiles(
}

@PatchMapping(value = "/techeer", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasPermission(#userId, 'User', 'CHANGE_TECHEER')")
@Override
public ResponseEntity<Void> changeTecheer(
@RequestPart("file") MultipartFile file,
Expand All @@ -272,6 +277,7 @@ public ResponseEntity<Void> changeTecheer(
}

@PostMapping("/github/username")
@PreAuthorize("hasPermission(#userId, 'User', 'UPDATE_GITHUB')")
@Override
public ResponseEntity<Void> syncGithubData(
@CurrentUser Long userId, @Valid @RequestBody UpdateGithubUrlRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

import backend.techeerzip.domain.user.entity.PermissionRequest;
import backend.techeerzip.global.entity.StatusCategory;

@Transactional
public interface PermissionRequestRepository extends JpaRepository<PermissionRequest, Long> {
@Transactional(readOnly = true)
List<PermissionRequest> findByStatus(StatusCategory status);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(
"UPDATE PermissionRequest p SET p.status = :status WHERE p.user.id = :userId AND p.status = backend.techeerzip.global.entity.StatusCategory.PENDING")
int updateStatusByUserId(@Param("userId") Long userId, @Param("status") StatusCategory status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

@Repository
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
Optional<User> findByEmail(String email);
@Query("SELECT u FROM User u WHERE u.email = :email AND u.isDeleted = false")
Optional<User> findByEmail(@Param("email") String email);

boolean existsByEmail(String email);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
import backend.techeerzip.domain.user.exception.UserNotFoundException;
import backend.techeerzip.domain.user.exception.UserNotResumeException;
import backend.techeerzip.domain.user.exception.UserProfileImgFailException;
import backend.techeerzip.domain.user.exception.UserUnauthorizedAdminException;
import backend.techeerzip.domain.user.mapper.TechStackMapper;
import backend.techeerzip.domain.user.mapper.UserMapper;
import backend.techeerzip.domain.user.repository.PermissionRequestRepository;
Expand Down Expand Up @@ -222,10 +221,17 @@ public void signUp(
TaskType.SIGNUP_BLOG_FETCH, savedUser.getId(), blogUrls);
}

userRepository.flush();

User managedUser =
userRepository
.findById(savedUser.getId())
.orElseThrow(() -> new UserNotFoundException());

CreateResumeRequest resumeRequest = createUserWithResumeRequest.getCreateResumeRequest();
// 이력서 저장
resumeService.createResumeByUser(
savedUser,
managedUser,
resumeFile,
resumeRequest.getTitle(),
resumeRequest.getPosition(),
Expand Down Expand Up @@ -666,19 +672,13 @@ public void deleteExperience(Long experienceId) {
userExperienceRepository
.findById(experienceId)
.orElseThrow(UserExperienceNotFoundException::new);
userExperienceRepository.delete(experience);
experience.delete();
userExperienceRepository.save(experience);
}

@Transactional
public void updateNickname(Long userId, String nickname) {
User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new);

Long roleId = user.getRole().getId();
if (roleId == 3) {
logger.warn("권한 없음 - userId: {}", userId);
throw new UserUnauthorizedAdminException();
}

user.setNickname(nickname);
userRepository.save(user);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

import backend.techeerzip.domain.userExperience.entity.UserExperience;

@Transactional
public interface UserExperienceRepository extends JpaRepository<UserExperience, Long> {
@Modifying
@Query("UPDATE UserExperience ue SET ue.isDeleted = true WHERE ue.user.id = :userId")
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE UserExperience ue SET ue.isDeleted = true WHERE ue.userId = :userId")
void updateIsDeletedByUserId(@Param("userId") Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package backend.techeerzip.global.permission;

import java.io.Serializable;

import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import backend.techeerzip.domain.auth.jwt.CustomUserPrincipal;
import backend.techeerzip.domain.userExperience.entity.UserExperience;
import backend.techeerzip.domain.userExperience.exception.UserExperienceNotFoundException;
import backend.techeerzip.domain.userExperience.repository.UserExperienceRepository;
import backend.techeerzip.global.exception.PermissionDeniedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class UserExperiencePermissionEvaluator implements DomainPermissionEvaluator {

private final UserExperienceRepository userExperienceRepository;

@Override
public boolean supports(String targetType) {
return "UserExperience".equals(targetType);
}

@Override
@Transactional(readOnly = true)
public boolean hasPermission(
Authentication auth, Serializable targetId, String targetType, String permission) {
if (!supports(targetType)) {
return false;
}

CustomUserPrincipal user = (CustomUserPrincipal) auth.getPrincipal();
Long userId = user.getUserId();
Long experienceId = (Long) targetId;

if (!isSupportedPermission(permission)) {
return false;
}

try {
// 경력이 존재하는지 확인
UserExperience experience =
userExperienceRepository
.findById(experienceId)
.orElseThrow(UserExperienceNotFoundException::new);

// 본인의 경력인지 확인
if (!userId.equals(experience.getUserId())) {
log.warn(
"UserExperience 권한 없음 - userId: {}, experienceId: {}, experienceUserId: {}, permission: {}",
userId,
experienceId,
experience.getUserId(),
permission);
throw new PermissionDeniedException();
}

log.info(
"UserExperience 권한 확인 완료 - userId: {}, experienceId: {}, permission: {}",
userId,
experienceId,
permission);
return true;
} catch (PermissionDeniedException e) {
throw e;
} catch (Exception e) {
log.error(
"UserExperience 권한 검사 중 오류 - experienceId: {}, error: {}",
experienceId,
e.getMessage());
throw new PermissionDeniedException();
}
}

private boolean isSupportedPermission(String permission) {
return switch (permission) {
case "DELETE" -> true;
default -> false;
};
}
}
Loading
Loading