From b8c2d4f693c631616de6bf556d180f11fac189c6 Mon Sep 17 00:00:00 2001 From: abeb Date: Sat, 21 Mar 2026 19:20:18 +0300 Subject: [PATCH 1/7] feat: add user and refresh token entities --- .../entity/RefreshTokenEntity.java | 33 +++++++++ .../smartjamapi/entity/UserEntity.java | 70 +++++++++++++++++++ .../com/smartjam/smartjamapi/enums/Role.java | 6 ++ .../smartjamapi/enums/StatusRefreshToken.java | 7 ++ .../repository/RefreshTokenRepository.java | 35 ++++++++++ .../repository/UserRepository.java | 14 ++++ 6 files changed, 165 insertions(+) create mode 100644 backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java create mode 100644 backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java create mode 100644 backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java create mode 100644 backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java create mode 100644 backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java create mode 100644 backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java new file mode 100644 index 0000000..e174419 --- /dev/null +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java @@ -0,0 +1,33 @@ +package com.smartjam.smartjamapi.entity; + +import java.time.Instant; +import java.util.UUID; + +import jakarta.persistence.*; + +import com.smartjam.smartjamapi.enums.StatusRefreshToken; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "refresh_tokens") +@Data +@NoArgsConstructor +public class RefreshTokenEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(nullable = false, unique = true) + private String tokenHash; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private UserEntity user; + + @Column(nullable = false, name = "expires_at") + private Instant expiresAt; + + @Column(nullable = false) + private StatusRefreshToken status; +} \ No newline at end of file diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java new file mode 100644 index 0000000..5d9b4db --- /dev/null +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java @@ -0,0 +1,70 @@ +package com.smartjam.smartjamapi.entity; + +import java.time.Instant; +import java.util.UUID; + +import jakarta.persistence.*; + +import com.smartjam.smartjamapi.enums.Role; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "users") +@Entity +public class UserEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false, unique = true) + private String email; + + @Column(name = "password_hash", nullable = false) + private String passwordHash; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Column(name = "avatar_url") + private String avatarUrl; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role = Role.STUDENT; + + @Column(name = "fcm_token") + private String fcmToken; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + + // @Override + // public @NonNull Collection getAuthorities() { + // return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); + // } + // + // @Override + // public @NonNull String getPassword() { + // return passwordHash; + // } +} \ No newline at end of file diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java new file mode 100644 index 0000000..1297151 --- /dev/null +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java @@ -0,0 +1,6 @@ +package com.smartjam.smartjamapi.enums; + +public enum Role { + STUDENT, + TEACHER +} \ No newline at end of file diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java new file mode 100644 index 0000000..7f1d369 --- /dev/null +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java @@ -0,0 +1,7 @@ +package com.smartjam.smartjamapi.enums; + +public enum StatusRefreshToken { + ACTIVE, + USED, + REVOKED +} \ No newline at end of file diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..7e3d345 --- /dev/null +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java @@ -0,0 +1,35 @@ +package com.smartjam.smartjamapi.repository; + +import java.util.Optional; +import java.util.UUID; + +import com.smartjam.smartjamapi.entity.RefreshTokenEntity; +import com.smartjam.smartjamapi.entity.UserEntity; +import com.smartjam.smartjamapi.enums.StatusRefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +public interface RefreshTokenRepository extends JpaRepository { + Optional findByTokenHash(String token); + + @Transactional + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RefreshTokenEntity r SET r.status = :status WHERE r.tokenHash = :tokenHash") + void setStatusByRefreshToken(@Param("tokenHash") String tokenHash, @Param("status") StatusRefreshToken status); + + @Modifying + @Transactional + @Query(""" + UPDATE RefreshTokenEntity r + SET r.status = :newStatus + WHERE r.user = :user + AND r.status = :currentStatus +""") + void setStatusUsedRefreshToken( + @Param("user") UserEntity userEntity, + @Param("currentStatus") StatusRefreshToken currentStatus, + @Param("newStatus") StatusRefreshToken newStatus); +} \ No newline at end of file diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java new file mode 100644 index 0000000..1d70b0c --- /dev/null +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.smartjam.smartjamapi.repository; + +import java.util.Optional; +import java.util.UUID; + +import com.smartjam.smartjamapi.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String login); + + boolean existsByEmail(String email); +} \ No newline at end of file From 93edee3e435293d56ea565198f3745294b7f15e2 Mon Sep 17 00:00:00 2001 From: abeb Date: Sat, 21 Mar 2026 19:25:08 +0300 Subject: [PATCH 2/7] fix: style --- .../com/smartjam/smartjamapi/entity/RefreshTokenEntity.java | 2 +- .../main/java/com/smartjam/smartjamapi/entity/UserEntity.java | 2 +- .../src/main/java/com/smartjam/smartjamapi/enums/Role.java | 2 +- .../java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java | 2 +- .../smartjam/smartjamapi/repository/RefreshTokenRepository.java | 2 +- .../com/smartjam/smartjamapi/repository/UserRepository.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java index e174419..e058918 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java @@ -30,4 +30,4 @@ public class RefreshTokenEntity { @Column(nullable = false) private StatusRefreshToken status; -} \ No newline at end of file +} diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java index 5d9b4db..56ff969 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java @@ -67,4 +67,4 @@ public class UserEntity { // public @NonNull String getPassword() { // return passwordHash; // } -} \ No newline at end of file +} diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java index 1297151..0e1a3f7 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java @@ -3,4 +3,4 @@ public enum Role { STUDENT, TEACHER -} \ No newline at end of file +} diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java index 7f1d369..f7e9f5b 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java @@ -4,4 +4,4 @@ public enum StatusRefreshToken { ACTIVE, USED, REVOKED -} \ No newline at end of file +} diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java index 7e3d345..3718ae0 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java @@ -32,4 +32,4 @@ void setStatusUsedRefreshToken( @Param("user") UserEntity userEntity, @Param("currentStatus") StatusRefreshToken currentStatus, @Param("newStatus") StatusRefreshToken newStatus); -} \ No newline at end of file +} diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java index 1d70b0c..3546fb6 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java @@ -11,4 +11,4 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String login); boolean existsByEmail(String email); -} \ No newline at end of file +} From 72db2eef418fb19f7a0df6eb7cc199f39c74aa5a Mon Sep 17 00:00:00 2001 From: abeb Date: Sat, 21 Mar 2026 19:57:00 +0300 Subject: [PATCH 3/7] fix: remove @Data from JPA entities to prevent lazy loading issues --- .../smartjamapi/entity/RefreshTokenEntity.java | 8 +++++--- .../com/smartjam/smartjamapi/entity/UserEntity.java | 10 ---------- .../smartjamapi/repository/RefreshTokenRepository.java | 6 +++--- .../smartjamapi/repository/UserRepository.java | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java index e058918..8a85c0c 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java @@ -6,13 +6,14 @@ import jakarta.persistence.*; import com.smartjam.smartjamapi.enums.StatusRefreshToken; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Table(name = "refresh_tokens") -@Data +@Getter +@Setter @NoArgsConstructor +@AllArgsConstructor public class RefreshTokenEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) @@ -29,5 +30,6 @@ public class RefreshTokenEntity { private Instant expiresAt; @Column(nullable = false) + @Enumerated(EnumType.STRING) private StatusRefreshToken status; } diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java index 56ff969..71512c5 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java @@ -57,14 +57,4 @@ public class UserEntity { @UpdateTimestamp @Column(name = "updated_at", nullable = false) private Instant updatedAt; - - // @Override - // public @NonNull Collection getAuthorities() { - // return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); - // } - // - // @Override - // public @NonNull String getPassword() { - // return passwordHash; - // } } diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java index 3718ae0..47f1e84 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java @@ -16,19 +16,19 @@ public interface RefreshTokenRepository extends JpaRepository findByTokenHash(String token); @Transactional - @Modifying(clearAutomatically = true, flushAutomatically = true) + @Modifying(clearAutomatically = true) @Query("UPDATE RefreshTokenEntity r SET r.status = :status WHERE r.tokenHash = :tokenHash") void setStatusByRefreshToken(@Param("tokenHash") String tokenHash, @Param("status") StatusRefreshToken status); - @Modifying @Transactional + @Modifying(clearAutomatically = true) @Query(""" UPDATE RefreshTokenEntity r SET r.status = :newStatus WHERE r.user = :user AND r.status = :currentStatus """) - void setStatusUsedRefreshToken( + void updateStatusByUserAndCurrentStatus( @Param("user") UserEntity userEntity, @Param("currentStatus") StatusRefreshToken currentStatus, @Param("newStatus") StatusRefreshToken newStatus); diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java index 3546fb6..dfb51c8 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java @@ -8,7 +8,7 @@ public interface UserRepository extends JpaRepository { - Optional findByEmail(String login); + Optional findByEmail(String email); boolean existsByEmail(String email); } From c86e0514d9df2b8c190d485f69191ac4cb597d0a Mon Sep 17 00:00:00 2001 From: abeb Date: Sat, 21 Mar 2026 20:18:52 +0300 Subject: [PATCH 4/7] fix: delete space and add parameter flush to @Modifying --- .../smartjamapi/repository/RefreshTokenRepository.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java index 47f1e84..0d54ba7 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java @@ -16,12 +16,12 @@ public interface RefreshTokenRepository extends JpaRepository findByTokenHash(String token); @Transactional - @Modifying(clearAutomatically = true) - @Query("UPDATE RefreshTokenEntity r SET r.status = :status WHERE r.tokenHash = :tokenHash") + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RefreshTokenEntity r SET r.status = :status WHERE r.tokenHash = :tokenHash") void setStatusByRefreshToken(@Param("tokenHash") String tokenHash, @Param("status") StatusRefreshToken status); @Transactional - @Modifying(clearAutomatically = true) + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" UPDATE RefreshTokenEntity r SET r.status = :newStatus From 7a5465fb7f9f589bc232c1d65e6c40a177db2f19 Mon Sep 17 00:00:00 2001 From: abeb Date: Sun, 22 Mar 2026 04:16:12 +0300 Subject: [PATCH 5/7] fix: all without javadoc --- .../smartjamapi/entity/RefreshTokenEntity.java | 14 ++++++++++++-- .../smartjam/smartjamapi/entity/UserEntity.java | 15 ++++++++------- ...sRefreshToken.java => RefreshTokenStatus.java} | 5 ++--- .../repository/RefreshTokenRepository.java | 12 ++++++------ 4 files changed, 28 insertions(+), 18 deletions(-) rename backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/{StatusRefreshToken.java => RefreshTokenStatus.java} (50%) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java index 8a85c0c..edddb6e 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java @@ -5,8 +5,10 @@ import jakarta.persistence.*; -import com.smartjam.smartjamapi.enums.StatusRefreshToken; +import com.smartjam.smartjamapi.enums.RefreshTokenStatus; import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; @Entity @Table(name = "refresh_tokens") @@ -31,5 +33,13 @@ public class RefreshTokenEntity { @Column(nullable = false) @Enumerated(EnumType.STRING) - private StatusRefreshToken status; + private RefreshTokenStatus status = RefreshTokenStatus.ACTIVE; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; } diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java index 71512c5..b08248b 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java @@ -4,12 +4,10 @@ import java.util.UUID; import jakarta.persistence.*; +import jakarta.validation.constraints.Email; import com.smartjam.smartjamapi.enums.Role; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -19,19 +17,22 @@ @AllArgsConstructor @Table(name = "users") @Entity +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) + @EqualsAndHashCode.Include private UUID id; @Column(nullable = false) private String nickname; - @Column(nullable = false, unique = true) + @Column(nullable = false, unique = true, length = 255) + @Email private String email; - @Column(name = "password_hash", nullable = false) + @Column(name = "password_hash", nullable = false, length = 255) private String passwordHash; @Column(name = "first_name") @@ -40,7 +41,7 @@ public class UserEntity { @Column(name = "last_name") private String lastName; - @Column(name = "avatar_url") + @Column(name = "avatar_url", length = 2048) private String avatarUrl; @Enumerated(EnumType.STRING) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/RefreshTokenStatus.java similarity index 50% rename from backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java rename to backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/RefreshTokenStatus.java index f7e9f5b..d45d56a 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/StatusRefreshToken.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/RefreshTokenStatus.java @@ -1,7 +1,6 @@ package com.smartjam.smartjamapi.enums; -public enum StatusRefreshToken { +public enum RefreshTokenStatus { ACTIVE, - USED, - REVOKED + INACTIVE } diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java index 0d54ba7..6fc55e7 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java @@ -5,7 +5,7 @@ import com.smartjam.smartjamapi.entity.RefreshTokenEntity; import com.smartjam.smartjamapi.entity.UserEntity; -import com.smartjam.smartjamapi.enums.StatusRefreshToken; +import com.smartjam.smartjamapi.enums.RefreshTokenStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -18,7 +18,7 @@ public interface RefreshTokenRepository extends JpaRepository Date: Sun, 22 Mar 2026 04:26:21 +0300 Subject: [PATCH 6/7] fix: set length passHash + int instead of boolean --- .../com/smartjam/smartjamapi/entity/RefreshTokenEntity.java | 3 +-- .../smartjamapi/repository/RefreshTokenRepository.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java index edddb6e..5404f54 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java @@ -15,13 +15,12 @@ @Getter @Setter @NoArgsConstructor -@AllArgsConstructor public class RefreshTokenEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - @Column(nullable = false, unique = true) + @Column(nullable = false, unique = true, length = 64) private String tokenHash; @ManyToOne(fetch = FetchType.LAZY) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java index 6fc55e7..3d41710 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java @@ -18,7 +18,7 @@ public interface RefreshTokenRepository extends JpaRepository Date: Sun, 22 Mar 2026 12:12:45 +0300 Subject: [PATCH 7/7] docs: add javadoc for all files --- .../entity/RefreshTokenEntity.java | 39 ++++++++++++ .../smartjamapi/entity/UserEntity.java | 37 +++++++++++ .../smartjamapi/enums/RefreshTokenStatus.java | 11 ++++ .../com/smartjam/smartjamapi/enums/Role.java | 6 ++ .../repository/RefreshTokenRepository.java | 62 ++++++++++++++++++- .../repository/UserRepository.java | 33 ++++++++++ 6 files changed, 187 insertions(+), 1 deletion(-) diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java index 5404f54..25dbf3e 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java @@ -10,34 +10,73 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +/** + * JPA entity representing a refresh token record in the SmartJam platform. + * + *

Mapped to the {@code refresh_tokens} database table. Rather than storing the raw token string, only a + * cryptographic hash of the token is persisted in {@link #tokenHash}. This prevents token replay attacks in the event + * of a database compromise. + * + *

The lifecycle of a token is tracked via the {@link #status} field. Tokens start as + * {@link RefreshTokenStatus#ACTIVE} and transition to {@link RefreshTokenStatus#INACTIVE} upon revocation or expiry. + * + *

Persistence exceptions: Attempting to persist or merge an instance with a duplicate {@code tokenHash}, a + * {@code null} required field, or a value that violates a column constraint will cause the JPA provider to throw a + * {@link jakarta.persistence.PersistenceException} (typically wrapped by Spring as + * {@link org.springframework.dao.DataIntegrityViolationException}). + */ @Entity @Table(name = "refresh_tokens") @Getter @Setter @NoArgsConstructor public class RefreshTokenEntity { + + /** Unique identifier for the refresh token record, generated automatically as a UUID. */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; + /** + * Cryptographic hash (e.g. SHA-256 hex digest) of the opaque refresh token string. Stored instead of the raw token + * to mitigate replay attacks from a compromised database. Length is 64 characters, sufficient for a SHA-256 hex + * digest. + */ @Column(nullable = false, unique = true, length = 64) private String tokenHash; + /** + * The user to whom this refresh token belongs. Loaded lazily to avoid unnecessary joins when only token metadata is + * needed. + */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private UserEntity user; + /** The point in time at which this refresh token expires and must no longer be accepted. */ @Column(nullable = false, name = "expires_at") private Instant expiresAt; + /** + * The current lifecycle status of this refresh token. Defaults to {@link RefreshTokenStatus#ACTIVE} upon creation. + * Persisted as a {@link String} in the database. + */ @Column(nullable = false) @Enumerated(EnumType.STRING) private RefreshTokenStatus status = RefreshTokenStatus.ACTIVE; + /** + * Timestamp of when this refresh token record was first created. Set automatically by Hibernate on insert and never + * updated afterwards. + */ @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; + /** + * Timestamp of the most recent update to this refresh token record. Updated automatically by Hibernate on every + * merge/flush. + */ @UpdateTimestamp @Column(name = "updated_at", nullable = false) private Instant updatedAt; diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java index b08248b..52bf5e3 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java @@ -11,6 +11,21 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +/** + * JPA entity representing a registered user in the SmartJam platform. + * + *

Mapped to the {@code users} database table. The primary key is a UUID generated automatically by the persistence + * provider. Equality and hash-code are based solely on {@link #id} to ensure correct behavior in JPA-managed + * collections and during entity detachment/reattachment cycles. + * + *

The {@code email} field is the unique login identifier. {@code nickname} is a non-unique display name. Passwords + * are never stored in plain text — only the hashed value is persisted in {@code passwordHash}. + * + *

Persistence exceptions: Attempting to persist or merge an instance with a duplicate {@code email}, a + * {@code null} required field, or a value that violates a column constraint will cause the underlying JPA provider to + * throw a {@link jakarta.persistence.PersistenceException} (typically wrapped by Spring as + * {@link org.springframework.dao.DataIntegrityViolationException}). + */ @Setter @Getter @NoArgsConstructor @@ -20,41 +35,63 @@ @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class UserEntity { + /** Unique identifier for the user, generated automatically as a UUID. */ @Id @GeneratedValue(strategy = GenerationType.UUID) @EqualsAndHashCode.Include private UUID id; + /** Non-unique display name (nickname) of the user shown in the UI. Must not be {@code null}. */ @Column(nullable = false) private String nickname; + /** + * The user's email address, used as the unique login identifier. Must be a valid email format, non-null, and unique + * across all users. + */ @Column(nullable = false, unique = true, length = 255) @Email private String email; + /** Bcrypt (or equivalent) hash of the user's password. The plain-text password is never stored. */ @Column(name = "password_hash", nullable = false, length = 255) private String passwordHash; + /** Optional first name of the user. */ @Column(name = "first_name") private String firstName; + /** Optional last name of the user. */ @Column(name = "last_name") private String lastName; + /** Optional URL pointing to the user's avatar image. Supports long URLs (up to 2048 characters). */ @Column(name = "avatar_url", length = 2048) private String avatarUrl; + /** + * The role of the user, determining their permissions within the platform. Defaults to {@link Role#STUDENT} for all + * newly created users. Persisted as a {@link String} in the database. + */ @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role = Role.STUDENT; + /** Optional Firebase Cloud Messaging token used to send push notifications to the user's device. */ @Column(name = "fcm_token") private String fcmToken; + /** + * Timestamp of when the user record was first created. Set automatically by Hibernate on insert and never updated + * afterwards. + */ @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; + /** + * Timestamp of the most recent update to the user record. Updated automatically by Hibernate on every merge/flush. + */ @UpdateTimestamp @Column(name = "updated_at", nullable = false) private Instant updatedAt; diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/RefreshTokenStatus.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/RefreshTokenStatus.java index d45d56a..862708b 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/RefreshTokenStatus.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/RefreshTokenStatus.java @@ -1,6 +1,17 @@ package com.smartjam.smartjamapi.enums; +/** + * Represents the lifecycle status of a refresh token stored in the database. + * + *

Status values are persisted as {@link String} via {@link jakarta.persistence.EnumType#STRING} in the + * {@code refresh_tokens} table. + */ public enum RefreshTokenStatus { + /** The refresh token is valid and can be used to obtain a new access token. */ ACTIVE, + /** + * The refresh token is no longer valid and cannot be used for token refresh. This covers tokens that have been + * revoked, used, or expired. + */ INACTIVE } diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java index 0e1a3f7..8f80849 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/Role.java @@ -1,5 +1,11 @@ package com.smartjam.smartjamapi.enums; +/** + * Represents the role of a user within the SmartJam platform. + * + *

Roles are used to control access and permissions across the application. The role value is persisted as a {`@link` + * String} in the database via {`@link` jakarta.persistence.EnumType#STRING}. + */ public enum Role { STUDENT, TEACHER diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java index 3d41710..287ea0c 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/RefreshTokenRepository.java @@ -6,20 +6,80 @@ import com.smartjam.smartjamapi.entity.RefreshTokenEntity; import com.smartjam.smartjamapi.entity.UserEntity; import com.smartjam.smartjamapi.enums.RefreshTokenStatus; +import org.springframework.dao.DataAccessException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +/** + * Spring Data JPA repository for {@link RefreshTokenEntity} persistence operations. + * + *

In addition to standard CRUD operations inherited from {@link JpaRepository}, this repository provides token-hash + * lookups and bulk JPQL {@code UPDATE} statements for efficient status transitions without loading full entity graphs. + * + *

All {@link Modifying} methods use {@code clearAutomatically = true} and {@code flushAutomatically = true} to + * ensure the persistence context is flushed before and cleared after each bulk update, preventing stale first-level + * cache entries from being read after the operation. + */ public interface RefreshTokenRepository extends JpaRepository { - Optional findByTokenHash(String token); + /** + * Finds a refresh token record by the hash of the raw token string. + * + *

+ * + * @param tokenHash the SHA-256 hex digest (or equivalent hash) of the refresh token; must not be {@code null} + * @return an {@link Optional} containing the matching {@link RefreshTokenEntity}, or {@link Optional#empty()} if no + * record with the given hash exists + * @throws IllegalArgumentException if {@code token} is {@code null} + * @throws DataAccessException if a database access error occurs (e.g. connection failure, query execution error) + */ + Optional findByTokenHash(String tokenHash); + + /** + * Updates the status of the refresh token identified by the given token hash. + * + *

This is a bulk JPQL {@code UPDATE} that does not load the entity into the persistence context, making it more + * efficient than a find-then-save pattern. + * + *

+ * + * @param tokenHash the SHA-256 hex digest of the refresh token whose status should be updated; must not be + * {@code null} + * @param status the new {@link RefreshTokenStatus} to set; must not be {@code null} + * @return the number of rows affected by the update (0 or 1) + * @throws IllegalArgumentException if {@code tokenHash} or {@code status} is {@code null} + * @throws org.springframework.dao.DataAccessException if a database access error occurs during the update + * @throws jakarta.persistence.TransactionRequiredException if called outside an active transaction (normally + * prevented by the {@code @Transactional} annotation on this method) + */ @Transactional @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE RefreshTokenEntity r SET r.status = :status WHERE r.tokenHash = :tokenHash") int setStatusByTokenHash(@Param("tokenHash") String tokenHash, @Param("status") RefreshTokenStatus status); + /** + * Bulk-updates the status of all refresh tokens belonging to the given user whose current status matches + * {@code currentStatus}. + * + *

Typically used to invalidate all active tokens for a user on logout or password change, by transitioning them + * from {@link RefreshTokenStatus#ACTIVE} to {@link RefreshTokenStatus#INACTIVE}. + * + *

+ * + * @param user the owner {@link UserEntity} whose tokens should be updated; must not be {@code null} + * @param currentStatus the status that tokens must currently have to be included in the update; must not be + * {@code null} + * @param newStatus the new {@link RefreshTokenStatus} to assign; must not be {@code null} + * @return the number of rows affected by the update + * @throws IllegalArgumentException if any of {@code user}, {@code currentStatus}, or {@code newStatus} is + * {@code null} + * @throws org.springframework.dao.DataAccessException if a database access error occurs during the update + * @throws jakarta.persistence.TransactionRequiredException if called outside an active transaction (normally + * prevented by the {@code @Transactional} annotation on this method) + */ @Transactional @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java index dfb51c8..9574072 100644 --- a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/repository/UserRepository.java @@ -6,9 +6,42 @@ import com.smartjam.smartjamapi.entity.UserEntity; import org.springframework.data.jpa.repository.JpaRepository; +/** + * Spring Data JPA repository for {@link UserEntity} persistence operations. + * + *

Provides standard CRUD operations inherited from {@link JpaRepository}, as well as derived query methods for + * email-based lookups used during authentication and registration flows. + */ public interface UserRepository extends JpaRepository { + /** + * Finds a user by their email address. + * + *

+ * + * @param email the email address to search for; must not be {@code null} + * @return an {@link Optional} containing the matching {@link UserEntity}, or {@link Optional#empty()} if no user + * with the given email exists + * @throws IllegalArgumentException if {@code email} is {@code null} + * @throws org.springframework.dao.DataAccessException if a database access error occurs (e.g. connection failure, + * query execution error); this is a Spring-translated unchecked exception wrapping the underlying + * {@link jakarta.persistence.PersistenceException} + */ Optional findByEmail(String email); + /** + * Checks whether a user with the given email address already exists. + * + *

Prefer this method over {@link #findByEmail(String)} when only presence needs to be verified, as it avoids + * loading the full entity. + * + *

+ * + * @param email the email address to check; must not be {@code null} + * @return {@code true} if a user with the specified email exists, {@code false} otherwise + * @throws IllegalArgumentException if {@code email} is {@code null} + * @throws org.springframework.dao.DataAccessException if a database access error occurs (e.g. connection failure, + * query execution error) + */ boolean existsByEmail(String email); }