Skip to content

Commit

Permalink
fix: ✏️ Device Token Session 관리를 위한 로직 수정 (#152)
Browse files Browse the repository at this point in the history
* fix: device token entity updated_at auditing 제거 -> 조회할 때마다 last_sigend_in 필드 업데이트

* fix: register service에서 이미 존재하는 토큰 조회 시, signed in at 갱신

* fix: device token 만료 에러 상수 제거

* docs: put_device 활성화되지 않은 토큰 에러 제거

* fix: device_token 삭제 요청시, deactivate 호출하는 로직으로 변경

* style: 사용하지 않는 의존성 제거

* test: 토큰 활성화, 비활성화 테스트 수정
  • Loading branch information
psychology50 authored Aug 13, 2024
1 parent a67b589 commit 5f1e909
Show file tree
Hide file tree
Showing 8 changed files with 53 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,6 @@ public interface UserAccountApi {
@Operation(summary = "디바이스 등록", description = "사용자의 디바이스 정보를 등록합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schemaProperties = @SchemaProperty(name = "deviceToken", schema = @Schema(implementation = DeviceTokenDto.RegisterRes.class)))),
@ApiResponse(responseCode = "400", content = @Content(mediaType = "application/json", examples = {
@ExampleObject(name = "잘못된 디바이스 토큰 저장 요청", description = "서버에 동일한 이름의 토큰이 사용자에게 등록되어 있고, 해당 토큰이 만료처리되어 있을 경우에 해당한다. (애초에 발생해선 안 되는 에러)", value = """
{
"code": "4005",
"message": "활성화되지 않은 디바이스 토큰 정보입니다."
}
""")
})),
@ApiResponse(responseCode = "404", content = @Content(mediaType = "application/json", examples = {
@ExampleObject(name = "수정 요청 시, token에 매칭하는 디바이스 정보가 없는 경우", value = """
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package kr.co.pennyway.api.apis.users.service;

import kr.co.pennyway.domain.domains.device.domain.DeviceToken;
import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorCode;
import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorException;
import kr.co.pennyway.domain.domains.device.service.DeviceTokenService;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.exception.UserErrorCode;
Expand All @@ -13,6 +11,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
Expand All @@ -23,17 +23,25 @@ public class DeviceTokenRegisterService {
@Transactional
public DeviceToken execute(Long userId, String token) {
User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND));
DeviceToken deviceToken = getOrCreateDevice(user, token);

if (!deviceToken.isActivated()) {
throw new DeviceTokenErrorException(DeviceTokenErrorCode.NOT_ACTIVATED_DEVICE);
}

return deviceToken;
return getOrCreateDevice(user, token);
}

/**
* 사용자의 디바이스 토큰을 가져오거나 생성한다.
* <p>
* 이미 등록된 디바이스 토큰인 경우 마지막 로그인 시간을 갱신한다.
*/
private DeviceToken getOrCreateDevice(User user, String token) {
return deviceTokenService.readDeviceByUserIdAndToken(user.getId(), token)
.orElseGet(() -> deviceTokenService.createDevice(DeviceToken.of(token, user)));
Optional<DeviceToken> deviceToken = deviceTokenService.readDeviceByUserIdAndToken(user.getId(), token);

if (deviceToken.isPresent()) {
DeviceToken device = deviceToken.get();
device.activate();
device.updateLastSignedInAt();
return device;
} else {
return deviceTokenService.createDevice(DeviceToken.of(token, user));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorCode;
import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorException;
import kr.co.pennyway.domain.domains.device.service.DeviceTokenService;
import kr.co.pennyway.domain.domains.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -14,7 +13,6 @@
@Service
@RequiredArgsConstructor
public class DeviceTokenUnregisterService {
private final UserService userService;
private final DeviceTokenService deviceTokenService;

@Transactional
Expand All @@ -23,6 +21,6 @@ public void execute(Long userId, String token) {
() -> new DeviceTokenErrorException(DeviceTokenErrorCode.NOT_FOUND_DEVICE)
);

deviceTokenService.deleteDevice(deviceToken);
deviceToken.deactivate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
import kr.co.pennyway.api.config.fixture.UserFixture;
import kr.co.pennyway.domain.config.JpaConfig;
import kr.co.pennyway.domain.domains.device.domain.DeviceToken;
import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorCode;
import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorException;
import kr.co.pennyway.domain.domains.device.service.DeviceTokenService;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.service.UserService;
Expand All @@ -25,7 +23,6 @@
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.test.util.AssertionErrors.*;

@ExtendWith(MockitoExtension.class)
Expand Down Expand Up @@ -101,16 +98,18 @@ void registerNewDeviceWhenDeviceIsAlreadyExists() {

@Test
@Transactional
@DisplayName("[3] token 등록 요청이 들어왔을 때, 활성화되지 않은 디바이스 토큰이 존재하는 경우 NOT_ACTIVATED_DEVICE 에러를 반환한다.")
@DisplayName("[3] token 등록 요청이 들어왔을 때, 활성화되지 않은 디바이스 토큰이 존재하는 경우 토큰을 활성화 상태로 변경한다.")
void registerNewDeviceWhenDeviceIsNotActivated() {
// given
DeviceToken originDeviceToken = DeviceTokenFixture.INIT.toDevice(requestUser);
originDeviceToken.deactivate();
deviceTokenService.createDevice(originDeviceToken);
DeviceTokenDto.RegisterReq request = DeviceTokenFixture.INIT.toRegisterReq();

// when - then
DeviceTokenErrorException ex = assertThrows(DeviceTokenErrorException.class, () -> deviceTokenRegisterService.execute(requestUser.getId(), request.token()));
assertEquals("활성화되지 않은 디바이스 토큰이 존재하는 경우 Not Activated Device를 반환한다.", DeviceTokenErrorCode.NOT_ACTIVATED_DEVICE, ex.getBaseErrorCode());
// when
DeviceToken response = deviceTokenRegisterService.execute(requestUser.getId(), request.token());

// then
assertTrue("디바이스가 활성화 상태여야 한다.", response.getActivated());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertNull;
import static org.springframework.test.util.AssertionErrors.assertFalse;

@ExtendWith(MockitoExtension.class)
@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=create")
Expand Down Expand Up @@ -56,7 +54,7 @@ void setUp() {

@Test
@Transactional
@DisplayName("사용자 ID와 origin token에 매칭되는 활성 디바이스가 존재하는 경우 디바이스를 삭제한다.")
@DisplayName("사용자 ID와 origin token에 매칭되는 활성 디바이스가 존재하는 경우 디바이스를 비활성화한다.")
void unregisterDevice() {
// given
DeviceToken deviceToken = DeviceTokenFixture.INIT.toDevice(requestUser);
Expand All @@ -66,8 +64,8 @@ void unregisterDevice() {
deviceTokenUnregisterService.execute(requestUser.getId(), deviceToken.getToken());

// then
Optional<DeviceToken> deletedDevice = deviceTokenService.readDeviceByUserIdAndToken(requestUser.getId(), deviceToken.getToken());
assertNull("디바이스가 삭제되어 있어야 한다.", deletedDevice.orElse(null));
DeviceToken deletedDevice = deviceTokenService.readDeviceByUserIdAndToken(requestUser.getId(), deviceToken.getToken()).get();
assertFalse("디바이스가 비활성화 되어있어야 한다.", deletedDevice.isActivated());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
package kr.co.pennyway.domain.domains.device.domain;

import jakarta.persistence.*;
import kr.co.pennyway.domain.common.model.DateAuditable;
import kr.co.pennyway.domain.domains.user.domain.User;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;
import java.util.Objects;

@Entity
@Getter
@Table(name = "device_token")
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DeviceToken extends DateAuditable {
public class DeviceToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String token;

@ColumnDefault("true")
private Boolean activated;

@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
private LocalDateTime lastSignedInAt;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
Expand All @@ -31,6 +40,7 @@ private DeviceToken(String token, Boolean activated, User user) {
this.token = Objects.requireNonNull(token, "token은 null이 될 수 없습니다.");
this.activated = Objects.requireNonNull(activated, "activated는 null이 될 수 없습니다.");
this.user = Objects.requireNonNull(user, "user는 null이 될 수 없습니다.");
this.lastSignedInAt = LocalDateTime.now();
}

public static DeviceToken of(String token, User user) {
Expand All @@ -49,12 +59,19 @@ public void deactivate() {
this.activated = Boolean.FALSE;
}

public void updateLastSignedInAt() {
this.lastSignedInAt = LocalDateTime.now();
}

/**
* 디바이스 토큰을 갱신하고 활성화 상태로 변경한다.
* 디바이스 토큰이 만료되었는지 확인한다.
*
* @return 토큰이 갱신된지 7일이 지났으면 true, 그렇지 않으면 false
*/
public void updateToken(String token) {
this.activated = Boolean.TRUE;
this.token = token;
public boolean isExpired() {
LocalDateTime now = LocalDateTime.now();

return lastSignedInAt.plusDays(7).isBefore(now);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@

@RequiredArgsConstructor
public enum DeviceTokenErrorCode implements BaseErrorCode {
/* 400 BAD_REQUEST */
NOT_ACTIVATED_DEVICE(StatusCode.BAD_REQUEST, ReasonCode.CLIENT_ERROR, "활성화되지 않은 디바이스 토큰 정보입니다."),

/* 404 NOT_FOUND */
NOT_FOUND_DEVICE(StatusCode.NOT_FOUND, ReasonCode.REQUESTED_RESOURCE_NOT_FOUND, "디바이스를 찾을 수 없습니다.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,7 @@ public DeviceToken createDevice(DeviceToken deviceToken) {
public Optional<DeviceToken> readDeviceByUserIdAndToken(Long userId, String token) {
return deviceTokenRepository.findByUser_IdAndToken(userId, token);
}

@Transactional
public void deleteDevice(DeviceToken deviceToken) {
deviceTokenRepository.delete(deviceToken);
}


@Transactional
public void deleteDevicesByUserIdInQuery(Long userId) {
deviceTokenRepository.deleteAllByUserIdInQuery(userId);
Expand Down

0 comments on commit 5f1e909

Please sign in to comment.