Skip to content

Commit

Permalink
Merge pull request #352 from UPbrella/dev
Browse files Browse the repository at this point in the history
release 0.4.5 release-dev
  • Loading branch information
birdieHyun authored Nov 5, 2023
2 parents adff973 + 1bad138 commit 5a547ee
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dev-pr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Upbrella DEV PR Test

on:
pull_request:
branches: [ "release-dev", "release-production"]
branches: [ "release-dev", "release-production" ]
permissions:
contents: read
pull-requests: write
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Spring Session Data Redis
implementation 'org.springframework.session:spring-session-data-redis'

// HOTP
implementation 'com.github.bastiaanjansen:otp-java:2.0.3'
}

ext {
Expand Down
1 change: 1 addition & 0 deletions src/docs/asciidoc/api/rent/rent.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ include::{snippets}/find-rental-form-doc/response-fields-data.adoc[]

include::{snippets}/find-return-form-doc/http-request.adoc[]
include::{snippets}/find-return-form-doc/path-parameters.adoc[]
include::{snippets}/find-return-form-doc/request-parameters.adoc[]

==== HTTP Response
include::{snippets}/find-return-form-doc/http-response.adoc[]
Expand Down
28 changes: 22 additions & 6 deletions src/main/java/upbrella/be/rent/controller/RentController.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import upbrella.be.rent.dto.response.*;
import upbrella.be.rent.service.ConditionReportService;
import upbrella.be.rent.service.ImprovementReportService;
import upbrella.be.rent.service.LockerService;
import upbrella.be.rent.service.RentService;
import upbrella.be.slack.service.SlackAlarmService;
import upbrella.be.user.dto.response.SessionUser;
Expand All @@ -20,6 +21,7 @@

import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.security.NoSuchAlgorithmException;

@Slf4j
@RestController
Expand All @@ -31,6 +33,7 @@ public class RentController {
private final RentService rentService;
private final UserService userService;
private final SlackAlarmService slackAlarmService;
private final LockerService lockerService;

@GetMapping("/rent/form/{umbrellaId}")
public ResponseEntity<CustomResponse<RentFormResponse>> findRentForm(@PathVariable long umbrellaId) {
Expand All @@ -48,12 +51,16 @@ public ResponseEntity<CustomResponse<RentFormResponse>> findRentForm(@PathVariab
}

@GetMapping("/return/form/{storeId}")
public ResponseEntity<CustomResponse<ReturnFormResponse>> findReturnForm(@PathVariable long storeId, HttpSession httpSession) {
public ResponseEntity<CustomResponse<ReturnFormResponse>> findReturnForm(
@PathVariable long storeId,
HttpSession httpSession,
@RequestParam(required = false) String salt,
@RequestParam(required = false) String signature) {

SessionUser user = (SessionUser) httpSession.getAttribute("user");
User userToReturn = userService.findUserById(user.getId());

ReturnFormResponse returnForm = rentService.findReturnForm(storeId, userToReturn);
ReturnFormResponse returnForm = rentService.findReturnForm(storeId, userToReturn, salt, signature);

return ResponseEntity
.ok()
Expand All @@ -66,25 +73,33 @@ public ResponseEntity<CustomResponse<ReturnFormResponse>> findReturnForm(@PathVa
}

@PostMapping("/rent")
public ResponseEntity<CustomResponse> rentUmbrellaByUser(@RequestBody @Valid RentUmbrellaByUserRequest rentUmbrellaByUserRequest, HttpSession httpSession) {
public ResponseEntity<CustomResponse<LockerPasswordResponse>> rentUmbrellaByUser(@RequestBody @Valid RentUmbrellaByUserRequest rentUmbrellaByUserRequest, HttpSession httpSession) {

SessionUser user = (SessionUser) httpSession.getAttribute("user");
User userToRent = userService.findUserById(user.getId());

LockerPasswordResponse lockerPasswordResponse = lockerService.findLockerPassword(rentUmbrellaByUserRequest);

rentService.addRental(rentUmbrellaByUserRequest, userToRent);

log.info("UBU 우산 대여 성공");

return ResponseEntity
.ok()
.body(new CustomResponse(
"success",
200,
"우산 대여 성공"
"우산 대여 성공",
lockerPasswordResponse
));
}


@PatchMapping("/rent")
public ResponseEntity<CustomResponse> returnUmbrellaByUser(@RequestBody @Valid ReturnUmbrellaByUserRequest returnUmbrellaByUserRequest, HttpSession httpSession) {
public ResponseEntity<CustomResponse> returnUmbrellaByUser(
@RequestBody @Valid
ReturnUmbrellaByUserRequest returnUmbrellaByUserRequest,
HttpSession httpSession) {

SessionUser user = (SessionUser) httpSession.getAttribute("user");
User userToReturn = userService.findUserById(user.getId());
Expand All @@ -103,7 +118,8 @@ public ResponseEntity<CustomResponse> returnUmbrellaByUser(@RequestBody @Valid R
}

@GetMapping("/admin/rent/histories")
public ResponseEntity<CustomResponse<RentalHistoriesPageResponse>> findRentalHistory(@ModelAttribute HistoryFilterRequest filter, Pageable pageable) {
public ResponseEntity<CustomResponse<RentalHistoriesPageResponse>> findRentalHistory
(@ModelAttribute HistoryFilterRequest filter, Pageable pageable) {

RentalHistoriesPageResponse histories = rentService.findAllHistories(filter, pageable);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import upbrella.be.rent.exception.*;
import upbrella.be.store.exception.NonExistingClassificationException;
import upbrella.be.util.CustomErrorResponse;

@RestControllerAdvice
Expand Down Expand Up @@ -69,4 +68,40 @@ public ResponseEntity<CustomErrorResponse> notAvailableUmbrellaException(NotAvai
e.getMessage()
));
}

@ExceptionHandler(LockerCodeAlreadyIssuedException.class)
public ResponseEntity<CustomErrorResponse> lockerCodeAlreadyIssuedException(LockerCodeAlreadyIssuedException e) {

return ResponseEntity
.status(429)
.body(new CustomErrorResponse(
"Too Many Requests",
429,
e.getMessage()
));
}

@ExceptionHandler(NoSignatureException.class)
public ResponseEntity<CustomErrorResponse> noSignatureException(NoSignatureException e) {

return ResponseEntity
.status(400)
.body(new CustomErrorResponse(
"Bad Request",
400,
e.getMessage()
));
}

@ExceptionHandler(LockerSignatureErrorException.class)
public ResponseEntity<CustomErrorResponse> lockerSignatureException(LockerSignatureErrorException e) {

return ResponseEntity
.status(403)
.body(new CustomErrorResponse(
"Forbidden",
403,
e.getMessage()
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package upbrella.be.rent.dto.response;

import lombok.Getter;

@Getter
public class LockerPasswordResponse {

private String password;

public LockerPasswordResponse(String password) {
this.password = password.substring(0, 4);
}
}
34 changes: 34 additions & 0 deletions src/main/java/upbrella/be/rent/entity/Locker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package upbrella.be.rent.entity;

import lombok.*;
import upbrella.be.store.entity.StoreMeta;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Locker {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@OneToOne
@JoinColumn(name = "store_meta_id")
private StoreMeta storeMeta;
private long count;
private String secretKey;
private LocalDateTime lastAccess;

public void updateCount() {
this.count += 1;
}

public void updateLastAccess(LocalDateTime now) {
this.lastAccess = now;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package upbrella.be.rent.exception;

public class LockerCodeAlreadyIssuedException extends RuntimeException {

public LockerCodeAlreadyIssuedException(String message) {

super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package upbrella.be.rent.exception;

public class LockerSignatureErrorException extends RuntimeException {

public LockerSignatureErrorException(String message) {

super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package upbrella.be.rent.exception;

public class NoSignatureException extends RuntimeException{

public NoSignatureException(String message) {

super(message);
}
}
11 changes: 11 additions & 0 deletions src/main/java/upbrella/be/rent/repository/LockerRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package upbrella.be.rent.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import upbrella.be.rent.entity.Locker;

import java.util.Optional;

public interface LockerRepository extends JpaRepository<Locker, Long> {

Optional<Locker> findByStoreMetaId(Long storeMetaId);
}
99 changes: 99 additions & 0 deletions src/main/java/upbrella/be/rent/service/LockerService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package upbrella.be.rent.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import upbrella.be.rent.dto.request.RentUmbrellaByUserRequest;
import upbrella.be.rent.dto.response.LockerPasswordResponse;
import upbrella.be.rent.entity.Locker;
import upbrella.be.rent.exception.LockerCodeAlreadyIssuedException;
import upbrella.be.rent.exception.LockerSignatureErrorException;
import upbrella.be.rent.exception.NoSignatureException;
import upbrella.be.rent.repository.LockerRepository;
import upbrella.be.util.HotpGenerator;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class LockerService {

private final LockerRepository lockerRepository;

@Transactional
public LockerPasswordResponse findLockerPassword(RentUmbrellaByUserRequest rentUmbrellaByUserRequest) {

Optional<Locker> lockerOptional = lockerRepository.findByStoreMetaId(rentUmbrellaByUserRequest.getStoreId());

return lockerOptional.map(this::getLockerPasswordResponse).orElse(null);
}

public void validateLockerSignature(Long storeId, String salt, String signature) {

Optional<Locker> lockerOptional = lockerRepository.findByStoreMetaId(storeId);

if (lockerOptional.isEmpty() && (salt != null || signature != null)) {
throw new NoSignatureException("구형 보관함은 salt와 signature가 없습니다.");
}

if (lockerOptional.isPresent()) {
Locker locker = lockerOptional.get();
String lockerSecretKey = locker.getSecretKey().toUpperCase();
salt = salt.toUpperCase();

validateSignature(signature, salt, lockerSecretKey);
}
}

private LockerPasswordResponse getLockerPasswordResponse(Locker locker) {

if (locker.getLastAccess() != null && locker.getLastAccess().isAfter(LocalDateTime.now().minusMinutes(1))) {
throw new LockerCodeAlreadyIssuedException("UBU 우산 대여 실패: 1분 이내에 이미 대여된 우산");
}

String password = HotpGenerator.generate((int) locker.getCount(), locker.getSecretKey());
locker.updateCount();
locker.updateLastAccess(LocalDateTime.now());

return new LockerPasswordResponse(password);
}

private void validateSignature(String signature, String salt, String lockerSecretKey) {

String lockerSignature = encodeHash(lockerSecretKey, salt);

if (!lockerSignature.equals(signature)) {
throw new LockerSignatureErrorException("우산 반납 실패: 잘못된 signature");
}
}

private String encodeHash(String lockerSecretKey, String salt) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}

byte[] encodedhash = digest.digest((lockerSecretKey + "." + salt).getBytes(StandardCharsets.UTF_8));

// 바이트 배열을 16진수 문자열로 변환
StringBuilder hexString = new StringBuilder(2 * encodedhash.length);

for (byte b : encodedhash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}



8 changes: 7 additions & 1 deletion src/main/java/upbrella/be/rent/service/RentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class RentService {
private final UserService userService;
private final RentRepository rentRepository;
private final ConditionReportService conditionReportService;
private final LockerService lockerService;

public RentFormResponse findRentForm(long umbrellaId) {

Expand All @@ -52,10 +53,12 @@ public RentFormResponse findRentForm(long umbrellaId) {
return RentFormResponse.of(umbrella);
}

public ReturnFormResponse findReturnForm(long storeId, User userToReturn) {
public ReturnFormResponse findReturnForm(long storeId, User userToReturn, String salt, String signature) {

StoreMeta storeMeta = storeMetaService.findStoreMetaById(storeId);

lockerService.validateLockerSignature(storeMeta.getId(), salt, signature);

History history = rentRepository.findByUserIdAndReturnedAtIsNull(userToReturn.getId())
.orElseThrow(() -> new NonExistingUmbrellaForRentException("[ERROR] 해당 유저가 대여 중인 우산이 없습니다."));

Expand Down Expand Up @@ -94,6 +97,9 @@ public void addRental(RentUmbrellaByUserRequest rentUmbrellaByUserRequest, User
@Transactional
public void returnUmbrellaByUser(User userToReturn, ReturnUmbrellaByUserRequest request) {

// 반납일 때 secretKey.salt 대문자 후 SHA256 해싱 -> signature와 검증 후, 검증 실패 시 예외 발생
// 보관함이 없는 store일때 salt와 signature 입력 시 예외발생

userService.checkBlackList(userToReturn.getId());
History history = rentRepository.findByUserIdAndReturnedAtIsNull(userToReturn.getId())
.orElseThrow(() -> new NonExistingUmbrellaForRentException("[ERROR] 해당 유저가 대여 중인 우산이 없습니다."));
Expand Down
Loading

0 comments on commit 5a547ee

Please sign in to comment.