Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement read giftcard-info API #128

Merged
merged 15 commits into from
Oct 19, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
"/auth/sign-up",
"/auth/sign-in",
"/auth/sign-in/**",
"/giftcards/**",
"/swagger-resources/**",
"/swagger-ui/**",
"/v3/api-docs/**",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.swmaestro.repl.gifthub.giftcard.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.encrypt.AesBytesEncryptor;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Component
@ConfigurationProperties(prefix = "giftcard")
@Getter
@Setter
public class GiftcardConfig {
private int effectiveDay;
private String secret;
private String salt;

@Bean
public AesBytesEncryptor aesBytesEncryptor() {
return new AesBytesEncryptor(secret, salt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.swmaestro.repl.gifthub.giftcard.contorller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.swmaestro.repl.gifthub.giftcard.dto.GiftcardResponseDto;
import org.swmaestro.repl.gifthub.giftcard.service.GiftcardService;
import org.swmaestro.repl.gifthub.util.BasicAuthenticationDecoder;
import org.swmaestro.repl.gifthub.util.Message;
import org.swmaestro.repl.gifthub.util.SuccessMessage;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/giftcards")
@RequiredArgsConstructor
@Tag(name = "GiftCard", description = "공유하기 관련 API")
public class GiftcardController {
private final GiftcardService giftcardService;

@GetMapping("/{id}")
@Operation(summary = "공유 정보 요청 메서드", description = "클라이언트에서 요청한 공유 정보를 전달하기 위한 메서드입니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "공유하기 정보 조회 성공"),
@ApiResponse(responseCode = "400", description = "만료된 공유하기 정보 접근"),
@ApiResponse(responseCode = "403", description = "일치하지 않는 비밀번호 입력"),
@ApiResponse(responseCode = "404", description = "존재하지 않는 공유하기 정보 접근")
})
public ResponseEntity<Message> read(HttpServletRequest request, @PathVariable String id) {
GiftcardResponseDto giftcardResponseDto = giftcardService.read(id, BasicAuthenticationDecoder.decode(request));
return ResponseEntity.ok(
SuccessMessage.builder()
.path(request.getRequestURI())
.data(giftcardResponseDto)
.build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.swmaestro.repl.gifthub.giftcard.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GiftcardRequestDto {
private String password;

@Builder
public GiftcardRequestDto(String password) {
this.password = password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.swmaestro.repl.gifthub.giftcard.dto;

import java.time.LocalDate;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GiftcardResponseDto {
private String sender;
private String message;
private String brandName;
private String productName;
private LocalDate expiresAt;

@Builder
public GiftcardResponseDto(String sender, String message, String brandName, String productName,
LocalDate expiresAt) {
this.sender = sender;
this.message = message;
this.brandName = brandName;
this.productName = productName;
this.expiresAt = expiresAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class Giftcard {
@Column(nullable = false)
private LocalDateTime expiresAt;

@Column(length = 4)
@Column(length = 60, nullable = false)
private String password;

@Builder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package org.swmaestro.repl.gifthub.giftcard.service;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Random;
import java.util.UUID;

import org.springframework.security.crypto.encrypt.AesBytesEncryptor;
import org.springframework.stereotype.Service;
import org.swmaestro.repl.gifthub.exception.BusinessException;
import org.swmaestro.repl.gifthub.giftcard.config.GiftcardConfig;
import org.swmaestro.repl.gifthub.giftcard.dto.GiftcardResponseDto;
import org.swmaestro.repl.gifthub.giftcard.entity.Giftcard;
import org.swmaestro.repl.gifthub.giftcard.repository.GiftcardRepository;
import org.swmaestro.repl.gifthub.util.ByteArrayUtils;
import org.swmaestro.repl.gifthub.util.StatusEnum;
import org.swmaestro.repl.gifthub.vouchers.dto.VoucherShareResponseDto;
import org.swmaestro.repl.gifthub.vouchers.entity.Voucher;
Expand All @@ -18,17 +23,20 @@
@RequiredArgsConstructor
public class GiftcardService {
private final GiftcardRepository giftCardRepository;
private final GiftcardConfig giftcardConfig;
private final AesBytesEncryptor aesBytesEncryptor;

public VoucherShareResponseDto create(Voucher voucher, String message) {
if (isExist(voucher.getId())) {
throw new BusinessException("이미 공유된 기프티콘입니다.", StatusEnum.BAD_REQUEST);
}

Giftcard giftCard = Giftcard.builder()
.id(generateUUID())
.voucher(voucher)
.password(generatePassword())
.password(encryptPassword(generatePassword()))
.message(message)
.expiresAt(LocalDateTime.now().plusDays(3))
.expiresAt(LocalDateTime.now().plusDays(giftcardConfig.getEffectiveDay()))
.build();
giftCardRepository.save(giftCard);

Expand All @@ -37,8 +45,40 @@ public VoucherShareResponseDto create(Voucher voucher, String message) {
.build();
}

public boolean isExist(Long id) {
return giftCardRepository.existsByVoucherId(id);
public Giftcard read(String id) {
if (!isExist(id)) {
throw new BusinessException("존재하지 않는 링크입니다.", StatusEnum.NOT_FOUND);
}

return giftCardRepository.findById(id).get();
}

public GiftcardResponseDto read(String id, String password) {
Giftcard giftcard = read(id);

if (giftcard.getExpiresAt().isBefore(LocalDateTime.now())) {
throw new BusinessException("만료된 링크입니다.", StatusEnum.BAD_REQUEST);
}

if (!decryptPassword(giftcard.getPassword()).equals(password)) {
throw new BusinessException("비밀번호가 일치하지 않습니다.", StatusEnum.FORBIDDEN);
}

return GiftcardResponseDto.builder()
.sender(giftcard.getVoucher().getUser().getNickname())
.message(giftcard.getMessage())
.brandName(giftcard.getVoucher().getBrand().getName())
.productName(giftcard.getVoucher().getProduct().getName())
.expiresAt(giftcard.getExpiresAt().toLocalDate())
.build();
}

public boolean isExist(String id) {
return giftCardRepository.existsById(id);
}

public boolean isExist(Long voucherId) {
return giftCardRepository.existsByVoucherId(voucherId);
}

public String generateUUID() {
Expand All @@ -49,4 +89,16 @@ public String generatePassword() {
int random = new Random().nextInt(10000);
return String.format("%04d", random);
}

private String decryptPassword(String password) {
byte[] bytes = ByteArrayUtils.stringToByteArray(password);
byte[] decrypt = aesBytesEncryptor.decrypt(bytes);
return new String(decrypt, StandardCharsets.UTF_8);
}

private String encryptPassword(String password) {
byte[] bytes = password.getBytes(StandardCharsets.UTF_8);
byte[] encrypt = aesBytesEncryptor.encrypt(bytes);
return ByteArrayUtils.byteArrayToString(encrypt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.swmaestro.repl.gifthub.util;

import java.util.Base64;

import jakarta.servlet.http.HttpServletRequest;

public class BasicAuthenticationDecoder {
public static String decode(HttpServletRequest request) {
String header = request.getHeader("Authorization");
return new String(Base64.getDecoder().decode(header.replace("Basic ", "")));
}
}
23 changes: 23 additions & 0 deletions src/main/java/org/swmaestro/repl/gifthub/util/ByteArrayUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.swmaestro.repl.gifthub.util;

import java.nio.ByteBuffer;

public class ByteArrayUtils {
public static String byteArrayToString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte abyte : bytes) {
sb.append(abyte);
sb.append(" ");
}
return sb.toString();
}

public static byte[] stringToByteArray(String byteString) {
String[] split = byteString.split("\\s");
ByteBuffer buffer = ByteBuffer.allocate(split.length);
for (String s : split) {
buffer.put((byte)Integer.parseInt(s));
}
return buffer.array();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.swmaestro.repl.gifthub.giftcard.contorller;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.time.LocalDate;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.swmaestro.repl.gifthub.giftcard.dto.GiftcardResponseDto;
import org.swmaestro.repl.gifthub.giftcard.service.GiftcardService;

import com.fasterxml.jackson.databind.ObjectMapper;

@SpringBootTest
@AutoConfigureMockMvc
class GiftcardControllerTest {
@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private GiftcardService giftcardService;

@Test
@WithMockUser(username = "test", roles = "USER")
void read() throws Exception {
// given
String giftcardId = "id";
String apiPath = "/giftcards/" + giftcardId;
String encodedPassword = "MDAwMA==";
String decodedPassword = "0000";

GiftcardResponseDto giftcardResponseDto = GiftcardResponseDto.builder()
.sender("보내는 사람")
.message("메시지")
.productName("상품명")
.brandName("브랜드명")
.expiresAt(LocalDate.now())
.build();

// when
when(giftcardService.read(giftcardId, decodedPassword)).thenReturn(giftcardResponseDto);

// then
mockMvc.perform(get(apiPath)
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Basic " + encodedPassword)
.content(objectMapper.writeValueAsString("test")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.path").value(apiPath))
.andExpect(jsonPath("$.data.sender").value(giftcardResponseDto.getSender()))
.andExpect(jsonPath("$.data.message").value(giftcardResponseDto.getMessage()))
.andExpect(jsonPath("$.data.product_name").value(giftcardResponseDto.getProductName()))
.andExpect(jsonPath("$.data.brand_name").value(giftcardResponseDto.getBrandName()))
.andExpect(jsonPath("$.data.expires_at").value(giftcardResponseDto.getExpiresAt().toString()));
}
}