Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@Profile("local")
@RestController
@RequiredArgsConstructor
@RequestMapping("/ai/test")
@RequestMapping("/debug/ai/test")
public class AiTestController {

private final AiBatchService aiBatchService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
import com.nova.nova_server.domain.auth.dto.AuthResponse;
import com.nova.nova_server.domain.auth.error.AuthErrorCode;
import com.nova.nova_server.domain.auth.util.JwtUtil;
import com.nova.nova_server.domain.member.dto.MemberRequestDto;
import com.nova.nova_server.domain.member.dto.MemberResponseDto;
import com.nova.nova_server.domain.member.entity.Member;
import com.nova.nova_server.domain.member.repository.MemberRepository;
import com.nova.nova_server.domain.member.service.MemberService;
import com.nova.nova_server.global.apiPayload.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -73,6 +72,19 @@ public ApiResponse<MemberResponseDto> createMockUser() {
return ApiResponse.success(member);
}

@Operation(
summary = "관리자 계정 생성",
description = "관리자 계정이 없을 때만 생성합니다. 소셜 계정은 연결되지 않습니다."
)
@PostMapping("/create-admin")
@Transactional
public ApiResponse<MemberResponseDto> createAdmin(
@RequestBody MemberRequestDto requestDto
) {
MemberResponseDto member = memberService.createAdminMember(requestDto);
return ApiResponse.success(member);
}

@Operation(
summary = "테스트 토큰 발급",
description = "요청한 사용자 ID로 7일 유효 테스트용 JWT를 발급합니다."
Expand All @@ -96,4 +108,23 @@ public ApiResponse<AuthResponse> generateTestToken(
.name(member.getName())
.build());
}

@Operation(
summary = "관리자 토큰 발급",
description = "등록된 관리자 계정으로 7일 유효 JWT를 발급합니다."
)
@PostMapping("/generate-admin-token")
public ApiResponse<AuthResponse> generateAdminToken() {
MemberResponseDto admin = memberService.getAdminMemberInfo();

long sevenDaysInMillis = 7L * 24 * 60 * 60 * 1000;
String token = jwtUtil.generateToken(admin.getId(), sevenDaysInMillis);

return ApiResponse.success(AuthResponse.builder()
.accessToken(token)
.memberId(admin.getId())
.email(admin.getEmail())
.name(admin.getName())
.build());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.nova.nova_server.domain.auth.filter;

import com.nova.nova_server.domain.member.entity.Member;
import com.nova.nova_server.domain.member.repository.MemberRepository;
import com.nova.nova_server.domain.auth.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -12,6 +14,7 @@
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

Expand All @@ -24,23 +27,27 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final MemberRepository memberRepository;

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String token = resolveToken(request);

if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
try {
Long memberId = jwtUtil.getMemberIdFromToken(token);
if (memberId == null) {
filterChain.doFilter(request, response);
return;
}

// memberId만 Principal로 설정 (DB 조회 없음)
Authentication authentication = createAuthentication(memberId);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
Expand All @@ -59,11 +66,15 @@ private String resolveToken(HttpServletRequest request) {
return null;
}

private Authentication createAuthentication(Long memberId) {
private Authentication createAuthentication(@NonNull Long memberId) {
Member.MemberRole role = memberRepository.findById(memberId)
.map(member -> member.getRole() != null ? member.getRole() : Member.MemberRole.USER)
.orElse(Member.MemberRole.USER);

return new UsernamePasswordAuthenticationToken(
memberId,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.name()))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public ApiResponse<Void> deleteMember(
@PathVariable("member_id") Long memberId,
@AuthenticationPrincipal Long authenticatedMemberId) {

memberService.deleteMember(memberId);
memberService.deleteMember(memberId, authenticatedMemberId);
return ApiResponse.success(null);
}

Expand Down Expand Up @@ -70,9 +70,10 @@ public ApiResponse<MemberPersonalizationDto> getPersonalization(
public ApiResponse<Void> updatePersonalization(
@Parameter(description = "사용자 ID")
@PathVariable Long memberId,
@AuthenticationPrincipal Long authenticatedMemberId,
@RequestBody MemberPersonalizationDto request) {

memberService.updateMemberPersonalization(memberId, request);
memberService.updateMemberPersonalization(memberId, authenticatedMemberId, request);
return ApiResponse.success(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

Expand All @@ -32,13 +32,14 @@ public class MemberProfileImageController {
public ApiResponse<Void> uploadProfileImage(
@Parameter(description = "이미지를 업로드할 사용자의 ID", example = "1")
@PathVariable Long memberId,
@AuthenticationPrincipal Long authenticatedMemberId,
@Parameter(
description = "업로드할 이미지 파일 (MultipartFile)",
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
)
@RequestParam("file") MultipartFile file) throws IOException {

memberProfileImageService.uploadProfileImageRaw(memberId, file);
memberProfileImageService.uploadProfileImageRaw(memberId, authenticatedMemberId, file);
return ApiResponse.success(null);
}

Expand All @@ -65,9 +66,10 @@ public ResponseEntity<byte[]> getProfileImage(
@DeleteMapping("/{memberId}/profile-image")
public ApiResponse<Void> deleteProfileImage(
@Parameter(description = "이미지를 삭제할 사용자의 ID", example = "1")
@PathVariable Long memberId) {
@PathVariable Long memberId,
@AuthenticationPrincipal Long authenticatedMemberId) {

memberProfileImageService.deleteProfileImage(memberId);
memberProfileImageService.deleteProfileImage(memberId, authenticatedMemberId);
return ApiResponse.success(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ public class MemberResponseDto {
private String name;
private String email;
private String profileImage;
private Member.MemberRole role;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@
import com.nova.nova_server.domain.cardNews.entity.CardNewsBookmark;
import com.nova.nova_server.domain.cardNews.entity.CardNewsRelevance;
import com.nova.nova_server.domain.common.BaseEntity;
import com.nova.nova_server.domain.member.dto.MemberConnectedAccountsResponseDto;
import com.nova.nova_server.global.apiPayload.ApiResponse;
import com.nova.nova_server.global.apiPayload.exception.NovaException;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -40,6 +36,12 @@ public class Member extends BaseEntity {
@Column(nullable = false, length = 100)
private String name;

@Setter
@Builder.Default
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private MemberRole role = MemberRole.USER;

@Column(nullable = true, unique = false, length = 255)
private String email;

Expand Down Expand Up @@ -75,6 +77,11 @@ public enum MemberLevel {
ADVANCED // 숙련자
}

public enum MemberRole {
USER,
ADMIN
}

public void connectGoogle(String googleId) {
this.googleId = googleId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
public enum MemberErrorCode implements ErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER4040", "존재하지 않는 사용자입니다."),
ADMIN_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER4041", "관리자 계정이 존재하지 않습니다."),
ADMIN_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4090", "이미 관리자 계정이 존재합니다."),
MEMBER_READ_FORBIDDEN(HttpStatus.FORBIDDEN, "MEMBER4030", "본인의 정보만 조회할 수 있습니다."),
MEMBER_FORBIDDEN(HttpStatus.FORBIDDEN, "MEMBER4030", "본인의 정보만 수정할 수 있습니다."),
MEMBER_DELETE_FORBIDDEN(HttpStatus.FORBIDDEN, "MEMBER4031", "일반 사용자는 본인 계정만 탈퇴할 수 있습니다."),
ADMIN_DELETE_ADMIN_FORBIDDEN(HttpStatus.FORBIDDEN, "MEMBER4032", "관리자 계정은 탈퇴시킬 수 없습니다."),
MEMBER_NAME_REQUIRED(HttpStatus.BAD_REQUEST, "MEMBER4000", "이름은 필수 입력 항목입니다.");

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByKakaoId(String kakaoId);
Optional<Member> findByGithubId(String githubId);
boolean existsByGithubId(String githubId);
boolean existsByRole(Member.MemberRole role);
Optional<Member> findByRole(Member.MemberRole role);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.nova.nova_server.domain.member.service;

import com.nova.nova_server.domain.member.dto.MemberResponseDto;
import com.nova.nova_server.domain.member.entity.Member;
import com.nova.nova_server.domain.member.entity.MemberProfileImage;
import com.nova.nova_server.domain.member.error.MemberErrorCode;
Expand All @@ -10,7 +9,6 @@
import com.nova.nova_server.global.apiPayload.exception.NovaException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -27,10 +25,15 @@ public class MemberProfileImageService {
private final MemberProfileImageRepository memberProfileImageRepository;

@Transactional
public void uploadProfileImageRaw(Long memberId, MultipartFile file) throws IOException {
public void uploadProfileImageRaw(Long memberId, Long authenticatedMemberId, MultipartFile file) throws IOException {
Member authenticatedMember = memberRepository.findById(authenticatedMemberId)
.orElseThrow(() -> new NovaException(MemberErrorCode.MEMBER_NOT_FOUND));

Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NovaException(MemberErrorCode.MEMBER_NOT_FOUND));

validateProfileImagePermission(authenticatedMember, member);

log.info("Original size: {} bytes", file.getSize());

byte[] compressedImage = imageProcessor.compressImage(file);
Expand All @@ -51,11 +54,28 @@ public Optional<byte[]> getProfileImageRaw(Long memberId) {
}

@Transactional
public void deleteProfileImage(Long memberId) {
public void deleteProfileImage(Long memberId, Long authenticatedMemberId) {
Member authenticatedMember = memberRepository.findById(authenticatedMemberId)
.orElseThrow(() -> new NovaException(MemberErrorCode.MEMBER_NOT_FOUND));

Member targetMember = memberRepository.findById(memberId)
.orElseThrow(() -> new NovaException(MemberErrorCode.MEMBER_NOT_FOUND));

validateProfileImagePermission(authenticatedMember, targetMember);

if (!memberProfileImageRepository.existsById(memberId)) {
throw new NovaException(MemberErrorCode.MEMBER_NOT_FOUND);
}

memberProfileImageRepository.deleteById(memberId);
}

private void validateProfileImagePermission(Member authenticatedMember, Member targetMember) {
boolean isAdmin = authenticatedMember.getRole() == Member.MemberRole.ADMIN;
boolean isSelf = authenticatedMember.getId().equals(targetMember.getId());

if (!isAdmin && !isSelf) {
throw new NovaException(MemberErrorCode.MEMBER_FORBIDDEN);
}
}
}
Loading