diff --git a/src/main/java/de/dhbw/tinf22b6/codespark/api/model/Account.java b/src/main/java/de/dhbw/tinf22b6/codespark/api/model/Account.java index 278bd7f..824fd6d 100644 --- a/src/main/java/de/dhbw/tinf22b6/codespark/api/model/Account.java +++ b/src/main/java/de/dhbw/tinf22b6/codespark/api/model/Account.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -46,11 +47,20 @@ public class Account { @OneToOne(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true) private ExamDate examDate; - public Account(String username, String email, String password, UserRoleType role, boolean verified) { + @Column(nullable = false, updatable = false) + private LocalDateTime creationDate; + + @Column(nullable = false) + private LocalDateTime lastLogin; + + public Account(String username, String email, String password, UserRoleType role, boolean verified, + LocalDateTime creationDate, LocalDateTime lastLogin) { this.username = username; this.email = email; this.password = password; this.role = role; this.verified = verified; + this.creationDate = creationDate; + this.lastLogin = lastLogin; } } diff --git a/src/main/java/de/dhbw/tinf22b6/codespark/api/payload/response/AccountDetailsResponse.java b/src/main/java/de/dhbw/tinf22b6/codespark/api/payload/response/AccountDetailsResponse.java index b5126e6..b1c1c7d 100644 --- a/src/main/java/de/dhbw/tinf22b6/codespark/api/payload/response/AccountDetailsResponse.java +++ b/src/main/java/de/dhbw/tinf22b6/codespark/api/payload/response/AccountDetailsResponse.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import java.time.LocalDateTime; import java.util.UUID; @Getter @@ -24,4 +25,7 @@ public class AccountDetailsResponse { @JsonProperty("profile_image_url") private String profileImageUrl; + + @JsonProperty("creation_date") + private LocalDateTime creationDate; } diff --git a/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImpl.java b/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImpl.java index 3c53ca5..397a4f5 100644 --- a/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImpl.java +++ b/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImpl.java @@ -27,6 +27,8 @@ import java.io.IOException; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -60,7 +62,8 @@ public AccountDetailsResponse getAccountDetails(Account account) { account.getId(), account.getUsername(), account.getEmail(), - account.getProfileImageUrl() + account.getProfileImageUrl(), + account.getCreationDate() ); } @@ -75,7 +78,9 @@ public void createAccount(AccountCreateRequest request) { } String encodedPassword = passwordEncoder.encode(request.getPassword()); - Account account = new Account(request.getUsername(), request.getEmail(), encodedPassword, UserRoleType.USER, false); + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + Account account = new Account(request.getUsername(), request.getEmail(), encodedPassword, + UserRoleType.USER, false, now, now); accountRepository.save(account); String token = UUID.randomUUID().toString(); diff --git a/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImpl.java b/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImpl.java index 35628d2..8fa84ba 100644 --- a/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImpl.java +++ b/src/main/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImpl.java @@ -14,6 +14,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + @Service public class AuthServiceImpl implements AuthService { private final AccountRepository accountRepository; @@ -45,6 +48,9 @@ public TokenResponse loginAccount(LoginRequest request) { String accessToken = jwtUtil.generateAccessToken(account.getUsername()); String refreshToken = jwtUtil.generateRefreshToken(account.getUsername()); + account.setLastLogin(LocalDateTime.now(ZoneOffset.UTC)); + accountRepository.save(account); + return new TokenResponse(accessToken, refreshToken); } @@ -55,7 +61,14 @@ public TokenResponse refreshAccessToken(RefreshTokenRequest request) { } String username = jwtUtil.extractUsername(request.getRefreshToken()); + Account account = accountRepository.findByUsername(username) + .orElseThrow(() -> new InvalidAccountCredentialsException("No account with username " + username + "was found.")); + String newAccessToken = jwtUtil.generateAccessToken(username); + + account.setLastLogin(LocalDateTime.now(ZoneOffset.UTC)); + accountRepository.save(account); + return new TokenResponse(newAccessToken, request.getRefreshToken()); } } diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 8fb4576..933e3b3 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -7,6 +7,8 @@ spring: console: enabled: true path: /h2-console + liquibase: + enabled: false auth: jwt: diff --git a/src/main/resources/db/changelog/changes/001_add-account-timestamps.yaml b/src/main/resources/db/changelog/changes/001_add-account-timestamps.yaml new file mode 100644 index 0000000..b2a8c91 --- /dev/null +++ b/src/main/resources/db/changelog/changes/001_add-account-timestamps.yaml @@ -0,0 +1,47 @@ +databaseChangeLog: + - changeSet: + id: 1 + author: denniskp + changes: + - addColumn: + tableName: account + columns: + - column: + name: creation_date + type: TIMESTAMP + defaultValueComputed: CURRENT_TIMESTAMP + constraints: + nullable: true + - column: + name: last_login + type: TIMESTAMP + defaultValueComputed: CURRENT_TIMESTAMP + constraints: + nullable: true + + - changeSet: + id: 1.1 + author: denniskp + comment: "Backfill existing records" + changes: + - update: + tableName: account + columns: + - column: + name: creation_date + valueComputed: CURRENT_TIMESTAMP + - column: + name: last_login + valueComputed: CURRENT_TIMESTAMP + + - changeSet: + id: 1.2 + author: denniskp + comment: "Make both columns NOT NULL" + changes: + - addNotNullConstraint: + tableName: account + columnName: creation_date + - addNotNullConstraint: + tableName: account + columnName: last_login diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index daf9f00..2f11b67 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -1 +1,3 @@ -databaseChangeLog: [] +databaseChangeLog: + - include: + file: db/changelog/changes/001_add-account-timestamps.yaml diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/controller/AccountControllerTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/controller/AccountControllerTests.java index bc35878..9b722b7 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/controller/AccountControllerTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/controller/AccountControllerTests.java @@ -24,6 +24,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDateTime; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -61,7 +62,8 @@ void testGetAccountDetails() throws Exception { UUID.fromString("e6c9f025-33ea-4b92-a114-6b6ee6c47d36"), "user", "email@example.com", - "https://photos.com/user-profile-photo" + "https://photos.com/user-profile-photo", + LocalDateTime.of(2022, 1, 1, 12, 0, 0) ); Mockito.when(accountService.getAccountDetails(any())).thenReturn(mockResponse); @@ -70,7 +72,8 @@ void testGetAccountDetails() throws Exception { .andExpect(jsonPath("$.id").value("e6c9f025-33ea-4b92-a114-6b6ee6c47d36")) .andExpect(jsonPath("$.username").value("user")) .andExpect(jsonPath("$.email").value("email@example.com")) - .andExpect(jsonPath("$.profile_image_url").value("https://photos.com/user-profile-photo")); + .andExpect(jsonPath("$.profile_image_url").value("https://photos.com/user-profile-photo")) + .andExpect(jsonPath("$.creation_date").value("2022-01-01T12:00:00")); } @Test diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/AccountRepositoryTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/AccountRepositoryTests.java index f2b7e14..5f2c0fd 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/AccountRepositoryTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/AccountRepositoryTests.java @@ -7,6 +7,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.ActiveProfiles; +import java.time.LocalDateTime; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -19,7 +20,8 @@ class AccountRepositoryTests { @Test void testFindByUsername_shouldReturnAccount() { - Account account = new Account("john_doe", "john@example.com", "hashed_pwd", UserRoleType.USER, true); + Account account = new Account("john_doe", "john@example.com", "hashed_pwd", + UserRoleType.USER, true, LocalDateTime.now(), LocalDateTime.now()); accountRepository.save(account); Optional result = accountRepository.findByUsername("john_doe"); @@ -30,7 +32,8 @@ void testFindByUsername_shouldReturnAccount() { @Test void findByEmail_shouldReturnAccount() { - Account account = new Account("jane_doe", "jane@example.com", "hashed_pwd", UserRoleType.USER, false); + Account account = new Account("jane_doe", "jane@example.com", "hashed_pwd", + UserRoleType.USER, false, LocalDateTime.now(), LocalDateTime.now()); accountRepository.save(account); Optional result = accountRepository.findByEmail("jane@example.com"); diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ConversationRepositoryTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ConversationRepositoryTests.java index ec4fbb1..884672c 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ConversationRepositoryTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ConversationRepositoryTests.java @@ -8,6 +8,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.ActiveProfiles; +import java.time.LocalDateTime; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -23,7 +24,8 @@ class ConversationRepositoryTests { @Test void testFindByAccount_shouldReturnConversation() { - Account account = new Account("user1", "user1@example.com", "password", UserRoleType.USER, true); + Account account = new Account("user1", "user1@example.com", "password", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now()); account = accountRepository.save(account); Conversation conversation = new Conversation(account); diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ExamDateRepositoryTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ExamDateRepositoryTests.java index c8eed1b..a0a2a1f 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ExamDateRepositoryTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/ExamDateRepositoryTests.java @@ -24,7 +24,8 @@ class ExamDateRepositoryTests { @Test void testFindByAccount_shouldReturnExamDate() { - Account account = new Account("student1", "student1@example.com", "password", UserRoleType.USER, true); + Account account = new Account("student1", "student1@example.com", "password", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now()); account = accountRepository.save(account); ExamDate examDate = new ExamDate(LocalDateTime.of(2025, 7, 15, 10, 0)); diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserBadgeRepositoryTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserBadgeRepositoryTests.java index 60f45a8..6319a4e 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserBadgeRepositoryTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserBadgeRepositoryTests.java @@ -29,7 +29,8 @@ class UserBadgeRepositoryTests { @Test void testFindByAccountAndBadge_shouldReturnUserBadge() { - Account account = new Account("user1", "user1@example.com", "secret", UserRoleType.USER, true); + Account account = new Account("user1", "user1@example.com", "secret", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now()); account = accountRepository.save(account); Badge badge = new Badge("First Win", "Earned after completing a lesson", BadgeType.FIRST_LESSON_COMPLETED, "icon.png"); @@ -46,7 +47,8 @@ void testFindByAccountAndBadge_shouldReturnUserBadge() { @Test void testExistsByAccountAndBadge_shouldReturnTrue() { - Account account = new Account("user2", "user2@example.com", "pass", UserRoleType.USER, true); + Account account = new Account("user2", "user2@example.com", "pass", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now()); account = accountRepository.save(account); Badge badge = new Badge("Achiever", "Earned after completing a chapter", BadgeType.FIRST_CHAPTER_COMPLETED, "icon2.png"); @@ -62,7 +64,8 @@ void testExistsByAccountAndBadge_shouldReturnTrue() { @Test void testExistsByAccountAndBadge_shouldReturnFalseWhenNotExists() { - Account account = accountRepository.save(new Account("user3", "user3@example.com", "pw", UserRoleType.USER, true)); + Account account = accountRepository.save(new Account("user3", "user3@example.com", "pw", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now())); Badge badge = badgeRepository.save(new Badge("Unobtainable", "Never awarded", BadgeType.ALL_CHAPTERS_COMPLETED, "icon3.png")); boolean exists = userBadgeRepository.existsByAccountAndBadge(account, badge); diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserLessonProgressRepositoryTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserLessonProgressRepositoryTests.java index d5350b5..e099fc4 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserLessonProgressRepositoryTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/UserLessonProgressRepositoryTests.java @@ -12,6 +12,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.ActiveProfiles; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -34,10 +35,11 @@ class UserLessonProgressRepositoryTests { @Test void testFindByAccountAndLesson_shouldReturnProgress() { - Account account = accountRepository.save(new Account("testUser", "user@example.com", "pw", UserRoleType.USER, true)); + Account account = accountRepository.save(new Account("testUser", "user@example.com", "pw", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now())); Chapter chapter = chapterRepository.save(new Chapter("Chapter 1", "Intro", null, null)); - TheoryLesson lesson = lessonRepository.save(new TheoryLesson("L1", "Theory", LessonType.THEORY, chapter, null, null, "Sample text")); + TheoryLesson lesson = lessonRepository.save(new TheoryLesson("L1", "Theory", LessonType.THEORY,chapter, null, null, "Sample text")); progressRepository.save(new UserLessonProgress(account, lesson, LessonProgressState.ATTEMPTED)); Optional result = progressRepository.findByAccountAndLesson(account, lesson); @@ -48,7 +50,8 @@ void testFindByAccountAndLesson_shouldReturnProgress() { @Test void testFindByAccountAndState_shouldReturnCorrectList() { - Account account = accountRepository.save(new Account("testUser2", "user2@example.com", "pw", UserRoleType.USER, true)); + Account account = accountRepository.save(new Account("testUser2", "user2@example.com", "pw", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now())); Chapter chapter = chapterRepository.save(new Chapter("Chapter 2", "Intro 2", null, null)); TheoryLesson lesson1 = lessonRepository.save(new TheoryLesson("L2", "Theory 2", LessonType.THEORY, chapter, null, null, "Text 1")); @@ -65,7 +68,8 @@ void testFindByAccountAndState_shouldReturnCorrectList() { @Test void testFindByAccountAndLesson_shouldReturnEmptyWhenNone() { - Account account = accountRepository.save(new Account("user3", "user3@example.com", "pw", UserRoleType.USER, true)); + Account account = accountRepository.save(new Account("user3", "user3@example.com", "pw", UserRoleType.USER, + true, LocalDateTime.now(), LocalDateTime.now())); Chapter chapter = chapterRepository.save(new Chapter("Chapter X", "X", null, null)); TheoryLesson lesson = lessonRepository.save(new TheoryLesson("L4", "No progress", LessonType.THEORY, chapter, null, null, "Empty")); diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/VerificationTokenRepositoryTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/VerificationTokenRepositoryTests.java index dda7b51..2d6ab5e 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/VerificationTokenRepositoryTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/repository/VerificationTokenRepositoryTests.java @@ -9,6 +9,7 @@ import org.springframework.test.context.ActiveProfiles; import java.time.Instant; +import java.time.LocalDateTime; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -24,7 +25,8 @@ class VerificationTokenRepositoryTests { @Test void testFindByTokenAndType_shouldReturnToken() { - Account account = new Account("user1", "test@example.com", "pass", null, true); + Account account = new Account("user1", "test@example.com", "pass", null, + true, LocalDateTime.now(), LocalDateTime.now()); accountRepository.save(account); VerificationToken token = new VerificationToken( diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImplTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImplTests.java index d744311..e4ec400 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImplTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AccountServiceImplTests.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.time.Instant; +import java.time.LocalDateTime; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -63,7 +64,8 @@ void setUp() { @Test void getAccountDetails_shouldReturnCorrectResponse() { - Account account = new Account("user", "user@example.com", "pass", UserRoleType.USER, true); + Account account = new Account("user", "user@example.com", "pass", + UserRoleType.USER, true, LocalDateTime.now(), LocalDateTime.now()); account.setId(UUID.randomUUID()); account.setProfileImageUrl("http://image.url"); diff --git a/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImplTests.java b/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImplTests.java index 6f9619d..2c04ad3 100644 --- a/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImplTests.java +++ b/src/test/java/de/dhbw/tinf22b6/codespark/api/service/AuthServiceImplTests.java @@ -100,10 +100,15 @@ void loginAccount_shouldThrow_whenAccountNotVerified() { @Test void refreshAccessToken_shouldReturnNewAccessToken_whenValid() { RefreshTokenRequest request = new RefreshTokenRequest("refresh-token"); + Account account = new Account(); + account.setUsername("testuser"); + account.setPassword("encoded"); + account.setVerified(false); when(jwtUtil.validateToken("refresh-token")).thenReturn(true); when(jwtUtil.extractUsername("refresh-token")).thenReturn("testuser"); when(jwtUtil.generateAccessToken("testuser")).thenReturn("new-access-token"); + when(accountRepository.findByUsername("testuser")).thenReturn(Optional.of(account)); TokenResponse response = authService.refreshAccessToken(request);